data endpoint for entity 90008 (aka. a website)
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: save last game and track so we can read if server dies

dusk 4083ad0a e56568d7

+66 -25
+2
.gitignore
··· 37 37 .bloop 38 38 guestbook/entries 39 39 guestbook/entries_size 40 + last_game.json 41 + last_track.json
+9
bun.lock
··· 8 8 "@rowanmanning/feed-parser": "^2.1.1", 9 9 "@skyware/bot": "^0.4.0", 10 10 "@std/toml": "npm:@jsr/std__toml", 11 + "@types/bun": "^1.2.20", 11 12 "@types/node-schedule": "^2.1.8", 12 13 "nanoid": "^5.1.5", 13 14 "node-schedule": "^2.1.1", ··· 270 271 271 272 "@tailwindcss/typography": ["@tailwindcss/typography@0.5.16", "", { "dependencies": { "lodash.castarray": "^4.4.0", "lodash.isplainobject": "^4.0.6", "lodash.merge": "^4.6.2", "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA=="], 272 273 274 + "@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="], 275 + 273 276 "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], 274 277 275 278 "@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="], ··· 283 286 "@types/node": ["@types/node@22.17.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-y3tBaz+rjspDTylNjAX37jEC3TETEFGNJL6uQDxwF9/8GLLIjW1rvVHlynyuUKMnMr1Roq8jOv3vkopBjC4/VA=="], 284 287 285 288 "@types/node-schedule": ["@types/node-schedule@2.1.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-k00g6Yj/oUg/CDC+MeLHUzu0+OFxWbIqrFfDiLi6OPKxTujvpv29mHGM8GtKr7B+9Vv92FcK/8mRqi1DK5f3hA=="], 289 + 290 + "@types/react": ["@types/react@19.1.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg=="], 286 291 287 292 "@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], 288 293 ··· 344 349 345 350 "browserslist": ["browserslist@4.25.2", "", { "dependencies": { "caniuse-lite": "^1.0.30001733", "electron-to-chromium": "^1.5.199", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA=="], 346 351 352 + "bun-types": ["bun-types@1.2.20", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="], 353 + 347 354 "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], 348 355 349 356 "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], ··· 375 382 "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], 376 383 377 384 "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], 385 + 386 + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], 378 387 379 388 "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], 380 389
+1
package.json
··· 44 44 "@rowanmanning/feed-parser": "^2.1.1", 45 45 "@skyware/bot": "^0.4.0", 46 46 "@std/toml": "npm:@jsr/std__toml", 47 + "@types/bun": "^1.2.20", 47 48 "@types/node-schedule": "^2.1.8", 48 49 "nanoid": "^5.1.5", 49 50 "node-schedule": "^2.1.1",
+8 -5
src/hooks.server.ts
··· 1 1 import { updateLastPosts } from '$lib/bluesky'; 2 - import { lastFmUpdateNowPlaying } from '$lib/lastfm'; 3 - import { steamUpdateNowPlaying } from '$lib/steam'; 2 + import { lastFmReadLast, lastFmUpdateNowPlaying } from '$lib/lastfm'; 3 + import { steamReadLastGame, steamUpdateNowPlaying } from '$lib/steam'; 4 4 import { updateCommits } from '$lib/activity'; 5 5 import { cancelJob, scheduleJob, scheduledJobs } from 'node-schedule'; 6 6 import { ··· 26 26 console.log(`${UPDATE_LAST_JOB_NAME} is already running, cancelling so we can start a new one`); 27 27 cancelJob(UPDATE_LAST_JOB_NAME); 28 28 } 29 + 30 + await steamReadLastGame(); 31 + await lastFmReadLast(); 29 32 30 33 console.log(`starting ${UPDATE_LAST_JOB_NAME} job...`); 31 34 scheduleJob(UPDATE_LAST_JOB_NAME, '*/1 * * * *', async () => { ··· 66 69 const isFakeVisit = 67 70 (await testUa(event.url.toString(), event.request.headers.get('user-agent') ?? '')) === false; 68 71 if (isFakeVisit) { 69 - pushMetric({ gazesys_visit_fake_total: incrementFakeVisitCount() }); 72 + pushMetric({ gazesys_visit_fake_total: await incrementFakeVisitCount() }); 70 73 throw error(403, 'get a better user agent silly'); 71 74 } 72 75 73 76 // only push metric if legit page visit (still want rss to count here though) 74 77 const isPageVisit = !isApi() && !isPrefetch(); 75 - if (isPageVisit) pushMetric({ gazesys_visit_real_total: incrementLegitVisitCount() }); 78 + if (isPageVisit) pushMetric({ gazesys_visit_real_total: await incrementLegitVisitCount() }); 76 79 77 80 // only add visitors if its a "legit" page visit 78 81 let id = null; 79 82 let valid = false; 80 83 if (isPageVisit && !isRss()) { 81 84 id = addLastVisitor(event.request, event.cookies); 82 - valid = incrementVisitCount(event.request, event.cookies); 85 + valid = await incrementVisitCount(event.request, event.cookies); 83 86 } 84 87 85 88 // actually resolve event
+8 -5
src/lib/counter.ts
··· 1 - import { existsSync, readFileSync, writeFileSync } from 'fs'; 2 1 import { get, writable } from 'svelte/store'; 3 2 4 3 /** ··· 7 6 * @param initialValue The initial value if the file doesn't exist 8 7 * @returns An object with methods to get, increment, and set the count 9 8 */ 10 - export const createFileCounter = (filePath: string, initialValue: number = 0) => { 9 + export const createFileCounter = async (filePath: string, initialValue: number = 0) => { 11 10 const counter = writable( 12 - parseInt(existsSync(filePath) ? readFileSync(filePath).toString() : initialValue.toString()) 11 + parseInt( 12 + (await Bun.file(filePath).exists()) 13 + ? await Bun.file(filePath).text() 14 + : initialValue.toString() 15 + ) 13 16 ); 14 17 15 - const saveToFile = (value: number) => { 16 - writeFileSync(filePath, value.toString()); 18 + const saveToFile = async (value: number) => { 19 + await Bun.write(filePath, value.toString()); 17 20 return value; 18 21 }; 19 22
+14
src/lib/lastfm.ts
··· 1 + import { env } from '$env/dynamic/private'; 1 2 import { get, writable } from 'svelte/store'; 2 3 3 4 const GET_RECENT_TRACKS_ENDPOINT = 4 5 'https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=yusdacra&api_key=da1911d405b5b37383e200b8f36ee9ec&format=json&limit=1'; 6 + const LAST_TRACK_FILE = `${env.WEBSITE_DATA_DIR}/last_track.json`; 5 7 6 8 type LastTrack = { 7 9 name: string; ··· 13 15 }; 14 16 const lastTrack = writable<LastTrack | null>(null); 15 17 18 + export const lastFmReadLast = async () => { 19 + try { 20 + const file = Bun.file(LAST_TRACK_FILE); 21 + const data = (await file.exists()) ? await file.text() : null; 22 + lastTrack.set(data ? JSON.parse(data) : null); 23 + } catch (why) { 24 + console.log('could not read last fm: ', why); 25 + lastTrack.set(null); 26 + } 27 + }; 28 + 16 29 export const lastFmUpdateNowPlaying = async () => { 17 30 try { 18 31 const resp = await (await fetch(GET_RECENT_TRACKS_ENDPOINT)).json(); ··· 29 42 playing: true 30 43 }; 31 44 lastTrack.set(data); 45 + await Bun.write(LAST_TRACK_FILE, JSON.stringify(data)); 32 46 } catch (why) { 33 47 console.log('could not fetch last fm: ', why); 34 48 lastTrack.update((t) => {
+6 -4
src/lib/metrics.ts
··· 34 34 } 35 35 }; 36 36 37 - export const bounceCount = createFileCounter(`${env.WEBSITE_DATA_DIR}/bouncecount`); 37 + export const bounceCount = await createFileCounter(`${env.WEBSITE_DATA_DIR}/bouncecount`); 38 38 export const incrementBounceCount = bounceCount.increment; 39 39 40 - export const legitVisitCount = createFileCounter(`${env.WEBSITE_DATA_DIR}/legitvisitcount`); 40 + export const legitVisitCount = await createFileCounter(`${env.WEBSITE_DATA_DIR}/legitvisitcount`); 41 41 export const incrementLegitVisitCount = legitVisitCount.increment; 42 42 43 - export const fakeVisitCount = createFileCounter(`${env.WEBSITE_DATA_DIR}/fakevisitcount`); 43 + export const fakeVisitCount = await createFileCounter(`${env.WEBSITE_DATA_DIR}/fakevisitcount`); 44 44 export const incrementFakeVisitCount = fakeVisitCount.increment; 45 45 46 - export const distanceTravelled = createFileCounter(`${env.WEBSITE_DATA_DIR}/distancetravelled`); 46 + export const distanceTravelled = await createFileCounter( 47 + `${env.WEBSITE_DATA_DIR}/distancetravelled` 48 + );
+13
src/lib/steam.ts
··· 4 4 5 5 const STEAM_ID = '76561198106829949'; 6 6 const GET_PLAYER_SUMMARY_ENDPOINT = `http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=${env.STEAM_API_KEY}&steamids=${STEAM_ID}&format=json`; 7 + const LAST_GAME_FILE = `${env.WEBSITE_DATA_DIR}/last_game.json`; 7 8 8 9 type LastGame = { 9 10 name: string; ··· 17 18 const steamgriddbClient = writable<SGDB | null>(null); 18 19 const lastGame = writable<LastGame | null>(null); 19 20 21 + export const steamReadLastGame = async () => { 22 + try { 23 + const file = Bun.file(LAST_GAME_FILE); 24 + const data = (await file.exists()) ? await file.text() : null; 25 + lastGame.set(data ? JSON.parse(data) : null); 26 + } catch (why) { 27 + console.log('could not read last game: ', why); 28 + lastGame.set(null); 29 + } 30 + }; 31 + 20 32 export const steamUpdateNowPlaying = async () => { 21 33 let griddbClient = get(steamgriddbClient); 22 34 if (griddbClient === null) { ··· 39 51 playing: true 40 52 }; 41 53 lastGame.set(game); 54 + await Bun.write(LAST_GAME_FILE, JSON.stringify(game)); 42 55 } catch (why) { 43 56 console.log('could not fetch steam: ', why); 44 57 lastGame.update((t) => {
+3 -4
src/lib/visits.ts
··· 1 1 import { env } from '$env/dynamic/private'; 2 2 import { scopeCookies } from '$lib'; 3 3 import type { Cookies } from '@sveltejs/kit'; 4 - import { existsSync, readFileSync, writeFileSync } from 'fs'; 5 4 import { nanoid } from 'nanoid'; 6 5 import { get, writable } from 'svelte/store'; 7 6 8 7 const visitCountFile = `${env.WEBSITE_DATA_DIR}/visitcount`; 9 8 export const visitCount = writable( 10 - parseInt(existsSync(visitCountFile) ? readFileSync(visitCountFile).toString() : '0') 9 + parseInt((await Bun.file(visitCountFile).exists()) ? await Bun.file(visitCountFile).text() : '0') 11 10 ); 12 11 13 12 export type Visitor = { visits: number[] }; ··· 18 17 visitCount.set(get(visitCount) - 1); 19 18 }; 20 19 21 - export const incrementVisitCount = (request: Request, cookies: Cookies) => { 20 + export const incrementVisitCount = async (request: Request, cookies: Cookies) => { 22 21 let currentVisitCount = get(visitCount); 23 22 // check whether the request is from a bot or not (this doesnt need to be accurate we just want to filter out honest bots) 24 23 if (isBot(request)) return false; ··· 36 35 // update the cookie with the current timestamp 37 36 scopedCookies.set('visitedTimestamp', currentTime.toString()); 38 37 // write the visit count to a file so we can load it later again 39 - writeFileSync(visitCountFile, currentVisitCount.toString()); 38 + await Bun.write(visitCountFile, currentVisitCount.toString()); 40 39 } 41 40 return true; 42 41 };
-5
src/routes/+page.svelte
··· 36 36 image: 'https://girlthi.ng/~thermia/img/88x31/thermia.gif' 37 37 }, 38 38 { 39 - name: 'sparkles', 40 - url: 'https://sparkles.getconfigured.org/', 41 - image: 'https://sparkles.getconfigured.org/assets/sparkles.png' 42 - }, 43 - { 44 39 name: 'indieweb', 45 40 url: 'https://indieweb.org/', 46 41 image: 'https://indieweb.org/images/9/91/indieweb88x31-retro-gif.gif'
+1 -1
src/routes/_api/pet/bounce/+server.ts
··· 5 5 export const GET = async ({ request, url }) => { 6 6 if (isBot(request) || !checkApiToken(url)) return new Response(); 7 7 try { 8 - await pushMetric({ gazesys_pet_bounce_total: incrementBounceCount() }); 8 + await pushMetric({ gazesys_pet_bounce_total: await incrementBounceCount() }); 9 9 } catch (error) { 10 10 console.log(`error while pushing bounce metric: ${error}`); 11 11 }
+1 -1
src/routes/_api/pet/distance/+server.ts
··· 6 6 if (isBot(request) || !checkApiToken(url)) return new Response(); 7 7 try { 8 8 const delta = parseFloat(await request.text()); 9 - await pushMetric({ gazesys_pet_distance_total: distanceTravelled.increment(delta) }); 9 + await pushMetric({ gazesys_pet_distance_total: await distanceTravelled.increment(delta) }); 10 10 } catch (error) { 11 11 console.log(`error while pushing bounce metric: ${error}`); 12 12 }