diff --git a/build.sbt b/build.sbt index f8bd0d1..65a08e0 100644 --- a/build.sbt +++ b/build.sbt @@ -1,5 +1,4 @@ enablePlugins(ScalaJSPlugin) -// enablePlugins(ScalaJSBundlerPlugin) name := "app" scalaVersion := "2.13.7" @@ -9,3 +8,7 @@ scalaJSUseMainModuleInitializer := true libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "2.1.0" libraryDependencies += "me.shadaj" %%% "slinky-core" % "0.7.0" libraryDependencies += "me.shadaj" %%% "slinky-web" % "0.7.0" +libraryDependencies ++= Seq( + "io.circe" %%% "circe-core", + "io.circe" %%% "circe-parser" +).map(_ % "0.14.1") diff --git a/src/main/scala/app/Base.scala b/src/main/scala/app/Base.scala index 32ac6c6..2e9e261 100644 --- a/src/main/scala/app/Base.scala +++ b/src/main/scala/app/Base.scala @@ -6,10 +6,10 @@ import slinky.core.FunctionalComponent import slinky.web.html._ import slinky.core.facade.Hooks._ -import app.handlers.{Handler, Nothing, KeyHandling} +import app.handlers.{Handler, Nothing, KeyHandling, EventSignatures} object Base { - val handlers: List[Handler] = List(KeyHandling, Nothing) + val handlers: List[Handler] = List(KeyHandling, EventSignatures, Nothing) val component = FunctionalComponent[Unit] { props => val (typedValue, setTypedValue) = useState("") diff --git a/src/main/scala/app/handlers/EventSignatures.scala b/src/main/scala/app/handlers/EventSignatures.scala index 240710d..49caf21 100644 --- a/src/main/scala/app/handlers/EventSignatures.scala +++ b/src/main/scala/app/handlers/EventSignatures.scala @@ -1,21 +1,200 @@ package app.handlers +import scala.annotation.{nowarn, tailrec} +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future import scala.scalajs.js import slinky.core.FunctionalComponent import slinky.web.html._ import slinky.core.facade.Hooks._ import slinky.core.facade.Fragment -import io.circe._ -import io.circe.parser._ +import io.circe.{Json, HCursor} +import io.circe.parser.{parse} +import app.modules.Nostr import app.handlers.{Handler} +import app.components.{Item} object EventSignatures extends Handler { - override def handles(value: String): Boolean = false + val keymatcher = "^[a-f0-9]{64}$".r - override val component = FunctionalComponent[String] { props => - Fragment( - "nada" + def badProperties(c: HCursor): Seq[String] = Seq( + ( + c.get[Double]("kind").getOrElse[Double](-1) >= 0 match { + case true => None; + case false => Some("kind") + } + ), + ( + keymatcher.matches( + c.get[String]("pubkey").getOrElse("").toLowerCase() + ) match { + case true => None; + case false => Some("pubkey") + } + ), + ( + c.get[String]("content").exists((_) => true) match { + case true => None; + case false => Some("content") + } + ), + ( + c + .get[List[List[String]]]("tags") + .exists((_) => true) match { + case true => None; + case false => Some("tags") + } + ) + ) + .filter(res => res.isDefined) + .map(res => res.get) + + override def handles(value: String): Boolean = parse(value) match { + case Left(_) => false + case Right(json) => { + badProperties(json.hcursor).length < 4 + } + } + + type MaybeItem = Future[ + Either[slinky.core.TagMod[Nothing], slinky.core.TagMod[Nothing]] + ] + + @nowarn("cat=other") + def itemWrongProperties(evtj: String): MaybeItem = Future { + val c = parse(evtj).toOption.get.hcursor + val bad = badProperties(c) + + if (bad.length > 0) { + Left( + Item.component( + Item.props( + "event missing or wrong properties", + "", + bad.mkString(", ") + ) + ) + ) + } else { + Right(div()) + } + } + + @nowarn("cat=other") + def itemSerializedEvent(evtj: String): MaybeItem = Future { + val event: js.Dynamic = js.JSON.parse(evtj) + + Right( + Item.component( + Item.props( + "serialized event", + "according to nip-01 signature algorithm", + Nostr.serializeEvent(event) + ) + ) ) } + + @nowarn("cat=other") + def itemEventId(evtj: String): MaybeItem = Future { + val event: js.Dynamic = js.JSON.parse(evtj) + + Right( + Item.component( + Item.props( + "event id", + "sha256 hash of serialized event", + Nostr.getEventHash(event) + ) + ) + ) + } + + @nowarn("cat=other") + def itemEventIdMatches(evtj: String): MaybeItem = Future { + val c = parse(evtj).toOption.get.hcursor + val event: js.Dynamic = js.JSON.parse(evtj) + + def render(result: Boolean) = Item.component( + Item.props( + "does the implied event id match the given event id?", + "if not, that means you're hashing the event uncorrectly", + f"${result match { + case true => "yes"; case false => "no" + }}" + ) + ) + + Nostr + .getEventHash(event) == (c.get[String]("id")).toOption.get match { + case true => Right(render(true)) + case false => Left(render(false)) + } + } + + @nowarn("cat=other") + def itemSignatureValid(evtj: String): MaybeItem = Future { + val event: js.Dynamic = js.JSON.parse(evtj) + + def render(result: Boolean) = Item.component( + Item.props( + "is signature valid?", + "", + f"${result match { + case true => "yes"; case false => "no" + }}" + ) + ) + + true match { + case true => Right(render(true)) + case false => Left(render(false)) + } + } + + val protoElements = List[(String) => MaybeItem]( + itemWrongProperties, + itemSerializedEvent, + itemEventId, + itemEventIdMatches, + itemSignatureValid + ) + + @nowarn("cat=other") + override val component = FunctionalComponent[String] { props => + val (elements, setElements) = + useState(Seq.empty[slinky.core.TagMod[Nothing]]) + + useEffect( + () => { + def runAndUnwrapUntilFirstLeft( + remaining: List[String => Future[ + Either[slinky.core.TagMod[Nothing], slinky.core.TagMod[Nothing]] + ]], + acc: List[slinky.core.TagMod[Nothing]] + ): Future[List[slinky.core.TagMod[Nothing]]] = remaining match { + case fn :: tail => { + fn(props) flatMap { res => + res match { + case Left(el) => runAndUnwrapUntilFirstLeft(Nil, el :: acc) + case Right(el) => runAndUnwrapUntilFirstLeft(tail, el :: acc) + } + } + } + case Nil => Future { acc.reverse } + } + + runAndUnwrapUntilFirstLeft(protoElements, List()) foreach { elements => + setElements(elements) + } + + () => {} + }, + Seq(props) + ) + + Fragment(elements: _*) + } } diff --git a/src/main/scala/app/modules/Nostr.scala b/src/main/scala/app/modules/Nostr.scala index 63483f4..bfb83c0 100644 --- a/src/main/scala/app/modules/Nostr.scala +++ b/src/main/scala/app/modules/Nostr.scala @@ -7,4 +7,8 @@ import scala.scalajs.js @JSGlobal object Nostr extends js.Object { def getPublicKey(text: String): String = js.native + def getEventHash(evt: js.Dynamic): String = js.native + def serializeEvent(evt: js.Dynamic): String = js.native + def verifySignature(evt: js.Dynamic): Boolean = js.native + def signEvent(evt: js.Dynamic, privateKey: String): String = js.native }