feat: add pushing notifs

This commit is contained in:
dusk 2025-04-03 14:05:38 +03:00
parent 305ca820d0
commit e9cbc93bf8
Signed by: dusk
SSH Key Fingerprint: SHA256:Abmvag+juovVufZTxyWY8KcVgrznxvBjQpJesv071Aw
4 changed files with 190 additions and 108 deletions

@ -2,6 +2,7 @@ import { getLastPosts } from '$lib/bluesky.js';
import { getNowPlaying } from '$lib/lastfm';
import { getLastGame } from '$lib/steam';
import { noteFromBskyPost } from '../components/note.svelte';
import { env } from '$env/dynamic/private';
export const load = async () => {
const lastTrack = getNowPlaying();
@ -16,6 +17,20 @@ export const load = async () => {
return { banners, lastTrack, lastGame, lastNote };
};
export const actions = {
pushnotif: async ({ request }: RequestEvent) => {
const form = await request.formData();
const content = encodeURIComponent(form.get('content')?.toString().substring(0, 100));
try {
fetch(
`https://api.day.app/${env.BARK_DEVICE_ID}/gaze.systems/${content}?icon=https://gaze.systems/icons/gaze_site.webp`
);
} catch (err) {
console.log(`failed to push notification: ${err}`);
}
}
};
const getBannerNo = (others: number[]) => {
const no = Math.floor(Math.random() * 20) + 1;
if (others.includes(no)) {

@ -15,14 +15,14 @@
let { data }: Props = $props();
</script>
<div class="flex flex-col md:flex-row gap-4 md:gap-8 md:h-full h-card">
<div class="flex flex-col gap-4 md:gap-8 ml-auto place-items-end">
<div class="flex flex-col md:flex-row gap-2 md:gap-4 md:h-full h-card">
<div class="flex flex-col gap-2 md:gap-6 ml-auto place-items-end">
<Window title="stuff im doing.." iconUri="/icons/msg_information.webp">
<div class="prose prose-ralsei prose-img:m-0 leading-6">
<LatestStuff />
</div>
</Window>
<Window style="md:mr-4" title="status" iconUri="/icons/msn.webp" removePadding>
<Window style="md:mr-8" title="status" iconUri="/icons/msn.webp" removePadding>
{#if data.lastNote}
<div class="m-1.5 flex flex-col font-monospace text-sm">
<p
@ -118,13 +118,31 @@
</div>
{/if}
</Window>
<Window title="cool stuff,,">
<Window title="notify me">
<form class="flex flex-row gap-1" method="post">
<input
type="text"
class="entry text-lg p-1 m-0 bg-transparent resize-none text-shadow-white placeholder-shown:[text-shadow:none] border-none"
name="content"
placeholder="push a notif into me~~"
maxlength="100"
required
/>
<input
type="submit"
value="send!!"
formaction="?/pushnotif"
class="entry text-ralsei-green-light leading-none hover:underline motion-safe:hover:animate-squiggle p-1 z-50"
/>
</form>
</Window>
<Window style="md:mr-2" title="cool stuff,,">
<div class="max-w-[50ch] prose prose-ralsei prose-a:!animate-none prose-img:m-0 leading-snug">
<CoolStuff />
</div>
</Window>
</div>
<div class="flex flex-col gap-4 md:gap-8 mr-auto w-full md:w-fit place-items-start">
<div class="flex flex-col gap-2 md:gap-6 mr-auto w-full md:w-fit place-items-start">
<Window title="links!" iconUri="/icons/contact.webp">
<div
class="[width:40ch] prose prose-ralsei prose-ul:px-[0.9rem] prose-ul:leading-[1.1rem] prose-headings:leading-none"
@ -272,3 +290,13 @@
</Window>
</div>
</div>
<style lang="postcss">
.entry {
@apply bg-ralsei-green-dark/70 border-ralsei-green-light/30 border-x-[4px] border-y-[5px];
border-style: ridge;
}
.entryflex {
@apply flex flex-row gap-1;
}
</style>

@ -1,4 +1,4 @@
import { redirect, type Cookies, type RequestEvent } from '@sveltejs/kit'
import { redirect, type Cookies, type RequestEvent } from '@sveltejs/kit';
import { scopeCookies as _scopeCookies, fancyText } from '$lib';
import { RetryAfterRateLimiter } from 'sveltekit-rate-limiter/server';
import { PUBLIC_BASE_URL } from '$env/static/public';
@ -10,92 +10,103 @@ import { get, writable } from 'svelte/store';
export const prerender = false;
const callbackUrl = `${PUBLIC_BASE_URL}/guestbook/`
const callbackUrl = `${PUBLIC_BASE_URL}/guestbook/`;
const createPostRatelimiter = new RetryAfterRateLimiter({
IP: [10, 'd'],
IPUA: [5, 'h'],
})
IP: [10, 'd'],
IPUA: [5, 'h']
});
const scopeCookies = (cookies: Cookies) => {
return _scopeCookies(cookies, '/guestbook')
}
return _scopeCookies(cookies, '/guestbook');
};
const postTokens = writable<Set<string>>(new Set());
export const actions = {
post: async (event: RequestEvent) => {
const { request, cookies } = event
const scopedCookies = scopeCookies(cookies)
const rateStatus = await createPostRatelimiter.check(event)
if (rateStatus.limited) {
scopedCookies.set("sendError", `you are being ratelimited sowwy :c, try again after ${rateStatus.retryAfter} seconds`)
redirect(303, callbackUrl)
}
const form = await request.formData()
const content = form.get("content")?.toString().substring(0, 300)
if (content === undefined) {
scopedCookies.set("sendError", "content field is missing")
redirect(303, callbackUrl)
}
// save form content in a cookie
scopedCookies.set("postData", content)
// create a token we will use to validate
const token = nanoid()
postTokens.update((set) => set.add(token))
scopedCookies.set("postAuth", token)
redirect(303, callbackUrl)
}
}
post: async (event: RequestEvent) => {
const { request, cookies } = event;
const scopedCookies = scopeCookies(cookies);
const rateStatus = await createPostRatelimiter.check(event);
if (rateStatus.limited) {
scopedCookies.set(
'sendError',
`you are being ratelimited sowwy :c, try again after ${rateStatus.retryAfter} seconds`
);
redirect(303, callbackUrl);
}
const form = await request.formData();
const content = form.get('content')?.toString().substring(0, 300);
if (content === undefined) {
scopedCookies.set('sendError', 'content field is missing');
redirect(303, callbackUrl);
}
// save form content in a cookie
scopedCookies.set('postData', content);
// create a token we will use to validate
const token = nanoid();
postTokens.update((set) => set.add(token));
scopedCookies.set('postAuth', token);
redirect(303, callbackUrl);
}
};
export async function load({ url, cookies }) {
const scopedCookies = scopeCookies(cookies)
var data = {
entries: [] as NoteData[],
sendError: scopedCookies.get("sendError") || "",
getError: "",
sendRatelimited: scopedCookies.get('sendRatelimited') || "",
getRatelimited: false,
fillText: fancyText(getVisitorId(cookies) ?? nanoid()),
}
const rawPostData = scopedCookies.get("postData") || null
const postAuth = scopedCookies.get("postAuth") || null
if (rawPostData !== null && postAuth !== null) {
// delete the postData cookie after we got it cause we dont need it anymore
scopedCookies.delete("postData")
scopedCookies.delete("postAuth")
// get and validate token
if (!get(postTokens).has(postAuth)) {
scopedCookies.set("sendError", "invalid post token! this is either a bug or you should stop doing silly stuff")
redirect(303, callbackUrl)
}
postTokens.update((set) => { set.delete(postAuth); return set })
// post entry
try {
// return error if content was not set or if empty
const content = rawPostData.substring(0, 300).trim()
if (content.length === 0) {
scopedCookies.set("sendError", `content field was empty`)
redirect(303, callbackUrl)
}
// post to guestbook account
await (await getBskyClient()).post({text: content, threadgate: { allowMentioned: false, allowFollowing: false }});
} catch (err: any) {
scopedCookies.set("sendError", err.toString())
redirect(303, callbackUrl)
}
redirect(303, callbackUrl)
}
// delete the cookies after we get em since we dont really need these more than once
scopedCookies.delete("sendError")
scopedCookies.delete("sendRatelimited")
// actually get posts
try {
const { posts } = await getUserPosts("did:web:guestbook.gaze.systems", 16)
data.entries = posts.map(noteFromBskyPost)
} catch (err: any) {
data.getError = err.toString()
}
const scopedCookies = scopeCookies(cookies);
let data = {
entries: [] as NoteData[],
sendError: scopedCookies.get('sendError') || '',
getError: '',
sendRatelimited: scopedCookies.get('sendRatelimited') || '',
getRatelimited: false,
fillText: fancyText(getVisitorId(cookies) ?? nanoid())
};
const rawPostData = scopedCookies.get('postData') || null;
const postAuth = scopedCookies.get('postAuth') || null;
if (rawPostData !== null && postAuth !== null) {
// delete the postData cookie after we got it cause we dont need it anymore
scopedCookies.delete('postData');
scopedCookies.delete('postAuth');
// get and validate token
if (!get(postTokens).has(postAuth)) {
scopedCookies.set(
'sendError',
'invalid post token! this is either a bug or you should stop doing silly stuff'
);
redirect(303, callbackUrl);
}
postTokens.update((set) => {
set.delete(postAuth);
return set;
});
// post entry
try {
// return error if content was not set or if empty
const content = rawPostData.substring(0, 300).trim();
if (content.length === 0) {
scopedCookies.set('sendError', `content field was empty`);
redirect(303, callbackUrl);
}
// post to guestbook account
await (
await getBskyClient()
).post({ text: content, threadgate: { allowMentioned: false, allowFollowing: false } });
} catch (err: any) {
scopedCookies.set('sendError', err.toString());
redirect(303, callbackUrl);
}
redirect(303, callbackUrl);
}
// delete the cookies after we get em since we dont really need these more than once
scopedCookies.delete('sendError');
scopedCookies.delete('sendRatelimited');
// actually get posts
try {
const { posts } = await getUserPosts('did:web:guestbook.gaze.systems', 16);
data.entries = posts.map(noteFromBskyPost);
} catch (err: any) {
data.getError = err.toString();
}
return data
}
return data;
}

@ -8,14 +8,6 @@
}
let { data }: Props = $props();
function resetEntriesAnimation() {
var el = document.getElementById('guestbookentries');
if (el === null) { return }
el.style.animation = 'none';
el.offsetHeight; /* trigger reflow */
el.style.animation = '';
}
</script>
<div class="flex flex-col-reverse md:flex-row gap-2 md:gap-4">
@ -24,7 +16,11 @@
<div class="prose prose-ralsei leading-6 entry p-2">
<p>hia, here is the guestbook if you wanna post anything :)</p>
<p>be a good human bean pretty please (and don't be shy!!!)</p>
<p class="text-sm italic">(to see all the entries, look <a href="https://bsky.app/profile/guestbook.gaze.systems">here</a>)</p>
<p class="text-sm italic">
(to see all the entries, look <a href="https://bsky.app/profile/guestbook.gaze.systems"
>here</a
>)
</p>
</div>
<form method="post">
<div class="entry entryflex">
@ -45,7 +41,8 @@
/>
<div class="marquee-wrapper entry text-ralsei-white/50">
<div class="marquee font-monospace">
<p class="text-shadow-none">{data.fillText}</p><p class="text-shadow-none">{data.fillText}</p>
<p class="text-shadow-none">{data.fillText}</p>
<p class="text-shadow-none">{data.fillText}</p>
</div>
</div>
</div>
@ -61,7 +58,13 @@
</form>
</div>
</Window>
<Window id='guestbookentries' style="mr-auto" title="entries" iconUri="/icons/entries.webp" removePadding>
<Window
id="guestbookentries"
style="mr-auto"
title="entries"
iconUri="/icons/entries.webp"
removePadding
>
<div class="flex flex-col gap-2 md:gap-4 2xl:w-[60ch]">
{#if data.getRatelimited}
<p class="text-error">
@ -80,21 +83,46 @@
prose-pre:!bg-ralsei-black prose-code:!bg-ralsei-black
"
>
<pre class="language-bash"><code class="language-bash"><nobr>
<Token v="[" punct/>gazesystems <Token v="/" keywd/><Token v="]$" punct/> <Token v="source" funct/> <Token v="scripts/log.nu" />
<br>
<Token v="[" punct/>gazesystems <Token v="/" keywd/><Token v="]$" punct/> <Token v="let" funct/> <Token v="entries"/> <Token v="=" punct/> <Token v="(" punct/><Token v="ls" funct/> <Token v="guestbook" /> <Token v="|" punct/> <Token v="reverse" funct/> <Token v="|" punct/> <Token v="take" funct/> <Token v="16"/><Token v=")" punct/>
<br>
<Token v="[" punct/>gazesystems <Token v="/" keywd/><Token v="]$" punct/> <Token v="$entries" /> <Token v="|" punct/> <Token v="each" funct/> <Token v="&#123;" punct/><Token v="|" punct/><Token v="file"/><Token v="|" punct/> <Token v="render" funct/> <Token v="(" punct/><Token v="open" funct/> <Token v="$file.name" /><Token v=")" punct/><Token v="&#125;" punct/>
<br>
<br>
<pre class="language-bash"><code class="language-bash"
><nobr>
<Token v="[" punct />gazesystems <Token v="/" keywd /><Token v="]$" punct /> <Token
v="source"
funct
/> <Token v="scripts/log.nu" />
<br />
<Token v="[" punct />gazesystems <Token v="/" keywd /><Token v="]$" punct /> <Token
v="let"
funct
/> <Token v="entries" /> <Token v="=" punct /> <Token v="(" punct /><Token
v="ls"
funct
/> <Token v="guestbook" /> <Token v="|" punct /> <Token v="reverse" funct /> <Token
v="|"
punct
/> <Token v="take" funct /> <Token v="16" /><Token v=")" punct />
<br />
<Token v="[" punct />gazesystems <Token v="/" keywd /><Token v="]$" punct /> <Token
v="$entries"
/> <Token v="|" punct /> <Token v="each" funct /> <Token v="&#123;" punct /><Token
v="|"
punct
/><Token v="file" /><Token v="|" punct /> <Token v="render" funct /> <Token
v="("
punct
/><Token v="open" funct /> <Token v="$file.name" /><Token v=")" punct /><Token
v="&#125;"
punct
/>
<br />
<br />
{#each data.entries as note, index}
<Note showOutgoing={false} {note}/>
<Note showOutgoing={false} {note} />
{#if index < data.entries.length - 1}
<div class="mt-3"></div>
{/if}
{/each}
</nobr></code></pre>
<div class="mt-3"></div>
{/if}
{/each}
</nobr></code
></pre>
</div>
{/if}
</div>