feat: use bsky posts for log instead of custom thing

This commit is contained in:
dusk 2025-02-06 23:03:00 +03:00
parent c4c4cae944
commit 2538bb614c
Signed by: dusk
SSH Key Fingerprint: SHA256:Abmvag+juovVufZTxyWY8KcVgrznxvBjQpJesv071Aw
13 changed files with 77 additions and 257 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -13,34 +13,34 @@
},
"devDependencies": {
"@sveltejs/enhanced-img": "^0.3.10",
"@sveltejs/kit": "^2.15.1",
"@sveltejs/kit": "^2.17.1",
"@sveltejs/vite-plugin-svelte": "^3.1.2",
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
"@types/eslint": "^9.6.1",
"@types/node": "^22.10.3",
"@types/node": "^22.13.1",
"autoprefixer": "^10.4.20",
"eslint": "^9.17.0",
"eslint": "^9.19.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.46.1",
"globals": "^15.14.0",
"mdsvex": "^0.12.3",
"postcss": "^8.4.49",
"postcss": "^8.5.1",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.2",
"prettier-plugin-svelte": "^3.3.3",
"svelte": "^4.2.19",
"svelte-adapter-bun": "^0.5.2",
"svelte-check": "^3.8.6",
"sveltekit-rate-limiter": "^0.6.1",
"tailwindcss": "^3.4.17",
"tslib": "^2.8.1",
"typescript": "^5.7.2",
"typescript-eslint": "^8.19.0",
"vite": "^5.4.11"
"typescript": "^5.7.3",
"typescript-eslint": "^8.23.0",
"vite": "^5.4.14"
},
"type": "module",
"dependencies": {
"@neodrag/svelte": "^2.2.0",
"@neodrag/svelte": "^2.3.0",
"@skyware/bot": "^0.3.8",
"@std/toml": "npm:@jsr/std__toml",
"base64url": "^3.0.1",

View File

@ -1,9 +1,32 @@
<script context="module" lang="ts">
import type { Post } from "@skyware/bot";
export interface OutgoingLink {
name: string,
link: string,
}
export interface NoteData {
content: string,
published: number,
hasMedia: boolean,
hasQuote: boolean,
outgoingLinks?: OutgoingLink[],
}
export const noteFromBskyPost = (post: Post): NoteData => {
return {
content: post.text,
published: post.createdAt.getTime(),
outgoingLinks: [{ name: "bsky", link: post.uri }],
hasMedia: (post.embed?.isImages() || post.embed?.isVideo()) ?? false,
hasQuote: post.embed?.isRecord() ?? false,
}
}
</script>
<script lang="ts">
import type { Note } from "$lib/notes";
import Token from "./token.svelte";
export let id: string;
export let note: Note;
export let note: NoteData;
export let isHighlighted = false;
export let onlyContent = false;
@ -19,10 +42,7 @@
const getOutgoingLink = (name: string, link: string) => {
if (name === "bsky") {
if (link.startsWith("https://bsky.gaze.systems")) {
return link
}
return `https://bsky.gaze.systems/post/${link.split('/').pop()}`
return `https://bsky.app/profile/gaze.systems/post/${link.split('/').pop()}`
}
return link
}
@ -36,7 +56,9 @@
</script>
<div class="text-wrap break-words max-w-[70ch] leading-none">
{#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/>
{#if !onlyContent}<Token v={renderDate(note.published)} small={!isHighlighted}/> {/if}<Token v={note.content} str/>
{#if note.hasMedia}<Token v="-contains media-" keywd small/>{/if}
{#if note.hasQuote}<Token v="-contains quote-" keywd small/>{/if}
{#each note.outgoingLinks ?? [] as {name, link}}
{@const color = outgoingLinkColors[name]}
<span class="text-sm"><Token v="(" punct/><a style="color: {color};{getTextShadowStyle(color)}" href={getOutgoingLink(name, link)}>{name}</a><Token v=")" punct/></span>

View File

@ -1,5 +1,5 @@
import { env } from '$env/dynamic/private'
import { Bot } from "@skyware/bot";
import { Bot, Post } from "@skyware/bot";
import { get, writable } from 'svelte/store'
const bskyClient = writable<null | Bot>(null)
@ -18,3 +18,18 @@ const loginToBsky = async () => {
await bot.login({ identifier: 'gaze.systems', password: env.BSKY_PASSWORD ?? "" })
return bot
}
export const getUserPosts = async (did: string, includeReposts: boolean = false, count: number = 10) => {
const client = await getBskyClient()
let feedCursor = undefined;
let posts: Post[] = []
// fetch requested amount of posts
while (posts.length < count || feedCursor === undefined) {
let feedData = await client.getUserPosts(
did, { limit: count, filter: 'posts_no_replies', cursor: feedCursor }
)
posts.push(...feedData.posts.filter((post) => !includeReposts && post.author.did === did))
feedCursor = feedData.cursor
}
return posts
}

View File

@ -1,68 +0,0 @@
import { existsSync, readFileSync, writeFileSync } from 'fs'
import { nanoid } from 'nanoid'
import { env } from '$env/dynamic/private'
export interface OutgoingLinkData {
name: string,
link: string,
}
export interface Note {
content: string,
published: number,
outgoingLinks?: OutgoingLinkData[],
replyTo?: NoteId,
}
type NoteId = string
export const notesFolder = `${env.WEBSITE_DATA_DIR}/note`
export const notesListFile = `${env.WEBSITE_DATA_DIR}/notes`
export const noteIdLength = 8;
export const getNotePath = (id: NoteId) => { return `${notesFolder}/${id}` }
export const genNoteId = () => {
let id = nanoid(noteIdLength)
while (existsSync(getNotePath(id))) {
id = nanoid(noteIdLength)
}
return id
}
export const noteExists = (id: NoteId) => { return existsSync(getNotePath(id)) }
export const readNote = (id: NoteId): Note => {
return JSON.parse(readFileSync(getNotePath(id)).toString())
}
export const findReplyRoot = (id: NoteId): {rootNote: Note, rootNoteId: NoteId} => {
let noteId: string | null = id
let current: {rootNote?: Note, rootNoteId?: NoteId} = {}
while (noteId !== null) {
current.rootNote = readNote(noteId)
current.rootNoteId = noteId
noteId = current.rootNote.replyTo ?? null
}
if (current.rootNote === undefined || current.rootNoteId === undefined) {
throw "no note with id found"
}
return {
rootNote: current.rootNote,
rootNoteId: current.rootNoteId,
}
}
export const writeNote = (id: NoteId, note: Note) => {
writeFileSync(getNotePath(id), JSON.stringify(note))
// only append to note list if its not in it yet
let noteList = readNotesList()
if (noteList.indexOf(id) === -1) {
writeNotesList([id].concat(noteList))
}
}
export const createNote = (id: NoteId, note: Note) => {
writeNote(id, note)
return id
}
export const readNotesList = (): NoteId[] => {
return JSON.parse(readFileSync(notesListFile).toString())
}
export const writeNotesList = (note_ids: NoteId[]) => {
writeFileSync(notesListFile, JSON.stringify(note_ids))
}

View File

@ -1,18 +1,18 @@
import { getUserPosts } from "$lib/bluesky.js"
import { lastFmGetNowPlaying } from "$lib/lastfm"
import { readNote, readNotesList } from "$lib/notes.js"
import { steamGetNowPlaying } from "$lib/steam"
import { noteFromBskyPost } from "../components/note.svelte"
export const load = async ({}) => {
const lastTrack = await lastFmGetNowPlaying()
const lastGame = await steamGetNowPlaying()
const lastNote = noteFromBskyPost((await getUserPosts("did:plc:dfl62fgb7wtjj3fcbb72naae", false, 1))[0])
let banners: number[] = []
while (banners.length < 3) {
const no = getBannerNo(banners)
banners.push(no)
}
const lastNoteId = readNotesList()[0]
const lastNote = readNote(lastNoteId)
return {banners, lastTrack, lastGame, lastNote, lastNoteId}
return {banners, lastTrack, lastGame, lastNote}
}
const getBannerNo = (others: number[]) => {

View File

@ -155,7 +155,7 @@
<span class="border-4 pl-[1ch]" style="border-style: none none none double;">published on {renderDate(data.lastNote.published)}</span>
</div>
<div class="mt-0 p-1 border-4 border-double bg-ralsei-black min-w-full max-w-[40ch]">
<Note id={data.lastNoteId} note={data.lastNote} onlyContent/>
<Note note={data.lastNote} onlyContent/>
</div>
</div>
{#if data.lastTrack}

View File

@ -10,6 +10,6 @@ export const load = (params) => {
if (log_page !== null) {
url.searchParams.append("page", log_page)
}
var logs_result = load_logs({url})
var logs_result = load_logs()
return logs_result
}

View File

@ -1,41 +1,12 @@
import { noteExists, readNote, readNotesList } from '$lib/notes'
import { getUserPosts } from '$lib/bluesky.js';
import { noteFromBskyPost } from '../../components/note.svelte';
const notesPerPage: number = 15
export const load = ({ url }) => {
return _load({ url })
export const load = async ({ }) => {
return _load()
}
export const _load = ({ url }: { url: URL }) => {
// get the note id to search for and display the page it is in
const noteId = url.searchParams.get("id")
// get the page no if one is provided, otherwise default to 1
let page = parseInt(url.searchParams.get("page") || "1")
if (isNaN(page)) { page = 1 }
// calculate page count
const notesList = readNotesList()
const pageCount = Math.ceil(notesList.length / notesPerPage)
// find what page the note id if supplied is from
if (noteId !== null && noteExists(noteId)) {
const noteIndex = notesList.lastIndexOf(noteId)
if (noteIndex > -1) {
page = Math.floor(noteIndex / notesPerPage) + 1
export const _load = async () => {
return {
feedPosts: (await getUserPosts("did:plc:dfl62fgb7wtjj3fcbb72naae", false, 13)).map(noteFromBskyPost),
}
}
// clamp page between our min and max
page = Math.min(page, pageCount)
page = Math.max(page, 1)
// get the notes from the chosen page
const notes = new Map(
notesList.slice((page - 1) * notesPerPage, page * notesPerPage)
.map(
(id) => { return [id, readNote(id)] }
)
)
return { notes, highlightedNote: noteId, page }
}

View File

@ -4,17 +4,8 @@
import Note from '../../components/note.svelte';
export let data;
const highlightedNote = data.notes.get(data.highlightedNote ?? '') ?? null
</script>
<svelte:head>
{#if highlightedNote !== null}
<meta property="og:description" content={highlightedNote.content} />
<meta property="og:title" content="log #{data.highlightedNote}" />
{/if}
</svelte:head>
<Window title="terminal" removePadding>
<div
class="
@ -29,10 +20,9 @@
<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/>
<br>
<br>
{#each data.notes as [id, note], index}
{@const isHighlighted = id === data.highlightedNote}
<Note {id} {note} {isHighlighted}/>
{#if index < data.notes.size - 1}
{#each data.feedPosts as note, index}
<Note {note}/>
{#if index < data.feedPosts.length - 1}
<div class="mt-3"/>
{/if}
{/each}

View File

@ -1,38 +0,0 @@
import { PUBLIC_BASE_URL } from '$env/static/public';
import { readNote, readNotesList, type Note } from '$lib/notes.ts';
const logUrl = `${PUBLIC_BASE_URL}/log`;
interface NoteData {
data: Note,
id: string,
}
export const GET = async ({ }) => {
const log = readNotesList().map((id) => {return { data: readNote(id), id }})
return new Response(
render(log),
{
headers: {
'content-type': 'application/xml',
'cache-control': 'no-store',
}
})
};
const render = (log: NoteData[]) => `<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<atom:link href="${logUrl}/_rss" rel="self" type="application/rss+xml" />
<title>dusk's notes (@gaze.systems)</title>
<link>${logUrl}</link>
<description>a collection of random notes i write whenever, aka my microblogging spot</description>
${log.map((note) => `<item>
<guid>${logUrl}/?id=${note.id}</guid>
<link>${logUrl}/?id=${note.id}</link>
<description>${note.data.content}</description>
<pubDate>${new Date(note.data.published).toUTCString()}</pubDate>
</item>`).join('')}
</channel>
</rss>
`;

View File

@ -1,76 +0,0 @@
import { env } from '$env/dynamic/private';
import { PUBLIC_BASE_URL } from '$env/static/public';
import { getBskyClient } from '$lib/bluesky.js';
import { createNote, findReplyRoot, genNoteId, readNote, type Note } from '$lib/notes';
import type { Post, PostPayload, PostReference, ReplyRef } from '@skyware/bot';
interface NoteData {
content: string,
replyTo?: string,
embedUri?: string,
bskyPosse: boolean,
}
export const POST = async ({ request }) => {
const token = request.headers.get('authorization')
if (token !== env.GAZEBOT_TOKEN) {
return new Response("rizz failed", { status: 403 })
}
// get id
const noteId = genNoteId()
// get note data
const noteData: NoteData = await request.json()
console.log(`want to create note #${noteId} with data: `, noteData)
// get a date before we start publishing to other platforms
let note: Note = {
content: noteData.content,
published: Date.now(),
outgoingLinks: [],
replyTo: noteData.replyTo,
}
let errors: string[] = []
let repliedNote: Note | null = null
if (noteData.replyTo !== undefined) {
repliedNote = readNote(noteData.replyTo)
}
// bridge to bsky if want to bridge
if (noteData.bskyPosse) {
const postContent = `${noteData.content} (${PUBLIC_BASE_URL}/log?id=${noteId})`
try {
const bot = await getBskyClient()
let postPayload: PostPayload = {
text: postContent,
createdAt: new Date(note.published),
external: noteData.embedUri,
}
let postRef: PostReference
// find parent and reply posts
let replyRef: ReplyRef | null = null
if (noteData.replyTo !== undefined && repliedNote !== null) {
const getBskyUri = (note: Note) => { return note.outgoingLinks?.find((v) => {return v.name === "bsky"})?.link }
const parentUri = getBskyUri(repliedNote)
if (parentUri !== undefined) {
const parentPost = await bot.getPost(parentUri)
postRef = await parentPost.reply(postPayload)
} else {
throw "a reply was requested but no reply is found"
}
} else {
postRef = await bot.post(postPayload)
}
note.outgoingLinks?.push({name: "bsky", link: postRef.uri})
} catch(why) {
console.log(`failed to post note #${noteId} to bsky: `, why)
errors.push(`error while posting to bsky: ${why}`)
}
}
// create note (this should never fail otherwise it would defeat the whole purpose lol)
createNote(noteId, note)
// send back created note id and any errors that occurred
return new Response(JSON.stringify({ noteId, errors }), {
headers: {
'content-type': 'application/json',
'cache-control': 'no-store',
}
})
};

View File

@ -1,5 +1,9 @@
@import './prism-synthwave84.css';
@import 'bluesky-profile-feed-embed/style.css';
@import 'bluesky-profile-feed-embed/themes/light.css' (prefers-color-scheme: light);
@import 'bluesky-profile-feed-embed/themes/dim.css' (prefers-color-scheme: dark);
@tailwind base;
@tailwind components;
@tailwind utilities;