···11import type { Cookies } from '@sveltejs/kit'
22+import { hash } from 'crypto'
2334export const scopeCookies = (cookies: Cookies, path: string) => {
45 return {
···1213 cookies.delete(key, { ...props, path })
1314 }
1415 }
1616+}
1717+1818+const cipherChars = ['#', '%', '+', '=', '//']
1919+export const fancyText = (input: string) => {
2020+ const hashed = hash("sha256", input, "hex")
2121+ let result = ""
2222+ let idx = 0
2323+ while (idx < hashed.length) {
2424+ result += cipherChars[hashed.charCodeAt(idx) % cipherChars.length]
2525+ idx += 1
2626+ }
2727+ return result
1528}
+6
src/lib/visits.ts
···4141 return visitors
4242}
43434444+export const getVisitorId = (cookies: Cookies) => {
4545+ const scopedCookies = scopeCookies(cookies, '/')
4646+ // parse the last visit timestamp from cookies if it exists
4747+ return scopedCookies.get('visitorId')
4848+}
4949+4450// why not use this for incrementVisitCount? cuz i wanna have separate visit counts (one per hour and one per day, per hour being recent visitors)
4551const _addLastVisitor = (visitors: Map<string, Visitor>, request: Request, cookies: Cookies) => {
4652 const currentTime = Date.now()
+47-93
src/routes/guestbook/+page.server.ts
···11-import { env } from '$env/dynamic/private'
21import { redirect, type Cookies, type RequestEvent } from '@sveltejs/kit'
33-import auth from '$lib/guestbookAuth'
44-import { scopeCookies as _scopeCookies } from '$lib';
22+import { scopeCookies as _scopeCookies, fancyText } from '$lib';
53import { RetryAfterRateLimiter } from 'sveltekit-rate-limiter/server';
44+import { PUBLIC_BASE_URL } from '$env/static/public';
55+import { getBskyClient, getUserPosts } from '$lib/bluesky.js';
66+import { getVisitorId } from '$lib/visits';
77+import { nanoid } from 'nanoid';
88+import { noteFromBskyPost, type NoteData } from '../../components/note.svelte';
99+import { get, writable } from 'svelte/store';
610711export const prerender = false;
1212+1313+const callbackUrl = `${PUBLIC_BASE_URL}/guestbook/`
814915const createPostRatelimiter = new RetryAfterRateLimiter({
1016 IP: [10, 'd'],
1117 IPUA: [5, 'h'],
1218})
1313-1414-interface Entry {
1515- author: string,
1616- content: string,
1717- timestamp: number,
1818-}
19192020const scopeCookies = (cookies: Cookies) => {
2121 return _scopeCookies(cookies, '/guestbook')
2222}
23232424-const postAction = (client: any, scopes: string[]) => {
2525- return async (event: RequestEvent) => {
2424+const postTokens = writable<Set<string>>(new Set());
2525+2626+export const actions = {
2727+ post: async (event: RequestEvent) => {
2628 const { request, cookies } = event
2729 const scopedCookies = scopeCookies(cookies)
2828- scopedCookies.set("postAuth", client.name)
2930 const rateStatus = await createPostRatelimiter.check(event)
3031 if (rateStatus.limited) {
3132 scopedCookies.set("sendError", `you are being ratelimited sowwy :c, try again after ${rateStatus.retryAfter} seconds`)
3232- redirect(303, auth.callbackUrl)
3333+ redirect(303, callbackUrl)
3334 }
3435 const form = await request.formData()
3535- const content = form.get("content")?.toString().substring(0, 512)
3636- const anon = !(form.get("anon") === null)
3636+ const content = form.get("content")?.toString().substring(0, 300)
3737 if (content === undefined) {
3838 scopedCookies.set("sendError", "content field is missing")
3939- redirect(303, auth.callbackUrl)
3939+ redirect(303, callbackUrl)
4040 }
4141 // save form content in a cookie
4242- const params = new URLSearchParams({ content, anon: anon ? "1" : "" })
4343- scopedCookies.set("postData", params.toString())
4444- // get auth url to redirect user to
4545- const authUrl = auth.createAuthUrl((state) => client.getAuthUrl(state, scopes), cookies)
4646- redirect(303, authUrl)
4242+ scopedCookies.set("postData", content)
4343+ // create a token we will use to validate
4444+ const token = nanoid()
4545+ postTokens.update((set) => set.add(token))
4646+ scopedCookies.set("postAuth", token)
4747+ redirect(303, callbackUrl)
4748 }
4849}
49505050-export const actions = {
5151- post_indielogin: postAction(auth.indielogin, []),
5252- post_discord: postAction(auth.discord, ["identify"]),
5353- post_github: postAction(auth.github, []),
5454-}
5555-5656-export async function load({ url, fetch, cookies }) {
5151+export async function load({ url, cookies }) {
5752 const scopedCookies = scopeCookies(cookies)
5853 var data = {
5959- entries: [] as [number, Entry][],
6060- page: parseInt(url.searchParams.get('page') || "1"),
6161- hasNext: false,
5454+ entries: [] as NoteData[],
6255 sendError: scopedCookies.get("sendError") || "",
6356 getError: "",
6457 sendRatelimited: scopedCookies.get('sendRatelimited') || "",
6558 getRatelimited: false,
5959+ fillText: fancyText(getVisitorId(cookies) ?? nanoid()),
6660 }
6761 const rawPostData = scopedCookies.get("postData") || null
6862 const postAuth = scopedCookies.get("postAuth") || null
···7064 // delete the postData cookie after we got it cause we dont need it anymore
7165 scopedCookies.delete("postData")
7266 scopedCookies.delete("postAuth")
7373- // check if we are landing from an auth from a post action
7474- let code: string | null = null
7575- // try to get the code, fails if invalid oauth request
7676- try {
7777- code = auth.extractCode(url, cookies)
7878- } catch (err: any) {
7979- data.sendError = err.toString()
6767+ // get and validate token
6868+ if (!get(postTokens).has(postAuth)) {
6969+ scopedCookies.set("sendError", "invalid post token! this is either a bug or you should stop doing silly stuff")
7070+ redirect(303, callbackUrl)
8071 }
8181- // if we do have a code, then make the access token request
8282- const authClient = auth.getAuthClient(postAuth)
8383- if (authClient !== null && code !== null) {
8484- // get and validate access token, also get username
8585- let author: string
8686- try {
8787- const tokenResp = await authClient.getToken(code)
8888- author = await authClient.identifyToken(tokenResp)
8989- } catch(err: any) {
9090- scopedCookies.set("sendError", `oauth failed: ${err.toString()}`)
9191- redirect(303, auth.callbackUrl)
9292- }
9393- let respRaw: Response
9494- try {
9595- const postData = new URLSearchParams(rawPostData)
9696- const anon = (postData.get('anon') ?? "1").length > 0
9797- // set author to the identified value we got if not anonymous
9898- postData.set('author', anon ? "[REDACTED]" : author)
9999- // return error if content was not set or if empty
100100- const content = postData.get('content')
101101- if (content === null || content.trim().length === 0) {
102102- scopedCookies.set("sendError", `content field was empty`)
103103- redirect(303, auth.callbackUrl)
104104- }
105105- // set content, make sure to trim it
106106- postData.set('content', content.substring(0, 512).trim())
107107- respRaw = await fetch(env.GUESTBOOK_BASE_URL, { method: 'POST', body: postData })
108108- } catch (err: any) {
109109- scopedCookies.set("sendError", `${err.toString()} (is guestbook server running?)`)
110110- redirect(303, auth.callbackUrl)
111111- }
112112- if (respRaw.status === 429) {
113113- scopedCookies.set("sendRatelimited", "true")
7272+ // post entry
7373+ try {
7474+ // return error if content was not set or if empty
7575+ const content = rawPostData.substring(0, 300).trim()
7676+ if (content.length === 0) {
7777+ scopedCookies.set("sendError", `content field was empty`)
7878+ redirect(303, callbackUrl)
11479 }
115115- redirect(303, auth.callbackUrl)
8080+ // post to guestbook account
8181+ await (await getBskyClient()).post({text: content, threadgate: { allowMentioned: false, allowFollowing: false }});
8282+ } catch (err: any) {
8383+ scopedCookies.set("sendError", err.toString())
8484+ redirect(303, callbackUrl)
11685 }
8686+ redirect(303, callbackUrl)
11787 }
11888 // delete the cookies after we get em since we dont really need these more than once
11989 scopedCookies.delete("sendError")
12090 scopedCookies.delete("sendRatelimited")
121121- // handle cases where the page query might be a string so we just return back page 1 instead
122122- data.page = isNaN(data.page) ? 1 : data.page
123123- data.page = Math.max(data.page, 1)
124124- let respRaw: Response
9191+ // actually get posts
12592 try {
126126- const count = 5
127127- const offset = (data.page - 1) * count
128128- respRaw = await fetch(`${env.GUESTBOOK_BASE_URL}?offset=${offset}&count=${count}`)
9393+ const { posts } = await getUserPosts("did:web:guestbook.gaze.systems", 16)
9494+ data.entries = posts.map(noteFromBskyPost)
12995 } catch (err: any) {
130130- data.getError = `${err.toString()} (is guestbook server running?)`
131131- return data
9696+ data.getError = err.toString()
13297 }
133133- data.getRatelimited = respRaw.status === 429
134134- if (!data.getRatelimited) {
135135- let body: any
136136- try {
137137- body = await respRaw.json()
138138- } catch (err: any) {
139139- data.getError = `invalid body? (${err.toString()})`
140140- return data
141141- }
142142- data.entries = body.entries
143143- data.hasNext = body.hasNext
144144- }
9898+14599 return data
146100}