Schedule posts to Bluesky with Cloudflare workers. skyscheduler.work
cf tool bsky-tool cloudflare bluesky schedule bsky service social-media cloudflare-workers

limit account updates as well

and add support for html returns from the rate limiter

+16 -3
+2 -1
src/endpoints/account.tsx
··· 5 5 import { ViolationNoticeBar } from "../layout/violationsBar"; 6 6 import { authMiddleware } from "../middleware/auth"; 7 7 import { corsHelperMiddleware } from "../middleware/corsHelper"; 8 + import { rateLimit } from "../middleware/rateLimit"; 8 9 import { verifyTurnstile } from "../middleware/turnstile"; 9 10 import { Bindings, LooseObj } from "../types"; 10 11 import { lookupBskyHandle, lookupBskyPDS } from "../utils/bskyApi"; ··· 70 71 } 71 72 }); 72 73 73 - account.post("/update", authMiddleware, async (c) => { 74 + account.post("/update", authMiddleware, rateLimit({limiter: "UPDATE_LIMITER", html: true}), async (c) => { 74 75 const body = await c.req.parseBody(); 75 76 const validation = AccountUpdateSchema.safeParse(body); 76 77 if (!validation.success) {
+7 -1
src/middleware/rateLimit.ts
··· 1 1 import { Context } from "hono"; 2 2 import { createMiddleware } from "hono/factory"; 3 + import { html } from "hono/html"; 3 4 import isEmpty from "just-is-empty"; 4 5 import get from 'just-safe-get'; 5 6 6 7 type RateLimitProps = { 7 8 limiter: string; 9 + html?: boolean; 8 10 }; 9 11 10 12 export const rateLimit = (prop: RateLimitProps) => { ··· 20 22 if (success) { 21 23 await next(); 22 24 } else { 23 - return c.json({ok: false, msg: "you are currently rate limited, try again in a minute"}, 429); 25 + if (prop.html) { 26 + return c.html(html`<b class="btn-error">You are being rate limited, try again later</b>`, 429); 27 + } else { 28 + return c.json({ok: false, msg: "You are currently rate limited, try again in a minute"}, 429); 29 + } 24 30 } 25 31 }); 26 32 };
+1
src/types.ts
··· 64 64 R2_SETTINGS: R2ConfigSettings; 65 65 POST_LIMITER: RateLimit; 66 66 REPOST_LIMITER: RateLimit; 67 + UPDATE_LIMITER: RateLimit; 67 68 DEFAULT_ADMIN_USER: string; 68 69 DEFAULT_ADMIN_PASS: string; 69 70 DEFAULT_ADMIN_BSKY_PASS: string;
+2 -1
src/wrangler.d.ts
··· 1 1 /* eslint-disable */ 2 - // Generated by Wrangler by running `wrangler types src/wrangler.d.ts` (hash: 83f9928a213fc29feaa94a1417c98771) 2 + // Generated by Wrangler by running `wrangler types src/wrangler.d.ts` (hash: ede590ad561b49f51ae6fcdf7fe7f2a3) 3 3 // Runtime types generated with workerd@1.20260305.0 2025-11-18 disable_ctx_exports,disable_nodejs_http_server_modules,nodejs_compat,nodejs_compat_do_not_populate_process_env 4 4 declare namespace Cloudflare { 5 5 interface GlobalProps { ··· 15 15 REPOST_QUEUE: Queue; 16 16 REPOST_LIMITER: RateLimit; 17 17 POST_LIMITER: RateLimit; 18 + UPDATE_LIMITER: RateLimit; 18 19 IMAGES: ImagesBinding; 19 20 ASSETS: Fetcher; 20 21 IMAGE_SETTINGS: {"enabled":true,"steps":[95,85,75],"bucket_url":"https://resize.skyscheduler.work/","max_width":3000};
+4
wrangler.toml
··· 47 47 name = "POST_LIMITER" 48 48 namespace_id = "1002" 49 49 simple = { limit = 2, period = 10 } 50 + [[ratelimits]] 51 + name = "UPDATE_LIMITER" 52 + namespace_id = "1003" 53 + simple = { limit = 2, period = 60 } 50 54 51 55 [triggers] 52 56 # Schedule cron triggers at the start of every hour and ~5:30pm on sunday for big cleanups: