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: use bsky posts for log instead of custom thing

dusk 2538bb61 c4c4cae9

+77 -257
bun.lockb

This is a binary file and will not be displayed.

+11 -11
package.json
··· 13 13 }, 14 14 "devDependencies": { 15 15 "@sveltejs/enhanced-img": "^0.3.10", 16 - "@sveltejs/kit": "^2.15.1", 16 + "@sveltejs/kit": "^2.17.1", 17 17 "@sveltejs/vite-plugin-svelte": "^3.1.2", 18 - "@tailwindcss/forms": "^0.5.9", 19 - "@tailwindcss/typography": "^0.5.15", 18 + "@tailwindcss/forms": "^0.5.10", 19 + "@tailwindcss/typography": "^0.5.16", 20 20 "@types/eslint": "^9.6.1", 21 - "@types/node": "^22.10.3", 21 + "@types/node": "^22.13.1", 22 22 "autoprefixer": "^10.4.20", 23 - "eslint": "^9.17.0", 23 + "eslint": "^9.19.0", 24 24 "eslint-config-prettier": "^9.1.0", 25 25 "eslint-plugin-svelte": "^2.46.1", 26 26 "globals": "^15.14.0", 27 27 "mdsvex": "^0.12.3", 28 - "postcss": "^8.4.49", 28 + "postcss": "^8.5.1", 29 29 "prettier": "^3.4.2", 30 - "prettier-plugin-svelte": "^3.3.2", 30 + "prettier-plugin-svelte": "^3.3.3", 31 31 "svelte": "^4.2.19", 32 32 "svelte-adapter-bun": "^0.5.2", 33 33 "svelte-check": "^3.8.6", 34 34 "sveltekit-rate-limiter": "^0.6.1", 35 35 "tailwindcss": "^3.4.17", 36 36 "tslib": "^2.8.1", 37 - "typescript": "^5.7.2", 38 - "typescript-eslint": "^8.19.0", 39 - "vite": "^5.4.11" 37 + "typescript": "^5.7.3", 38 + "typescript-eslint": "^8.23.0", 39 + "vite": "^5.4.14" 40 40 }, 41 41 "type": "module", 42 42 "dependencies": { 43 - "@neodrag/svelte": "^2.2.0", 43 + "@neodrag/svelte": "^2.3.0", 44 44 "@skyware/bot": "^0.3.8", 45 45 "@std/toml": "npm:@jsr/std__toml", 46 46 "base64url": "^3.0.1",
+30 -8
src/components/note.svelte
··· 1 + <script context="module" lang="ts"> 2 + import type { Post } from "@skyware/bot"; 3 + 4 + export interface OutgoingLink { 5 + name: string, 6 + link: string, 7 + } 8 + export interface NoteData { 9 + content: string, 10 + published: number, 11 + hasMedia: boolean, 12 + hasQuote: boolean, 13 + outgoingLinks?: OutgoingLink[], 14 + } 15 + 16 + export const noteFromBskyPost = (post: Post): NoteData => { 17 + return { 18 + content: post.text, 19 + published: post.createdAt.getTime(), 20 + outgoingLinks: [{ name: "bsky", link: post.uri }], 21 + hasMedia: (post.embed?.isImages() || post.embed?.isVideo()) ?? false, 22 + hasQuote: post.embed?.isRecord() ?? false, 23 + } 24 + } 25 + </script> 1 26 <script lang="ts"> 2 - import type { Note } from "$lib/notes"; 3 27 import Token from "./token.svelte"; 4 28 5 - export let id: string; 6 - export let note: Note; 29 + export let note: NoteData; 7 30 export let isHighlighted = false; 8 31 export let onlyContent = false; 9 32 ··· 19 42 20 43 const getOutgoingLink = (name: string, link: string) => { 21 44 if (name === "bsky") { 22 - if (link.startsWith("https://bsky.gaze.systems")) { 23 - return link 24 - } 25 - return `https://bsky.gaze.systems/post/${link.split('/').pop()}` 45 + return `https://bsky.app/profile/gaze.systems/post/${link.split('/').pop()}` 26 46 } 27 47 return link 28 48 } ··· 36 56 </script> 37 57 38 58 <div class="text-wrap break-words max-w-[70ch] leading-none"> 39 - {#if !onlyContent}<Token v={renderDate(note.published)} small={!isHighlighted}/> <Token v={id} keywd small={!isHighlighted}/><Token v="#" punct/>&nbsp;&nbsp;{/if}<Token v={note.content} str/> 59 + {#if !onlyContent}<Token v={renderDate(note.published)} small={!isHighlighted}/> {/if}<Token v={note.content} str/> 60 + {#if note.hasMedia}<Token v="-contains media-" keywd small/>{/if} 61 + {#if note.hasQuote}<Token v="-contains quote-" keywd small/>{/if} 40 62 {#each note.outgoingLinks ?? [] as {name, link}} 41 63 {@const color = outgoingLinkColors[name]} 42 64 <span class="text-sm"><Token v="(" punct/><a style="color: {color};{getTextShadowStyle(color)}" href={getOutgoingLink(name, link)}>{name}</a><Token v=")" punct/></span>
+16 -1
src/lib/bluesky.ts
··· 1 1 import { env } from '$env/dynamic/private' 2 - import { Bot } from "@skyware/bot"; 2 + import { Bot, Post } from "@skyware/bot"; 3 3 import { get, writable } from 'svelte/store' 4 4 5 5 const bskyClient = writable<null | Bot>(null) ··· 17 17 const bot = new Bot({ service: "https://bsky.social" }) 18 18 await bot.login({ identifier: 'gaze.systems', password: env.BSKY_PASSWORD ?? "" }) 19 19 return bot 20 + } 21 + 22 + export const getUserPosts = async (did: string, includeReposts: boolean = false, count: number = 10) => { 23 + const client = await getBskyClient() 24 + let feedCursor = undefined; 25 + let posts: Post[] = [] 26 + // fetch requested amount of posts 27 + while (posts.length < count || feedCursor === undefined) { 28 + let feedData = await client.getUserPosts( 29 + did, { limit: count, filter: 'posts_no_replies', cursor: feedCursor } 30 + ) 31 + posts.push(...feedData.posts.filter((post) => !includeReposts && post.author.did === did)) 32 + feedCursor = feedData.cursor 33 + } 34 + return posts 20 35 }
-68
src/lib/notes.ts
··· 1 - import { existsSync, readFileSync, writeFileSync } from 'fs' 2 - import { nanoid } from 'nanoid' 3 - import { env } from '$env/dynamic/private' 4 - 5 - export interface OutgoingLinkData { 6 - name: string, 7 - link: string, 8 - } 9 - 10 - export interface Note { 11 - content: string, 12 - published: number, 13 - outgoingLinks?: OutgoingLinkData[], 14 - replyTo?: NoteId, 15 - } 16 - type NoteId = string 17 - 18 - export const notesFolder = `${env.WEBSITE_DATA_DIR}/note` 19 - export const notesListFile = `${env.WEBSITE_DATA_DIR}/notes` 20 - export const noteIdLength = 8; 21 - 22 - export const getNotePath = (id: NoteId) => { return `${notesFolder}/${id}` } 23 - export const genNoteId = () => { 24 - let id = nanoid(noteIdLength) 25 - while (existsSync(getNotePath(id))) { 26 - id = nanoid(noteIdLength) 27 - } 28 - return id 29 - } 30 - export const noteExists = (id: NoteId) => { return existsSync(getNotePath(id)) } 31 - export const readNote = (id: NoteId): Note => { 32 - return JSON.parse(readFileSync(getNotePath(id)).toString()) 33 - } 34 - export const findReplyRoot = (id: NoteId): {rootNote: Note, rootNoteId: NoteId} => { 35 - let noteId: string | null = id 36 - let current: {rootNote?: Note, rootNoteId?: NoteId} = {} 37 - while (noteId !== null) { 38 - current.rootNote = readNote(noteId) 39 - current.rootNoteId = noteId 40 - noteId = current.rootNote.replyTo ?? null 41 - } 42 - if (current.rootNote === undefined || current.rootNoteId === undefined) { 43 - throw "no note with id found" 44 - } 45 - return { 46 - rootNote: current.rootNote, 47 - rootNoteId: current.rootNoteId, 48 - } 49 - } 50 - export const writeNote = (id: NoteId, note: Note) => { 51 - writeFileSync(getNotePath(id), JSON.stringify(note)) 52 - // only append to note list if its not in it yet 53 - let noteList = readNotesList() 54 - if (noteList.indexOf(id) === -1) { 55 - writeNotesList([id].concat(noteList)) 56 - } 57 - } 58 - export const createNote = (id: NoteId, note: Note) => { 59 - writeNote(id, note) 60 - return id 61 - } 62 - 63 - export const readNotesList = (): NoteId[] => { 64 - return JSON.parse(readFileSync(notesListFile).toString()) 65 - } 66 - export const writeNotesList = (note_ids: NoteId[]) => { 67 - writeFileSync(notesListFile, JSON.stringify(note_ids)) 68 - }
+4 -4
src/routes/+page.server.ts
··· 1 + import { getUserPosts } from "$lib/bluesky.js" 1 2 import { lastFmGetNowPlaying } from "$lib/lastfm" 2 - import { readNote, readNotesList } from "$lib/notes.js" 3 3 import { steamGetNowPlaying } from "$lib/steam" 4 + import { noteFromBskyPost } from "../components/note.svelte" 4 5 5 6 export const load = async ({}) => { 6 7 const lastTrack = await lastFmGetNowPlaying() 7 8 const lastGame = await steamGetNowPlaying() 9 + const lastNote = noteFromBskyPost((await getUserPosts("did:plc:dfl62fgb7wtjj3fcbb72naae", false, 1))[0]) 8 10 let banners: number[] = [] 9 11 while (banners.length < 3) { 10 12 const no = getBannerNo(banners) 11 13 banners.push(no) 12 14 } 13 - const lastNoteId = readNotesList()[0] 14 - const lastNote = readNote(lastNoteId) 15 - return {banners, lastTrack, lastGame, lastNote, lastNoteId} 15 + return {banners, lastTrack, lastGame, lastNote} 16 16 } 17 17 18 18 const getBannerNo = (others: number[]) => {
+1 -1
src/routes/+page.svelte
··· 155 155 <span class="border-4 pl-[1ch]" style="border-style: none none none double;">published on {renderDate(data.lastNote.published)}</span> 156 156 </div> 157 157 <div class="mt-0 p-1 border-4 border-double bg-ralsei-black min-w-full max-w-[40ch]"> 158 - <Note id={data.lastNoteId} note={data.lastNote} onlyContent/> 158 + <Note note={data.lastNote} onlyContent/> 159 159 </div> 160 160 </div> 161 161 {#if data.lastTrack}
+1 -1
src/routes/entries/+page.server.ts
··· 10 10 if (log_page !== null) { 11 11 url.searchParams.append("page", log_page) 12 12 } 13 - var logs_result = load_logs({url}) 13 + var logs_result = load_logs() 14 14 return logs_result 15 15 }
+7 -36
src/routes/log/+page.server.ts
··· 1 - import { noteExists, readNote, readNotesList } from '$lib/notes' 1 + import { getUserPosts } from '$lib/bluesky.js'; 2 + import { noteFromBskyPost } from '../../components/note.svelte'; 2 3 3 - const notesPerPage: number = 15 4 - 5 - export const load = ({ url }) => { 6 - return _load({ url }) 4 + export const load = async ({ }) => { 5 + return _load() 7 6 } 8 7 9 - export const _load = ({ url }: { url: URL }) => { 10 - // get the note id to search for and display the page it is in 11 - const noteId = url.searchParams.get("id") 12 - // get the page no if one is provided, otherwise default to 1 13 - let page = parseInt(url.searchParams.get("page") || "1") 14 - if (isNaN(page)) { page = 1 } 15 - 16 - // calculate page count 17 - const notesList = readNotesList() 18 - const pageCount = Math.ceil(notesList.length / notesPerPage) 19 - 20 - // find what page the note id if supplied is from 21 - if (noteId !== null && noteExists(noteId)) { 22 - const noteIndex = notesList.lastIndexOf(noteId) 23 - if (noteIndex > -1) { 24 - page = Math.floor(noteIndex / notesPerPage) + 1 25 - } 8 + export const _load = async () => { 9 + return { 10 + feedPosts: (await getUserPosts("did:plc:dfl62fgb7wtjj3fcbb72naae", false, 13)).map(noteFromBskyPost), 26 11 } 27 - 28 - // clamp page between our min and max 29 - page = Math.min(page, pageCount) 30 - page = Math.max(page, 1) 31 - 32 - // get the notes from the chosen page 33 - const notes = new Map( 34 - notesList.slice((page - 1) * notesPerPage, page * notesPerPage) 35 - .map( 36 - (id) => { return [id, readNote(id)] } 37 - ) 38 - ) 39 - 40 - return { notes, highlightedNote: noteId, page } 41 12 }
+3 -13
src/routes/log/+page.svelte
··· 4 4 import Note from '../../components/note.svelte'; 5 5 6 6 export let data; 7 - 8 - const highlightedNote = data.notes.get(data.highlightedNote ?? '') ?? null 9 7 </script> 10 - 11 - <svelte:head> 12 - {#if highlightedNote !== null} 13 - <meta property="og:description" content={highlightedNote.content} /> 14 - <meta property="og:title" content="log #{data.highlightedNote}" /> 15 - {/if} 16 - </svelte:head> 17 8 18 9 <Window title="terminal" removePadding> 19 10 <div ··· 29 20 <Token v="[" punct/>gazesystems <Token v="/" keywd/><Token v="]$" punct/> <Token v="ls" funct/> <Token v="log" /> <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/> 30 21 <br> 31 22 <br> 32 - {#each data.notes as [id, note], index} 33 - {@const isHighlighted = id === data.highlightedNote} 34 - <Note {id} {note} {isHighlighted}/> 35 - {#if index < data.notes.size - 1} 23 + {#each data.feedPosts as note, index} 24 + <Note {note}/> 25 + {#if index < data.feedPosts.length - 1} 36 26 <div class="mt-3"/> 37 27 {/if} 38 28 {/each}
-38
src/routes/log/_rss/+server.ts
··· 1 - import { PUBLIC_BASE_URL } from '$env/static/public'; 2 - import { readNote, readNotesList, type Note } from '$lib/notes.ts'; 3 - 4 - const logUrl = `${PUBLIC_BASE_URL}/log`; 5 - 6 - interface NoteData { 7 - data: Note, 8 - id: string, 9 - } 10 - 11 - export const GET = async ({ }) => { 12 - const log = readNotesList().map((id) => {return { data: readNote(id), id }}) 13 - return new Response( 14 - render(log), 15 - { 16 - headers: { 17 - 'content-type': 'application/xml', 18 - 'cache-control': 'no-store', 19 - } 20 - }) 21 - }; 22 - 23 - const render = (log: NoteData[]) => `<?xml version="1.0" encoding="UTF-8" ?> 24 - <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> 25 - <channel> 26 - <atom:link href="${logUrl}/_rss" rel="self" type="application/rss+xml" /> 27 - <title>dusk's notes (@gaze.systems)</title> 28 - <link>${logUrl}</link> 29 - <description>a collection of random notes i write whenever, aka my microblogging spot</description> 30 - ${log.map((note) => `<item> 31 - <guid>${logUrl}/?id=${note.id}</guid> 32 - <link>${logUrl}/?id=${note.id}</link> 33 - <description>${note.data.content}</description> 34 - <pubDate>${new Date(note.data.published).toUTCString()}</pubDate> 35 - </item>`).join('')} 36 - </channel> 37 - </rss> 38 - `;
-76
src/routes/log/create/+server.ts
··· 1 - import { env } from '$env/dynamic/private'; 2 - import { PUBLIC_BASE_URL } from '$env/static/public'; 3 - import { getBskyClient } from '$lib/bluesky.js'; 4 - import { createNote, findReplyRoot, genNoteId, readNote, type Note } from '$lib/notes'; 5 - import type { Post, PostPayload, PostReference, ReplyRef } from '@skyware/bot'; 6 - 7 - interface NoteData { 8 - content: string, 9 - replyTo?: string, 10 - embedUri?: string, 11 - bskyPosse: boolean, 12 - } 13 - 14 - export const POST = async ({ request }) => { 15 - const token = request.headers.get('authorization') 16 - if (token !== env.GAZEBOT_TOKEN) { 17 - return new Response("rizz failed", { status: 403 }) 18 - } 19 - // get id 20 - const noteId = genNoteId() 21 - // get note data 22 - const noteData: NoteData = await request.json() 23 - console.log(`want to create note #${noteId} with data: `, noteData) 24 - // get a date before we start publishing to other platforms 25 - let note: Note = { 26 - content: noteData.content, 27 - published: Date.now(), 28 - outgoingLinks: [], 29 - replyTo: noteData.replyTo, 30 - } 31 - let errors: string[] = [] 32 - let repliedNote: Note | null = null 33 - if (noteData.replyTo !== undefined) { 34 - repliedNote = readNote(noteData.replyTo) 35 - } 36 - // bridge to bsky if want to bridge 37 - if (noteData.bskyPosse) { 38 - const postContent = `${noteData.content} (${PUBLIC_BASE_URL}/log?id=${noteId})` 39 - try { 40 - const bot = await getBskyClient() 41 - let postPayload: PostPayload = { 42 - text: postContent, 43 - createdAt: new Date(note.published), 44 - external: noteData.embedUri, 45 - } 46 - let postRef: PostReference 47 - // find parent and reply posts 48 - let replyRef: ReplyRef | null = null 49 - if (noteData.replyTo !== undefined && repliedNote !== null) { 50 - const getBskyUri = (note: Note) => { return note.outgoingLinks?.find((v) => {return v.name === "bsky"})?.link } 51 - const parentUri = getBskyUri(repliedNote) 52 - if (parentUri !== undefined) { 53 - const parentPost = await bot.getPost(parentUri) 54 - postRef = await parentPost.reply(postPayload) 55 - } else { 56 - throw "a reply was requested but no reply is found" 57 - } 58 - } else { 59 - postRef = await bot.post(postPayload) 60 - } 61 - note.outgoingLinks?.push({name: "bsky", link: postRef.uri}) 62 - } catch(why) { 63 - console.log(`failed to post note #${noteId} to bsky: `, why) 64 - errors.push(`error while posting to bsky: ${why}`) 65 - } 66 - } 67 - // create note (this should never fail otherwise it would defeat the whole purpose lol) 68 - createNote(noteId, note) 69 - // send back created note id and any errors that occurred 70 - return new Response(JSON.stringify({ noteId, errors }), { 71 - headers: { 72 - 'content-type': 'application/json', 73 - 'cache-control': 'no-store', 74 - } 75 - }) 76 - };
+4
src/styles/app.css
··· 1 1 @import './prism-synthwave84.css'; 2 2 3 + @import 'bluesky-profile-feed-embed/style.css'; 4 + @import 'bluesky-profile-feed-embed/themes/light.css' (prefers-color-scheme: light); 5 + @import 'bluesky-profile-feed-embed/themes/dim.css' (prefers-color-scheme: dark); 6 + 3 7 @tailwind base; 4 8 @tailwind components; 5 9 @tailwind utilities;