feat: improved guestbook
This commit is contained in:
parent
c3fcb13b63
commit
9149b4b439
@ -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)
|
||||||
|
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -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,
|
|
||||||
}));
|
|
@ -1 +0,0 @@
|
|||||||
// place files you want to import through the `$lib` alias in this folder.
|
|
@ -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
1
src/routes/+page.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const prerender = true;
|
1
src/routes/about/+layout.ts
Normal file
1
src/routes/about/+layout.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const prerender = true;
|
1
src/routes/entries/+layout.ts
Normal file
1
src/routes/entries/+layout.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const prerender = true;
|
@ -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
|
||||||
}
|
}
|
@ -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)}
|
||||||
|
Loading…
Reference in New Issue
Block a user