feat: improved guestbook

This commit is contained in:
dusk 2024-08-23 16:44:40 +03:00
parent c3fcb13b63
commit 9149b4b439
Signed by: dusk
SSH Key Fingerprint: SHA256:Abmvag+juovVufZTxyWY8KcVgrznxvBjQpJesv071Aw
11 changed files with 71 additions and 97 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -14,38 +14,26 @@ import cats.effect.unsafe.implicits.global
import scala.concurrent.duration.FiniteDuration import scala.concurrent.duration.FiniteDuration
import org.http4s.Uri import org.http4s.Uri
class GuestbookRoutes(var guestbookConfig: Guestbook.Config, var websiteUri: Uri): class GuestbookRoutes(
var guestbookConfig: Guestbook.Config,
var websiteUri: Uri
):
val dsl = new Http4sDsl[IO] {} val dsl = new Http4sDsl[IO] {}
import dsl.* import dsl.*
def throttle( def throttle(
ratelimited: String,
amount: Int, amount: Int,
per: FiniteDuration per: FiniteDuration
)(routes: HttpRoutes[IO]): HttpRoutes[IO] = )(routes: HttpRoutes[IO]): HttpRoutes[IO] =
Throttle Throttle
.httpRoutes[IO](amount, per)(routes) .httpRoutes[IO](amount, per)(routes)
.unsafeRunSync() .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( def routes(
G: Guestbook[IO] G: Guestbook[IO]
): HttpRoutes[IO] = ): HttpRoutes[IO] =
val putEntry = HttpRoutes.of[IO] { val putEntry = HttpRoutes.of[IO] { case req @ POST -> Root =>
case req @ POST -> Root => for { for {
entry <- req.as[UrlForm].map { form => entry <- req.as[UrlForm].map { form =>
val author = form.getFirstOrElse("author", "error") val author = form.getFirstOrElse("author", "error")
val content = form.getFirstOrElse("content", "error") val content = form.getFirstOrElse("content", "error")
@ -56,11 +44,11 @@ class GuestbookRoutes(var guestbookConfig: Guestbook.Config, var websiteUri: Uri
resp <- SeeOther(Location(websiteUri / "guestbook")) resp <- SeeOther(Location(websiteUri / "guestbook"))
} yield resp } yield resp
} }
val getEntries = HttpRoutes.of[IO] { val getEntries = HttpRoutes.of[IO] { case GET -> Root / IntVar(page) =>
case GET -> Root / IntVar(page) => for { for {
entries <- G.read(guestbookConfig, (page - 1).max(0) * 5, 5) entries <- G.read(guestbookConfig, (page - 1).max(0) * 5, 5)
resp <- Ok(entries) resp <- Ok(entries)
} yield resp } yield resp
} }
throttle("get", 10, 2.seconds)(getEntries) throttle(30, 2.seconds)(getEntries)
<+> throttle("send", 5, 10.seconds)(putEntry) <+> throttle(5, 10.seconds)(putEntry)

View File

@ -12,8 +12,6 @@
"format": "prettier --write ." "format": "prettier --write ."
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^3.2.2",
"@sveltejs/adapter-static": "^3.0.2",
"@sveltejs/kit": "^2.5.20", "@sveltejs/kit": "^2.5.20",
"@sveltejs/vite-plugin-svelte": "^3.1.1", "@sveltejs/vite-plugin-svelte": "^3.1.1",
"@tailwindcss/forms": "^0.5.7", "@tailwindcss/forms": "^0.5.7",
@ -45,6 +43,9 @@
"rehype-slug": "^6.0.0" "rehype-slug": "^6.0.0"
}, },
"trustedDependencies": [ "trustedDependencies": [
"@sveltejs/kit",
"deasync",
"esbuild",
"svelte-preprocess" "svelte-preprocess"
] ]
} }

View File

@ -1,25 +0,0 @@
// hack/postSync.mjs
/**
,* @file POST some stuff to a URL.
,* Usage: one of
,* echo input | node postSync.mjs <url>
,* node postSync.mjs <url> <input>
,*/
// 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,
}));

View File

@ -1 +0,0 @@
// place files you want to import through the `$lib` alias in this folder.

View File

@ -1,8 +1,6 @@
import { dev } from '$app/environment'; export const csr = false;
export const csr = dev;
export const ssr = true; export const ssr = true;
export const prerender = true; export const prerender = false;
export const trailingSlash = 'always'; export const trailingSlash = 'always';
export async function load({ url }) { export async function load({ url }) {

1
src/routes/+page.ts Normal file
View File

@ -0,0 +1 @@
export const prerender = true;

View File

@ -0,0 +1 @@
export const prerender = true;

View File

@ -0,0 +1 @@
export const prerender = true;

View File

@ -1,6 +1,6 @@
import { GUESTBOOK_URL } from '$env/static/private' import { GUESTBOOK_BASE_URL } from '$env/static/private'
import { redirect } from '@sveltejs/kit'; import { PUBLIC_BASE_URL } from '$env/static/public'
import {spawnSync} from 'node:child_process' import { redirect } from '@sveltejs/kit'
interface Entry { interface Entry {
author: String, author: String,
@ -8,51 +8,56 @@ interface Entry {
timestamp: number, timestamp: number,
} }
interface FetchResult { export const actions = {
location: string, default: async ({ request, cookies }) => {
status: number, const body = await request.text()
body: any, let respRaw: Response
}
function fetchBlocking(url: string): FetchResult | string {
const spawnResult = spawnSync("bun", ["src/lib/fetchHack.mjs", url]);
const out = spawnResult.stdout.toString();
try { try {
return JSON.parse(out) respRaw = await fetch(`${GUESTBOOK_BASE_URL}`, { method: 'POST', body })
} catch (err: any) { } catch (err: any) {
return spawnResult.stderr.toString() cookies.set("sendError", err.toString(), { path: "/guestbook" })
redirect(303, `${PUBLIC_BASE_URL}/guestbook/`)
}
const ratelimited = respRaw.status === 429
cookies.set("sendRatelimited", ratelimited.toString(), { path: "/guestbook" })
redirect(303, `${PUBLIC_BASE_URL}/guestbook/`)
} }
} }
export function load({ url }) { export async function load({ url, fetch, cookies }) {
var data = { var data = {
entries: [] as [number, Entry][], entries: [] as [number, Entry][],
guestbook_url: GUESTBOOK_URL, page: parseInt(url.searchParams.get('page') || "1"),
ratelimitedFeat: url.searchParams.get('ratelimited') as string || "",
page: parseInt(url.searchParams.get('page') || "1") || 1,
hasNext: false, hasNext: false,
fetchError: "", sendError: cookies.get("sendError") || "",
getError: "",
sendRatelimited: cookies.get('sendRatelimited') || "",
getRatelimited: false,
} }
// delete the cookies after we get em since we dont really need these more than once
cookies.delete("sendError", { path: "/guestbook" })
cookies.delete("sendRatelimited", { path: "/guestbook" })
// handle cases where the page query might be a string so we just return back page 1 instead // 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 = isNaN(data.page) ? 1 : data.page
data.page = Math.max(data.page, 1) data.page = Math.max(data.page, 1)
if (data.ratelimitedFeat === "get") { let respRaw: Response
try {
respRaw = await fetch(GUESTBOOK_BASE_URL + "/" + data.page)
} catch (err: any) {
data.getError = err.toString()
return data return data
} }
const entriesResp = fetchBlocking(GUESTBOOK_URL + "/" + data.page) data.getRatelimited = respRaw.status === 429
if (typeof entriesResp === "string") { if (!data.getRatelimited) {
data.fetchError = entriesResp let body: any
try {
body = await respRaw.json()
} catch (err: any) {
data.getError = err.toString()
return data return data
} }
const locationRaw = entriesResp.status === 303 ? entriesResp.location : null data.entries = body.entries
if (locationRaw !== null && locationRaw.length > 0) { data.hasNext = body.hasNext
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 return data
} }

View File

@ -2,7 +2,6 @@
import Window from '../../components/window.svelte'; import Window from '../../components/window.svelte';
export let data; export let data;
const hasPreviousPage = data.page > 1; const hasPreviousPage = data.page > 1;
const hasNextPage = data.hasNext; const hasNextPage = data.hasNext;
</script> </script>
@ -10,11 +9,10 @@
<div class="flex flex-row flex-wrap"> <div class="flex flex-row flex-wrap">
<div class="fixed"> <div class="fixed">
<Window title="guestbook"> <Window title="guestbook">
{@const ratelimited = data.ratelimitedFeat === 'send'}
<div class="flex flex-col gap-4 w-[60ch]"> <div class="flex flex-col gap-4 w-[60ch]">
<p>hia, here is the guestbook if you wanna post anything :)</p> <p>hia, here is the guestbook if you wanna post anything :)</p>
<p>just be a good human bean pretty please</p> <p>just be a good human bean pretty please</p>
<form action={data.guestbook_url} method="post"> <form method="post">
<div class="entry"> <div class="entry">
<div class="flex flex-row"> <div class="flex flex-row">
<p class="place-self-start grow text-2xl font-monospace">###</p> <p class="place-self-start grow text-2xl font-monospace">###</p>
@ -42,10 +40,17 @@
value="post" value="post"
class="text-xl text-ralsei-green-light leading-5 motion-safe:hover:animate-bounce w-fit border-double border-4 p-1 pb-2" class="text-xl text-ralsei-green-light leading-5 motion-safe:hover:animate-bounce w-fit border-double border-4 p-1 pb-2"
/> />
{#if ratelimited} {#if data.sendRatelimited}
<p class="text-error self-center">you are ratelimited, try again in 30 seconds</p> <p class="text-error self-center">you are ratelimited, try again in 30 seconds</p>
{/if} {/if}
</div> </div>
{#if data.sendError}
<p class="text-error">got error trying to send post, pls tell me about this</p>
<details>
<summary>error</summary>
<p>{data.sendError}</p>
</details>
{/if}
</form> </form>
</div> </div>
</Window> </Window>
@ -53,15 +58,15 @@
<div class="grow" /> <div class="grow" />
<Window title="entries"> <Window title="entries">
<div class="flex flex-col gap-4 w-[60ch] max-w-[60ch]"> <div class="flex flex-col gap-4 w-[60ch] max-w-[60ch]">
{#if data.ratelimitedFeat === 'get'} {#if data.getRatelimited}
<p class="text-error"> <p class="text-error">
woops, looks like you are being ratelimited, try again in like half a minute :3 woops, looks like you are being ratelimited, try again in like half a minute :3
</p> </p>
{:else if data.fetchError} {:else if data.getError}
<p class="text-error">got error trying to fetch entries, pls tell me about this</p> <p class="text-error">got error trying to fetch entries, pls tell me about this</p>
<details> <details>
<summary>error</summary> <summary>error</summary>
<p>{data.fetchError}</p> <p>{data.getError}</p>
</details> </details>
{:else} {:else}
{#each data.entries as [entry_id, entry] (entry_id)} {#each data.entries as [entry_id, entry] (entry_id)}