Schedule posts to Bluesky with Cloudflare workers. skyscheduler.work
cf tool bsky-tool cloudflare bluesky schedule bsky service social-media cloudflare-workers
at main 147 lines 5.1 kB view raw
1import { drizzle } from "drizzle-orm/d1"; 2import { Hono } from "hono"; 3import { cache } from "hono/cache"; 4import { csrf } from "hono/csrf"; 5import isEmpty from "just-is-empty"; 6import { ContextVariables, createAuth } from "./auth"; 7import { ScheduledContext } from "./classes/context"; 8import { account } from "./endpoints/account"; 9import { admin } from "./endpoints/admin"; 10import { post } from "./endpoints/post"; 11import { preview } from "./endpoints/preview"; 12import { blankAuthEnv } from "./middleware/auth"; 13import { corsHelperMiddleware } from "./middleware/corsHelper"; 14import { redirectToDashIfLogin } from "./middleware/redirectDash"; 15import { redirectHomeIfLogout } from "./middleware/redirectHome"; 16import Dashboard from "./pages/dashboard"; 17import ForgotPassword from "./pages/forgot"; 18import Homepage from "./pages/homepage"; 19import Login from "./pages/login"; 20import PrivacyPolicy from "./pages/privacy"; 21import ResetPassword from "./pages/reset"; 22import Signup from "./pages/signup"; 23import TermsOfService from "./pages/tos"; 24import { ATPROTO_DID, SITE_URL } from "./siteinfo"; 25import { Bindings, QueueTaskData } from "./types"; 26import { makeConstScript } from "./utils/constScriptGen"; 27import { processQueue } from "./utils/queues/queueHandler"; 28import { cleanUpPostsTask, schedulePostTask } from "./utils/scheduler"; 29import { setupAccounts } from "./utils/setup"; 30 31const app = new Hono<{ Bindings: Bindings, Variables: ContextVariables }>(); 32app.use(blankAuthEnv); 33app.use(csrf({origin: SITE_URL})); 34 35///// Static Pages ///// 36 37// caches 38const staticFilesCache = cache({ cacheName: 'statics', cacheControl: 'max-age=604800' }); 39const staticPagesCache = cache({ cacheName: 'pages', cacheControl: 'max-age=259200' }); 40 41// Root route 42app.all("/", staticPagesCache, (c) => c.html(<Homepage />)); 43 44// atproto registration route 45if (!isEmpty(ATPROTO_DID)) { 46 app.get("/.well-known/atproto-did", staticFilesCache, (c) => c.text(ATPROTO_DID, 200)); 47} 48 49// JS injection of const variables 50app.get("/js/consts.js", staticFilesCache, (c) => { 51 const constScript = makeConstScript(); 52 return c.body(constScript, 200, {'Content-Type': 'text/javascript'}); 53}); 54 55// Write the robots.txt file dynamically 56app.get("/robots.txt", staticFilesCache, async (c) => { 57 const origin: string = new URL(c.req.url).origin; 58 const robotsFile = await c.env.ASSETS!.fetch(`${origin}/robots.txt`) 59 .then(async (resp) => await resp.text()); 60 return c.text(`${robotsFile}\nSitemap: ${SITE_URL}/sitemap.xml`, 200); 61}); 62 63// Legal linkies 64app.get("/tos", staticPagesCache, (c) => c.html(<TermsOfService />)); 65app.get("/privacy", staticPagesCache, (c) => c.html(<PrivacyPolicy />)); 66 67// Add redirects 68app.get("/contact", (c) => c.redirect(c.env.REDIRECTS.contact)); 69app.get("/tip", (c) => c.redirect(c.env.REDIRECTS.tip)); 70app.get("/terms", (c) => c.redirect("/tos")); 71 72///// Inline Middleware ///// 73// CORS configuration for auth routes 74app.use("/api/auth/**", corsHelperMiddleware); 75 76// Middleware to initialize auth instance for each request 77app.use("*", async (c, next) => { 78 const auth = createAuth(c.env, (c.req.raw as any).cf || {}); 79 c.set("auth", auth); 80 c.set("db", drizzle(c.env.DB)); 81 await next(); 82}); 83 84// Handle auth for all better auth as well. 85app.all("/api/auth/*", async (c) => { 86 const auth = c.get("auth"); 87 return auth.handler(c.req.raw); 88}); 89 90// Account endpoints 91app.route("/account", account); 92 93// Posts endpoints 94app.route("/post", post); 95 96// Admin endpoints 97app.route("/admin", admin); 98 99// Image preview endpoint 100app.route("/preview", preview); 101 102// Dashboard route 103app.get("/dashboard", redirectHomeIfLogout, (c) => c.html(<Dashboard c={c} />)); 104 105// Login route 106app.get("/login", redirectToDashIfLogin, (c) => c.html(<Login />)); 107 108// Signup route 109app.get("/signup", redirectToDashIfLogin, (c) => c.html(<Signup c={c} />)); 110 111// Forgot Password route 112app.get("/forgot", redirectToDashIfLogin, (c) => c.html(<ForgotPassword c={c} />)); 113 114// Reset Password route 115app.get("/reset", redirectToDashIfLogin, (c) => c.html(<ResetPassword />)); 116 117// Reset Password Confirm route 118app.get("/reset-password/:id", (c) => { 119 // Alternatively you can just URL rewrite this in cloudflare and it'll look 120 // 100x times better. 121 const { id } = c.req.param(); 122 return c.redirect(`/api/auth/reset-password/${id}?callbackURL=%2Freset`); 123}); 124 125// Startup Application 126app.get("/setup", async (c) => await setupAccounts(c)); 127 128export default { 129 scheduled(event: ScheduledEvent, env: Bindings, ctx: ExecutionContext) { 130 const runtimeWrapper = new ScheduledContext(env, ctx); 131 switch (event.cron) { 132 case "30 17 * * sun": 133 ctx.waitUntil(cleanUpPostsTask(runtimeWrapper)); 134 break; 135 default: 136 case "0 * * * *": 137 ctx.waitUntil(schedulePostTask(runtimeWrapper)); 138 break; 139 } 140 }, 141 async queue(batch: MessageBatch<QueueTaskData>, env: Bindings, ctx: ExecutionContext) { 142 await processQueue(batch, env, ctx); 143 }, 144 fetch(request: Request, env: Bindings, ctx: ExecutionContext) { 145 return app.fetch(request, env, ctx); 146 } 147};