From 5fce840adc324398bec09236ad32f7296abbbf2a Mon Sep 17 00:00:00 2001 From: Yusuf Bera Ertan Date: Thu, 22 Aug 2024 23:54:03 +0300 Subject: [PATCH] feat: le guestbook has been added --- .gitignore | 7 ++ .vscode/settings.json | 3 + bun.lockb | Bin 142788 -> 144300 bytes guestbook/.bsp/sbt.json | 1 + guestbook/.scalafmt.conf | 2 + guestbook/build.sbt | 29 +++++ guestbook/project/build.properties | 1 + guestbook/project/metals.sbt | 8 ++ guestbook/project/plugins.sbt | 3 + guestbook/project/project/metals.sbt | 8 ++ guestbook/project/project/project/metals.sbt | 8 ++ guestbook/src/main/resources/logback.xml | 16 +++ .../systems/gaze/guestbook/Guestbook.scala | 65 +++++++++++ .../gaze/guestbook/GuestbookRoutes.scala | 66 +++++++++++ .../gaze/guestbook/GuestbookServer.scala | 35 ++++++ .../scala/systems/gaze/guestbook/Main.scala | 20 ++++ package.json | 2 + src/lib/fetchHack.mjs | 25 +++++ src/lib/getTitle.ts | 2 +- src/routes/+layout.svelte | 3 +- src/routes/guestbook/+page.server.ts | 58 ++++++++++ src/routes/guestbook/+page.svelte | 106 ++++++++++++++++++ src/styles/app.css | 12 +- static/icons/guestbook.png | Bin 0 -> 563 bytes svelte.config.js | 2 +- tailwind.config.js | 3 +- 26 files changed, 479 insertions(+), 6 deletions(-) create mode 100644 guestbook/.bsp/sbt.json create mode 100644 guestbook/.scalafmt.conf create mode 100644 guestbook/build.sbt create mode 100644 guestbook/project/build.properties create mode 100644 guestbook/project/metals.sbt create mode 100644 guestbook/project/plugins.sbt create mode 100644 guestbook/project/project/metals.sbt create mode 100644 guestbook/project/project/project/metals.sbt create mode 100644 guestbook/src/main/resources/logback.xml create mode 100644 guestbook/src/main/scala/systems/gaze/guestbook/Guestbook.scala create mode 100644 guestbook/src/main/scala/systems/gaze/guestbook/GuestbookRoutes.scala create mode 100644 guestbook/src/main/scala/systems/gaze/guestbook/GuestbookServer.scala create mode 100644 guestbook/src/main/scala/systems/gaze/guestbook/Main.scala create mode 100644 src/lib/fetchHack.mjs create mode 100644 src/routes/guestbook/+page.server.ts create mode 100644 src/routes/guestbook/+page.svelte create mode 100644 static/icons/guestbook.png diff --git a/.gitignore b/.gitignore index 2228764..98b8382 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,10 @@ vite.config.ts.timestamp-* # nix /result + +# scala +target +.metals +.bloop +guestbook/entries +guestbook/entries_size \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 223cce8..cd64238 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,8 @@ }, "editor.quickSuggestions": { "strings": "on" + }, + "files.watcherExclude": { + "**/target": true } } \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index ac191c84a6ab69db16a23e3bf220749d0c8ebf86..7ff8555ff3c3165ffe318e21255ba12ef846498e 100644 GIT binary patch delta 21152 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 JxW%;i{{U?HCZPZT delta 20530 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 diff --git a/guestbook/.bsp/sbt.json b/guestbook/.bsp/sbt.json new file mode 100644 index 0000000..c61f66e --- /dev/null +++ b/guestbook/.bsp/sbt.json @@ -0,0 +1 @@ +{"name":"sbt","version":"1.10.1","bspVersion":"2.1.0-M1","languages":["scala"],"argv":["C:\\Users\\dusk\\scoop\\apps\\graalvm-oracle-jdk\\current/bin/java","-Xms100m","-Xmx100m","-classpath","C:\\Users\\dusk\\AppData\\Local\\Coursier\\cache\\arc\\https\\github.com\\sbt\\sbt\\releases\\download\\v1.10.1\\sbt-1.10.1.zip\\sbt\\bin\\sbt-launch.jar","-Dsbt.script=C:\\Users\\dusk\\AppData\\Local\\Coursier\\data\\bin\\sbt.bat","xsbt.boot.Boot","-bsp"]} \ No newline at end of file diff --git a/guestbook/.scalafmt.conf b/guestbook/.scalafmt.conf new file mode 100644 index 0000000..8134e97 --- /dev/null +++ b/guestbook/.scalafmt.conf @@ -0,0 +1,2 @@ +version = "3.7.15" +runner.dialect = scala3 \ No newline at end of file diff --git a/guestbook/build.sbt b/guestbook/build.sbt new file mode 100644 index 0000000..283dc77 --- /dev/null +++ b/guestbook/build.sbt @@ -0,0 +1,29 @@ +val Http4sVersion = "0.23.27" +val CirceVersion = "0.14.9" +val MunitVersion = "1.0.0" +val LogbackVersion = "1.5.6" +val MunitCatsEffectVersion = "2.0.0" + +lazy val root = (project in file(".")) + .settings( + organization := "systems.gaze", + name := "guestbook", + version := "0.0.1-SNAPSHOT", + scalaVersion := "3.4.2", + libraryDependencies ++= Seq( + "org.http4s" %% "http4s-ember-server" % Http4sVersion, + "org.http4s" %% "http4s-circe" % Http4sVersion, + "org.http4s" %% "http4s-dsl" % Http4sVersion, + "org.scalameta" %% "munit" % MunitVersion % Test, + "org.typelevel" %% "munit-cats-effect" % MunitCatsEffectVersion % Test, + "ch.qos.logback" % "logback-classic" % LogbackVersion % Runtime, + "com.lihaoyi" %% "os-lib" % "0.9.1", + "io.circe" %% "circe-core" % CirceVersion, + "io.circe" %% "circe-generic" % CirceVersion, + "io.circe" %% "circe-parser" % CirceVersion, + ), + assembly / assemblyMergeStrategy := { + case "module-info.class" => MergeStrategy.discard + case x => (assembly / assemblyMergeStrategy).value.apply(x) + } + ) diff --git a/guestbook/project/build.properties b/guestbook/project/build.properties new file mode 100644 index 0000000..ee4c672 --- /dev/null +++ b/guestbook/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.10.1 diff --git a/guestbook/project/metals.sbt b/guestbook/project/metals.sbt new file mode 100644 index 0000000..8d78f40 --- /dev/null +++ b/guestbook/project/metals.sbt @@ -0,0 +1,8 @@ +// format: off +// DO NOT EDIT! This file is auto-generated. + +// This file enables sbt-bloop to create bloop config files. + +addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.6.0") + +// format: on diff --git a/guestbook/project/plugins.sbt b/guestbook/project/plugins.sbt new file mode 100644 index 0000000..e8b42c0 --- /dev/null +++ b/guestbook/project/plugins.sbt @@ -0,0 +1,3 @@ +addSbtPlugin("org.typelevel" % "sbt-tpolecat" % "0.5.1") +addSbtPlugin("io.spray" % "sbt-revolver" % "0.10.0") +addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.2.0") diff --git a/guestbook/project/project/metals.sbt b/guestbook/project/project/metals.sbt new file mode 100644 index 0000000..8d78f40 --- /dev/null +++ b/guestbook/project/project/metals.sbt @@ -0,0 +1,8 @@ +// format: off +// DO NOT EDIT! This file is auto-generated. + +// This file enables sbt-bloop to create bloop config files. + +addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.6.0") + +// format: on diff --git a/guestbook/project/project/project/metals.sbt b/guestbook/project/project/project/metals.sbt new file mode 100644 index 0000000..8d78f40 --- /dev/null +++ b/guestbook/project/project/project/metals.sbt @@ -0,0 +1,8 @@ +// format: off +// DO NOT EDIT! This file is auto-generated. + +// This file enables sbt-bloop to create bloop config files. + +addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.6.0") + +// format: on diff --git a/guestbook/src/main/resources/logback.xml b/guestbook/src/main/resources/logback.xml new file mode 100644 index 0000000..8ea8412 --- /dev/null +++ b/guestbook/src/main/resources/logback.xml @@ -0,0 +1,16 @@ + + + + true + + [%thread] %highlight(%-5level) %cyan(%logger{15}) - %msg %n + + + + + + diff --git a/guestbook/src/main/scala/systems/gaze/guestbook/Guestbook.scala b/guestbook/src/main/scala/systems/gaze/guestbook/Guestbook.scala new file mode 100644 index 0000000..214b7ca --- /dev/null +++ b/guestbook/src/main/scala/systems/gaze/guestbook/Guestbook.scala @@ -0,0 +1,65 @@ +package systems.gaze.guestbook + +import cats.Applicative +import cats.syntax.all.* +import io.circe.Decoder +import io.circe.generic.auto._, io.circe.syntax._, io.circe._, io.circe.parser._ +import os.Path + +trait Guestbook[F[_]]: + def read(config: Guestbook.Config, from: Int, count: Int): F[Guestbook.Page] + def write(config: Guestbook.Config, entry: Guestbook.Entry): F[Unit] + +object Guestbook: + private val logger = org.log4s.getLogger + + final case class Config( + val entriesPath: Path, + val entryCountPath: Path, + ) + final case class Entry( + val author: String, + val content: String, + val timestamp: Long + ) + final case class Page( + val entries: List[Tuple2[Int, Entry]], + val hasNext: Boolean, + ) + + def impl[F[_]: Applicative]: Guestbook[F] = new Guestbook[F]: + def entriesSize(config: Config): Int = + os.read(config.entryCountPath).toInt + def read(config: Config, from: Int, count: Int): F[Page] = + val entryCount = entriesSize(config) + // limit from to 1 cuz duh lol + val startFrom = from.max(1) + // limit count to however many entries there are + val endAt = (startFrom + count - 1).min(entryCount).max(1) + // if it wants us to start from after entries just return empty + if startFrom > entryCount then + return Page(entries = List.empty, hasNext = false).pure[F] + logger.trace(s"want to read entries from $startFrom ($from) to $endAt ($from + $count)") + // actually get the entries + val entries = (startFrom to endAt) + .map((no) => // read the entries + val entryNo = entryCount - no + 1 + logger.trace(s"reading entry at $entryNo") + entryNo -> decode[Entry](os.read(config.entriesPath / entryNo.toString)).getOrElse( + Entry( + author = "error", + content = "woops, this is an error!", + timestamp = 0 + ) + ) + ) + .toList + Page(entries, hasNext = entryCount > endAt).pure[F] + def write(config: Config, entry: Entry): F[Unit] = + val entryNo = entriesSize(config) + 1 + val entryPath = config.entriesPath / entryNo.toString + // write entry + os.write.over(entryPath, entry.asJson.toString) + // update entry count + os.write.over(config.entryCountPath, entryNo.toString) + ().pure[F] diff --git a/guestbook/src/main/scala/systems/gaze/guestbook/GuestbookRoutes.scala b/guestbook/src/main/scala/systems/gaze/guestbook/GuestbookRoutes.scala new file mode 100644 index 0000000..dc0c608 --- /dev/null +++ b/guestbook/src/main/scala/systems/gaze/guestbook/GuestbookRoutes.scala @@ -0,0 +1,66 @@ +package systems.gaze.guestbook + +import io.circe.generic.auto._ +import org.http4s.HttpRoutes +import org.http4s.dsl.Http4sDsl +import org.http4s.circe.CirceEntityEncoder.circeEntityEncoder +import org.http4s.UrlForm +import org.http4s.headers.Location +import cats.effect.IO +import cats.implicits.* +import org.http4s.server.middleware.Throttle +import scala.concurrent.duration.DurationInt +import cats.effect.unsafe.implicits.global +import scala.concurrent.duration.FiniteDuration +import org.http4s.Uri + +class GuestbookRoutes(var guestbookConfig: Guestbook.Config, var websiteUri: Uri): + val dsl = new Http4sDsl[IO] {} + import dsl.* + + def throttle( + ratelimited: String, + amount: Int, + per: FiniteDuration + )(routes: HttpRoutes[IO]): HttpRoutes[IO] = + Throttle + .httpRoutes[IO](amount, per)(routes) + .unsafeRunSync() + .map((resp) => + // respond with SeeOther and put ratelimited query param + if (resp.status == TooManyRequests) + resp + .withStatus(SeeOther) + .withHeaders( + Location( + (websiteUri / "guestbook") + .withQueryParam("ratelimited", ratelimited) + ) + ) + else + resp + ) + + def routes( + G: Guestbook[IO] + ): HttpRoutes[IO] = + val putEntry = HttpRoutes.of[IO] { + case req @ POST -> Root => for { + entry <- req.as[UrlForm].map { form => + val author = form.getFirstOrElse("author", "error") + val content = form.getFirstOrElse("content", "error") + Guestbook + .Entry(author, content, timestamp = System.currentTimeMillis / 1000) + } + result <- G.write(guestbookConfig, entry) + resp <- SeeOther(Location(websiteUri / "guestbook")) + } yield resp + } + val getEntries = HttpRoutes.of[IO] { + case GET -> Root / IntVar(page) => for { + entries <- G.read(guestbookConfig, (page - 1).max(0) * 5, 5) + resp <- Ok(entries) + } yield resp + } + throttle("get", 10, 2.seconds)(getEntries) + <+> throttle("send", 5, 10.seconds)(putEntry) diff --git a/guestbook/src/main/scala/systems/gaze/guestbook/GuestbookServer.scala b/guestbook/src/main/scala/systems/gaze/guestbook/GuestbookServer.scala new file mode 100644 index 0000000..0a83a9a --- /dev/null +++ b/guestbook/src/main/scala/systems/gaze/guestbook/GuestbookServer.scala @@ -0,0 +1,35 @@ +package systems.gaze.guestbook + +import com.comcast.ip4s.* +import fs2.io.net.Network +import org.http4s.ember.server.EmberServerBuilder +import org.http4s.implicits.* +import org.http4s.server.middleware.Logger +import cats.effect.IO +import org.http4s.Uri + +object GuestbookServer: + + def run( + guestbookConfig: Guestbook.Config, + websiteUri: Uri, + ): IO[Nothing] = { + val guestbookAlg = Guestbook.impl[IO] + + // Combine Service Routes into an HttpApp. + // Can also be done via a Router if you + // want to extract segments not checked + // in the underlying routes. + val httpApp = + (GuestbookRoutes(guestbookConfig, websiteUri).routes(guestbookAlg)).orNotFound + + // With Middlewares in place + val finalHttpApp = Logger.httpApp(true, true)(httpApp) + + EmberServerBuilder + .default[IO] + .withHost(ipv4"0.0.0.0") + .withPort(port"8080") + .withHttpApp(finalHttpApp) + .build + }.useForever diff --git a/guestbook/src/main/scala/systems/gaze/guestbook/Main.scala b/guestbook/src/main/scala/systems/gaze/guestbook/Main.scala new file mode 100644 index 0000000..3ef23b7 --- /dev/null +++ b/guestbook/src/main/scala/systems/gaze/guestbook/Main.scala @@ -0,0 +1,20 @@ +package systems.gaze.guestbook + +import cats.effect.IOApp +import org.http4s.Uri + +object Main extends IOApp.Simple: + val websiteUri = + Uri + .fromString(sys.env("GUESTBOOK_WEBSITE_URI")) + .getOrElse(throw new Exception("write better uris lol")) + val guestbookConfig = Guestbook.Config( + entriesPath = os.pwd / "entries", + entryCountPath = os.pwd / "entries_size" + ) + // make the entries path if it doesnt exist + os.makeDir.all(guestbookConfig.entriesPath) + // make the entry count path if it doesnt exist + if !os.exists(guestbookConfig.entryCountPath) then + os.write(guestbookConfig.entryCountPath, 0.toString) + val run = GuestbookServer.run(guestbookConfig, websiteUri) diff --git a/package.json b/package.json index 43f6519..f5eb99b 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,10 @@ "@sveltejs/adapter-static": "^3.0.2", "@sveltejs/kit": "^2.5.20", "@sveltejs/vite-plugin-svelte": "^3.1.1", + "@tailwindcss/forms": "^0.5.7", "@tailwindcss/typography": "^0.5.14", "@types/eslint": "^8.56.11", + "@types/node": "^22.4.2", "autoprefixer": "^10.4.20", "eslint": "^9.8.0", "eslint-config-prettier": "^9.1.0", diff --git a/src/lib/fetchHack.mjs b/src/lib/fetchHack.mjs new file mode 100644 index 0000000..e8d25c2 --- /dev/null +++ b/src/lib/fetchHack.mjs @@ -0,0 +1,25 @@ +// hack/postSync.mjs +/** + ,* @file POST some stuff to a URL. + ,* Usage: one of + ,* echo input | node postSync.mjs + ,* node postSync.mjs + ,*/ + +// The argument count would break if called as a standalone script. +const url = process.argv[2] || process.exit(1); + +const response = await fetch(url, {method:'GET',redirect:'manual'}); +const json = await response.text().then(text => { + try { + const data = JSON.parse(text); + return data + } catch(err) { + return [] + } +}); +console.log(JSON.stringify({ + location: response.headers.get('location'), + status: response.status, + body: json, +})); \ No newline at end of file diff --git a/src/lib/getTitle.ts b/src/lib/getTitle.ts index 76a8a14..f2c51a9 100644 --- a/src/lib/getTitle.ts +++ b/src/lib/getTitle.ts @@ -1,7 +1,7 @@ const getTitle = (path: string) => { let sl = path.split('/') sl = sl.splice(1) - if (sl.length > 1) { + if (sl.length > 2) { sl[0] = sl[0][0] } const newPath = sl.join('/') diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 1af81d3..d4d9acd 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -14,7 +14,8 @@ let menuItems: MenuItem[] = [ { href: '', name: 'home', iconUri: '/icons/home.png' }, { href: 'entries', name: 'entries', iconUri: '/icons/entries.png' }, - { href: 'about', name: 'about', iconUri: '/icons/about.png' } + { href: 'guestbook', name: 'guestbook', iconUri: '/icons/guestbook.png' }, + { href: 'about', name: 'about', iconUri: '/icons/about.png' }, ]; const routeComponents = data.route.split('/'); diff --git a/src/routes/guestbook/+page.server.ts b/src/routes/guestbook/+page.server.ts new file mode 100644 index 0000000..abb7b83 --- /dev/null +++ b/src/routes/guestbook/+page.server.ts @@ -0,0 +1,58 @@ +import { GUESTBOOK_URL } from '$env/static/private' +import { redirect } from '@sveltejs/kit'; +import {spawnSync} from 'node:child_process' + +interface Entry { + author: String, + content: String, + timestamp: number, +} + +interface FetchResult { + location: string, + status: number, + body: any, +} + +function fetchBlocking(url: string): FetchResult | string { + const spawnResult = spawnSync("bun", ["src/lib/fetchHack.mjs", url]); + const out = spawnResult.stdout.toString(); + try { + return JSON.parse(out) + } catch(err: any) { + return spawnResult.stderr.toString() + } +} + +export function load({ url }) { + var data = { + entries: [] as [number, Entry][], + guestbook_url: GUESTBOOK_URL, + ratelimitedFeat: url.searchParams.get('ratelimited') as string || "", + page: parseInt(url.searchParams.get('page') || "1") || 1, + hasNext: false, + fetchError: "", + } + // handle cases where the page query might be a string so we just return back page 1 instead + data.page = isNaN(data.page) ? 1 : data.page + data.page = Math.max(data.page, 1) + if (data.ratelimitedFeat === "get") { + return data + } + const entriesResp = fetchBlocking(GUESTBOOK_URL + "/" + data.page) + if (typeof entriesResp === "string") { + data.fetchError = entriesResp + return data + } + const locationRaw = entriesResp.status === 303 ? entriesResp.location : null + if (locationRaw !== null && locationRaw.length > 0) { + const location = new URL(locationRaw) + data.ratelimitedFeat = location.searchParams.get('ratelimited') as string || "" + } + if (data.ratelimitedFeat === "get") { + return data + } + data.entries = entriesResp.body.entries + data.hasNext = entriesResp.body.hasNext + return data +} \ No newline at end of file diff --git a/src/routes/guestbook/+page.svelte b/src/routes/guestbook/+page.svelte new file mode 100644 index 0000000..f024858 --- /dev/null +++ b/src/routes/guestbook/+page.svelte @@ -0,0 +1,106 @@ + + +
+
+ + {@const ratelimited = data.ratelimitedFeat === 'send'} +
+

hia, here is the guestbook if you wanna post anything :)

+

just be a good human bean pretty please

+
+
+
+

###

+

...

+
+