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

Minor fixes

allow for generation of invite codes easily.

+72 -27
+1 -1
LICENSE
··· 1 1 MIT License 2 2 3 3 Copyright (c) 2024 Varun A P 4 - Copyright (c) 2025 SocksTheWolf 4 + Copyright (c) 2025-2026 SocksTheWolf 5 5 6 6 Permission is hereby granted, free of charge, to any person obtaining a copy 7 7 of this software and associated documentation files (the "Software"), to deal
+2
README.md
··· 85 85 86 86 9. Modify the metatags located in the `metaTags.tsx` (these are currently set up for the website attached to this project) 87 87 88 + 10. Run your application and go to `/setup`. This will create the admin account. 89 + 88 90 ## Configuration 89 91 90 92 ### Environment Variables
+10
package-lock.json
··· 12 12 "date-fns": "^4.1.0", 13 13 "drizzle-orm": "^0.45.1", 14 14 "hono": "^4.11.1", 15 + "human-id": "^4.1.3", 15 16 "image-dimensions": "^2.5.0", 16 17 "just-flatten-it": "^5.2.0", 17 18 "just-has": "^2.3.0", ··· 2352 2353 "license": "MIT", 2353 2354 "engines": { 2354 2355 "node": ">=16.9.0" 2356 + } 2357 + }, 2358 + "node_modules/human-id": { 2359 + "version": "4.1.3", 2360 + "resolved": "https://registry.npmjs.org/human-id/-/human-id-4.1.3.tgz", 2361 + "integrity": "sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==", 2362 + "license": "MIT", 2363 + "bin": { 2364 + "human-id": "dist/cli.js" 2355 2365 } 2356 2366 }, 2357 2367 "node_modules/image-dimensions": {
+1
package.json
··· 19 19 "date-fns": "^4.1.0", 20 20 "drizzle-orm": "^0.45.1", 21 21 "hono": "^4.11.1", 22 + "human-id": "^4.1.3", 22 23 "image-dimensions": "^2.5.0", 23 24 "just-flatten-it": "^5.2.0", 24 25 "just-has": "^2.3.0",
+17 -19
src/index.tsx
··· 16 16 import { post } from "./endpoints/post"; 17 17 import { redirectToDashIfLogin } from "./middleware/redirectDash"; 18 18 import ForgotPassword from "./pages/forgot"; 19 + import {humanId} from 'human-id' 20 + import setupAccounts from "./utils/setup"; 19 21 20 22 const app = new Hono<{ Bindings: Bindings, Variables: ContextVariables }>(); 21 23 ··· 76 78 return c.html(<ResetPassword />); 77 79 }); 78 80 81 + // Generate invites route 82 + app.get("/invite", every(authMiddleware, adminOnlyMiddleware), async (c) => { 83 + const newKey:string = humanId({ 84 + separator: '-', 85 + capitalize: false, 86 + }); 87 + await c.env.INVITE_POOL.put(newKey, "10"); 88 + return c.text(`${newKey} is good for 10 uses`); 89 + }); 90 + 91 + // Admin Maintenance Cleanup 79 92 app.get("/cron", every(authMiddleware, adminOnlyMiddleware), (c) => { 80 93 schedulePostTask(c.env, c.executionCtx); 81 94 return c.text("ran"); ··· 91 104 return c.text("ran"); 92 105 }); 93 106 94 - app.get("/start", async (c) => { 95 - if (await doesAdminExist(c)) 96 - return c.html("already created", 501); 97 - 98 - const data = await c.get("auth").api.signUpEmail({ 99 - body: { 100 - name: "admin", 101 - email: `${c.env.DEFAULT_ADMIN_USER}@skyscheduler.tld`, 102 - // @ts-ignore: Property does not exist (it does via an extension) 103 - username: c.env.DEFAULT_ADMIN_USER, 104 - password: c.env.DEFAULT_ADMIN_PASS, 105 - bskyAppPass: c.env.DEFAULT_ADMIN_BSKY_PASS 106 - } 107 - }); 108 - if (data.token !== null) 109 - return c.redirect("/"); 110 - else 111 - return c.html("failure", 401); 112 - }) 107 + // Startup Application 108 + app.get("/start", async (c) => await setupAccounts(c)); 109 + app.get("/setup", async (c) => await setupAccounts(c)); 110 + app.get("/startup", async (c) => await setupAccounts(c)); 113 111 114 112 export default { 115 113 scheduled(event: ScheduledEvent, env: Bindings, ctx: ExecutionContext) {
+3 -1
src/layout/account.tsx
··· 1 1 import { Context } from 'hono'; 2 2 import { html } from 'hono/html'; 3 3 import { Child } from 'hono/jsx'; 4 + import { HtmlEscapedString } from 'hono/utils/html'; 4 5 5 6 type FooterLink = { 6 7 title: string; ··· 17 18 successText: string; 18 19 redirect: string; 19 20 footerLinks?: FooterLink[] 21 + footerHTML?: string | Promise<HtmlEscapedString> 20 22 }; 21 23 22 24 export default function AccountHandler(props: AccountFormProps) { ··· 43 45 <footer> 44 46 <center> 45 47 <small id="footerLinks"> 46 - {footerLinkHTML} 48 + {props.footerLinks ? footerLinkHTML : (props.footerHTML || "")} 47 49 </small> 48 50 </center> 49 51 </footer>
+10
src/layout/footer.tsx
··· 1 + // Helper footer for various pages 2 + export default function FooterCopyright() { 3 + return ( 4 + <> 5 + <a class="secondary" target="_blank" href="https://github.com/SocksTheWolf/SkyScheduler">SkyScheduler</a> &copy; {new Date().getFullYear()} 6 + <span class="credits"><a href="https://socksthewolf.com">SocksTheWolf</a> - 7 + <a class="secondary" target="_blank" href="https://ko-fi.com/socksthewolf">Tip/Donate</a></span> 8 + </> 9 + ); 10 + }
+2 -4
src/pages/homepage.tsx
··· 1 + import FooterCopyright from "../layout/footer"; 1 2 import { BaseLayout } from "../layout/main"; 2 3 import NavTags from "../layout/navTags"; 3 4 import { MAX_HOURS_REPOSTING, MAX_REPOST_INTERVAL } from "../limits.d"; 4 5 5 6 export default function Home() { 6 - const currentYear = new Date().getFullYear(); 7 7 return ( 8 8 <BaseLayout title="SkyScheduler - Home"> 9 9 <NavTags /> ··· 35 35 </ul> 36 36 </p> 37 37 <footer><small> 38 - <a class="secondary" target="_blank" href="https://github.com/SocksTheWolf/SkyScheduler">SkyScheduler</a> &copy; {currentYear} 39 - <span class="credits"><a href="https://socksthewolf.com">SocksTheWolf</a> - 40 - <a class="secondary" target="_blank" href="https://ko-fi.com/socksthewolf">Tip/Donate</a></span> 38 + <FooterCopyright /> 41 39 </small></footer> 42 40 </article> 43 41 </section>
+4 -2
src/pages/signup.tsx
··· 7 7 import AccountHandler from "../layout/account"; 8 8 import UsernameField from "../layout/usernameField"; 9 9 import TurnstileCaptcha from "../layout/turnstile"; 10 + import FooterCopyright from "../layout/footer"; 10 11 11 12 export default function Signup(props:any) { 12 13 const ctx: Context = props.c; 13 14 const linkToInvites = isUsingInviteKeys(ctx) && !isEmpty(ctx.env.INVITE_THREAD) ? 14 - (<a href={ctx.env.INVITE_THREAD} target="_blank">You can check for invite codes in this thread</a>) : 15 + (<a href={ctx.env.INVITE_THREAD} target="_blank">Invite codes are routinely posted in this thread, grab one here</a>) : 15 16 "You can ask for the maintainer for it"; 16 17 17 18 return ( ··· 22 23 loadingText="Signing up..." 23 24 endpoint="/account/signup" 24 25 successText="Success! Redirecting to login..." 25 - redirect="/login"> 26 + redirect="/login" 27 + footerHTML={<FooterCopyright />}> 26 28 27 29 <label> 28 30 Dashboard Password
+22
src/utils/setup.ts
··· 1 + import { Context } from "hono"; 2 + import { doesAdminExist } from "./dbQuery"; 3 + 4 + export default async function setupAccounts(c: Context) { 5 + if (await doesAdminExist(c)) 6 + return c.html("already created", 501); 7 + 8 + const data = await c.get("auth").api.signUpEmail({ 9 + body: { 10 + name: "admin", 11 + email: `${c.env.DEFAULT_ADMIN_USER}@skyscheduler.tld`, 12 + // @ts-ignore: Property does not exist (it does via an extension) 13 + username: c.env.DEFAULT_ADMIN_USER, 14 + password: c.env.DEFAULT_ADMIN_PASS, 15 + bskyAppPass: c.env.DEFAULT_ADMIN_BSKY_PASS 16 + } 17 + }); 18 + if (data.token !== null) 19 + return c.redirect("/"); 20 + else 21 + return c.html("failure", 401); 22 + }