diff --git a/bun.lock b/bun.lock index d8c6001..ab93fcc 100644 --- a/bun.lock +++ b/bun.lock @@ -38,6 +38,7 @@ "@atcute/lexicons": "^1.2.9", "@atcute/oauth-node-client": "^1.1.0", "@atcute/xrpc-server": "^0.1.11", + "@types/node": "^25.5.0", "@vt3e.cat/bbell-lexicons": "workspace:*", "drizzle-orm": "^1.0.0-beta.12-a5629fb", }, diff --git a/pkgs/server/package.json b/pkgs/server/package.json index 64bbbb8..83f6fd7 100644 --- a/pkgs/server/package.json +++ b/pkgs/server/package.json @@ -20,6 +20,7 @@ "@atcute/lexicons": "^1.2.9", "@atcute/oauth-node-client": "^1.1.0", "@atcute/xrpc-server": "^0.1.11", + "@types/node": "^25.5.0", "@vt3e.cat/bbell-lexicons": "workspace:*", "drizzle-orm": "^1.0.0-beta.12-a5629fb" } diff --git a/pkgs/server/public/index.html b/pkgs/server/public/index.html new file mode 100644 index 0000000..04dae0b --- /dev/null +++ b/pkgs/server/public/index.html @@ -0,0 +1,99 @@ + + + + + + bbell + + + +
+
+

bluebell backend

+

+ testing page for bluebell's backend, which provides additional features to + bluebell like post scheduling, and deleting posts. maybe more things in the + future! +

+

+ bluebell is a bluesky client for the web & android. +

+ + + +
+
+ + +
+ +
+ + diff --git a/pkgs/server/src/db/schemas/posts.ts b/pkgs/server/src/db/schemas/posts.ts index 7aff8da..6108dec 100644 --- a/pkgs/server/src/db/schemas/posts.ts +++ b/pkgs/server/src/db/schemas/posts.ts @@ -8,6 +8,7 @@ export const scheduledPosts = sqliteTable("scheduled_posts", { status: text("status").notNull().default("pending"), // pending, published, failed createdAt: integer("created_at", { mode: "timestamp" }).notNull(), publishedAt: integer("published_at", { mode: "timestamp" }), + uri: text("uri"), error: text("error"), }); diff --git a/pkgs/server/src/index.ts b/pkgs/server/src/index.ts index 980de5e..537c238 100644 --- a/pkgs/server/src/index.ts +++ b/pkgs/server/src/index.ts @@ -4,6 +4,7 @@ import { cors } from "@atcute/xrpc-server/middlewares/cors"; import { CatVt3eBbellScheduledPostCreate } from "@vt3e.cat/bbell-lexicons"; import { requireAuth } from "./auth/middleware"; import config from "./config"; +import db, { scheduledPosts } from "./db"; import { stateStore } from "./db/store"; import { handleGetSession, @@ -23,18 +24,51 @@ const router = new XRPCRouter({ if (url.pathname === "/oauth/logout") return handleLogout(); if (url.pathname === "/oauth/session") return handleGetSession(req); + if (url.pathname === "/" || url.pathname === "") { + try { + const file = await Bun.file("public/index.html").text(); + return new Response(file, { + headers: { "content-type": "text/html" }, + }); + } catch { + return new Response("not found", { status: 404 }); + } + } + return new Response("not found", { status: 404 }); }, + handleException: async (error) => { + console.error(error); + return new Response("internal server error", { status: 500 }); + }, }); router.addProcedure(CatVt3eBbellScheduledPostCreate, { async handler({ input, request }) { const auth = await requireAuth(request); const postId = crypto.randomUUID(); - void auth; + + const [post] = await db + .insert(scheduledPosts) + .values({ + id: postId, + scheduledAt: new Date(input.scheduledAt), + post: JSON.stringify(input.post), + createdAt: new Date(), + did: auth.did, + }) + .returning() + .execute(); + + if (!post) { + console.error("[db] failed to insert scheduled post"); + return new Response("internal server error", { status: 500 }); + } + + console.log(`[db] inserted scheduled post ${post.id}`); return json({ - id: postId, + id: post.id, scheduledAt: input.scheduledAt, }); }, diff --git a/pkgs/server/src/services/scheduler.ts b/pkgs/server/src/services/scheduler.ts index 4164b43..3e8856d 100644 --- a/pkgs/server/src/services/scheduler.ts +++ b/pkgs/server/src/services/scheduler.ts @@ -1,6 +1,9 @@ +import { ok } from "@atcute/client"; +import { type ActorIdentifier, parseResourceUri } from "@atcute/lexicons"; import { and, eq, lte } from "drizzle-orm"; + import db, { scheduledPosts, selfDeletingPosts } from "../db/index.ts"; -// import { getClient } from "./atcute.ts"; +import { getClient } from "./atcute.ts"; export async function runScheduler() { const now = new Date(); @@ -37,13 +40,24 @@ export async function runScheduler() { async function processScheduledPost(post: typeof scheduledPosts.$inferSelect) { try { - console.log("[scheduler] would post:", post.id); + const client = await getClient(post.did); + + const res = ok( + await client.post("com.atproto.repo.createRecord", { + input: { + repo: post.did as ActorIdentifier, + collection: "app.bsky.feed.post", + record: JSON.parse(post.post as string), + }, + }), + ); await db .update(scheduledPosts) .set({ status: "published", publishedAt: new Date(), + uri: res.uri, }) .where(eq(scheduledPosts.id, post.id)); } catch (err) { @@ -63,7 +77,20 @@ async function processSelfDeletingPost( deletion: typeof selfDeletingPosts.$inferSelect, ) { try { - console.log("[scheduler] would delete:", deletion.id); + console.log("[scheduler] would delete:", deletion.uri); + const client = await getClient(deletion.did); + + const parsedUri = parseResourceUri(deletion.uri); + if (!parsedUri.ok) throw new Error(`invalid URI: ${parsedUri.error}`); + if (!parsedUri.value.rkey) throw new Error(`missing rkey`); + + await client.post("com.atproto.repo.deleteRecord", { + input: { + repo: deletion.did as ActorIdentifier, + collection: "app.bsky.feed.post", + rkey: parsedUri.value.rkey, + }, + }); await db .update(selfDeletingPosts) diff --git a/pkgs/server/tsconfig.json b/pkgs/server/tsconfig.json index bfa0fea..113c9ff 100644 --- a/pkgs/server/tsconfig.json +++ b/pkgs/server/tsconfig.json @@ -1,7 +1,9 @@ { "compilerOptions": { + "types": ["@atcute/bluesky", "@atcute/atproto", "node", "bun"], + // Environment setup & latest features - "lib": ["ESNext"], + "lib": ["ESNext", "DOM"], "target": "ESNext", "module": "Preserve", "moduleDetection": "force",