feat: use bsky posts for log instead of custom thing
This commit is contained in:
parent
c4c4cae944
commit
2538bb614c
22
package.json
22
package.json
@ -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",
|
||||
|
@ -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/> {/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>
|
||||
|
@ -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
|
||||
}
|
@ -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))
|
||||
}
|
@ -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[]) => {
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
}
|
@ -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 }
|
||||
}
|
@ -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="{" 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="}" 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}
|
||||
|
@ -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>
|
||||
`;
|
@ -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',
|
||||
}
|
||||
})
|
||||
};
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user