Schedule posts to Bluesky with Cloudflare workers.
skyscheduler.work
cf
tool
bsky-tool
cloudflare
bluesky
schedule
bsky
service
social-media
cloudflare-workers
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};