···28282929- Node.js (v24.x or later)
3030- Package Manager
3131-- Cloudflare Pro Workers account (for CPU)
3131+- Cloudflare Pro Workers account (you will hit CPU limits otherwise)
32323333### Installation
3434···5555 - `TURNSTILE_SECRET_KEY` - the turnstile secret key for captcha
5656 - `RESIZE_SECRET_HEADER` - a header value that will be included on requests while trying to resize images. Protects the resize bucket while still making it accessible to CF Images.
57575858-**Note**: When deploying, these variables should also be configured as secrets in your Cloudflare worker dashboard. You can also do this via `npx wrangler secret put <NAME_OF_SECRET>`.
5858+**Note**: When deploying, these variables should also be configured as secrets in your Cloudflare worker dashboard. You can also do this via `npx wrangler secret put <NAME_OF_SECRET>`. _Alternatively_, make a file like `.env.prod` and use `npx wrangler secret bulk FILENAME` to upload all the settings at once.
595960604. Update your `wrangler.toml` with changes that reflect your account.
6161- - You'll need to update the values for the kv, r2, d1 to reflect the bindings on your account.
6161+ - You'll need to update the values for the kv, r2, queues, d1 to reflect the bindings on your account.
6262 - Also make sure you update the `BETTER_AUTH_URL` to your working url as well.
6363 - Do remember to remove the domain bindings!
6464···8686npm run migrate:all
8787```
88888989-9. Modify the metatags located in the `metaTags.tsx` (these are currently set up for the website attached to this project)
8989+9. Modify the site specific information located in `limits.ts`, `metaTags.tsx`, `robots.txt`, `sitemap.xml` (these are currently set up for the website attached to this project)
9090919110. Run your application and go to `/setup`. This will create the admin account.
9292···102102103103### Minimization
104104105105-The application by default is configured to use the minified versions of the scripts in `assets/js`. By default these rebuild whenever any typescript file is changed or the application is deployed/ran.
105105+The application by default is configured to use the minified versions of the scripts in `assets/js`. By default all client JS files will rebuild whenever any typescript file is changed or the application is deployed/ran.
106106107107## Project Structure
108108···121121├── migrations/
122122├── .dev.vars
123123├── .node-version
124124+├── .markdownlint.json
124125├── .minify.json
125126├── drizzle.config.ts
126127├── package.json
···160161- Report bugs
161162- Suggest enhancements
162163- Submit pull requests
164164+- [Sponsor](https://ko-fi.com/socksthewolf/tip)
163165164166## License
165167
+3-3
src/auth/index.ts
···44import { username } from "better-auth/plugins";
55import { drizzle, DrizzleD1Database } from "drizzle-orm/d1";
66import { schema } from "../db";
77-import { BSKY_MAX_USERNAME_LENGTH, BSKY_MIN_USERNAME_LENGTH } from "../limits";
77+import { APP_NAME, BSKY_MAX_USERNAME_LENGTH, BSKY_MIN_USERNAME_LENGTH } from "../limits";
88import { Bindings } from "../types.d";
99import { lookupBskyHandle } from "../utils/bskyApi";
1010import { createDMWithUser } from "../utils/bskyMsg";
11111212function createPasswordResetMessage(url: string) {
1313- return `Your SkyScheduler password reset url is:
1313+ return `Your ${APP_NAME} password reset url is:
1414${url}
15151616This URL will expire in about an hour.
···107107 },
108108 }
109109 ),
110110- appName: "SkyScheduler",
110110+ appName: APP_NAME,
111111 secret: env?.BETTER_AUTH_SECRET,
112112 baseURL: (env?.BETTER_AUTH_URL === "*") ? undefined : env?.BETTER_AUTH_URL,
113113 user: {
+3-2
src/layout/helpers/footer.tsx
···11+import { APP_NAME } from "../../limits";
12import { PROGRESS_MADE, PROGRESS_TOTAL } from "../../progress";
2334// Helper footer for various pages
···1011export default function FooterCopyright(props: FooterCopyrightProps) {
1112 const newWinAttr = props.inNewWindow ? {"target": '_blank'} : {};
1213 const projectURL = (<a class="secondary" target="_blank" title="Project source on GitHub"
1313- href="https://github.com/SocksTheWolf/SkyScheduler">SkyScheduler</a>);
1414- const homepageURL = (<a class="secondary" title="Homepage" href="/">SkyScheduler</a>);
1414+ href="https://github.com/SocksTheWolf/SkyScheduler">{APP_NAME}</a>);
1515+ const homepageURL = (<a class="secondary" title="Homepage" href="/">{APP_NAME}</a>);
1516 const progressBarTooltip = `$${PROGRESS_MADE}/$${PROGRESS_TOTAL} for this month`;
1617 return (
1718 <center><small>
+6-6
src/layout/helpers/metaTags.tsx
···11import { raw } from "hono/html";
22+import { APP_NAME } from "../../limits";
2334export default function MetaTags() {
45 /* Modify these to change meta information on all pages. */
55- const Title: string = "SkyScheduler";
66 const URL: string = "https://skyscheduler.work";
77 const Description: string = "Schedule and automatically repost on Bluesky! Boost engagement and reach more people no matter what time of day!";
88 const SocialImage: string = "https://skyscheduler.work/dashboard.png";
991010 return (
1111 <>
1212- <meta name="title" content={Title} />
1212+ <meta name="title" content={APP_NAME} />
1313 <meta name="description" content={Description} />
1414 <meta property="og:type" content="website" />
1515 <meta property="og:url" content={URL} />
1616- <meta property="og:title" content={Title} />
1616+ <meta property="og:title" content={APP_NAME} />
1717 <meta property="og:description" content={Description} />
1818 <meta property="og:image" content={SocialImage} />
1919 <meta property="twitter:card" content="summary_large_image" />
2020 <meta property="twitter:url" content={URL} />
2121- <meta property="twitter:title" content={Title} />
2121+ <meta property="twitter:title" content={APP_NAME} />
2222 <meta property="twitter:description" content={Description} />
2323 <meta property="twitter:image" content={SocialImage} />
2424 <script type="application/ld+json">
2525 {raw(`{
2626 "@context": "https://schema.org",
2727 "@type": "WebSite",
2828- "name": "${Title}",
2929- "headline": "${Title}",
2828+ "name": "${APP_NAME}",
2929+ "headline": "${APP_NAME}",
3030 "url": "${URL}"
3131 }`)}
3232 </script>
···11import {
22+ APP_NAME,
23 BSKY_IMG_FILE_EXTS,
34 BSKY_IMG_SIZE_LIMIT_IN_MB,
45 BSKY_VIDEO_FILE_EXTS,
···6263 <li><span data-tooltip={BSKY_IMG_FILE_EXTS}>Images</span>:
6364 <ul>
6465 <li>must be less than {CF_IMAGES_MAX_DIMENSION}x{CF_IMAGES_MAX_DIMENSION} pixels</li>
6565- <li>must have a file size smaller than {CF_IMAGES_FILE_SIZE_LIMIT_IN_MB}MB (SkyScheduler will attempt to compress images to fit <span data-tooltip={bskyImageLimits}>BlueSky's requirements</span>)</li>
6666+ <li>must have a file size smaller than {CF_IMAGES_FILE_SIZE_LIMIT_IN_MB}MB ({APP_NAME} will attempt to compress images to fit <span data-tooltip={bskyImageLimits}>BlueSky's requirements</span>)</li>
6667 <li>thumbnails will only be shown here for images that are smaller than {MAX_THUMBNAIL_SIZE}MB</li>
6768 <li>don't upload and fail, it's recommended to use a lower resolution file instead</li>
6869 </ul></li>
+4-4
src/layout/settings.tsx
···11-import { MAX_DASHBOARD_PASS, MIN_DASHBOARD_PASS } from "../limits";
11+import { APP_NAME, MAX_DASHBOARD_PASS, MIN_DASHBOARD_PASS } from "../limits";
22import { PWAutoCompleteSettings } from "../types.d";
33import { settingsScriptStr } from "../utils/appScripts";
44import { BSkyAppPasswordField, DashboardPasswordField } from "./passwordFields";
···3232 <label>
3333 Dashboard Pass:
3434 <DashboardPasswordField autocomplete={PWAutoCompleteSettings.CurrentPass} />
3535- <small>The password to access the SkyScheduler Dashboard</small>
3535+ <small>The password to access the {APP_NAME} Dashboard</small>
3636 </label>
3737 <label>
3838 BSky App Password:
···6060 <dialog id="deleteAccount">
6161 <article>
6262 <header>Delete Account</header>
6363- <p>To delete your SkyScheduler account, please type your password below.<br />
6363+ <p>To delete your {APP_NAME} account, please type your password below.<br />
6464 All pending, scheduled posts + all unposted media will be deleted from this service.
65656666 <center><strong>NOTE</strong>: THIS ACTION IS <u>PERMANENT</u>.</center>
···7171 <label>
7272 Dashboard Pass: <input id="deleteAccountPass" type="password" name="password"
7373 minlength={MIN_DASHBOARD_PASS} maxlength={MAX_DASHBOARD_PASS} />
7474- <small>The password to access the SkyScheduler Dashboard</small>
7474+ <small>The password to access the {APP_NAME} Dashboard</small>
7575 </label>
7676 </form>
7777 <progress id="delSpinner" class="htmx-indicator" />
+2-1
src/layout/violationsBar.tsx
···11import { Context } from "hono";
22+import { APP_NAME } from "../limits";
23import { getViolationsForCurrentUser } from "../utils/db/violations";
3445type ViolationNoticeProps = {
···1112 if (violationData !== null) {
1213 let errorStr = "";
1314 if (violationData.tosViolation) {
1414- errorStr = "Your account is in violation of SkyScheduler usage.";
1515+ errorStr = `Your account is in violation of ${APP_NAME} usage.`;
1516 } else if(violationData.userPassInvalid) {
1617 errorStr = "Your Bluesky handle or application password is invalid. Please update these in the settings.";
1718 } else if (violationData.accountSuspended) {
+1
src/limits.ts
···11import remove from "just-remove";
2233/** APPLICATION CONFIGURATIONS **/
44+export const APP_NAME: string = "SkyScheduler";
45// minimum length of a post
56export const MIN_LENGTH: number = 1;
67// max amount of times something can be reposted
···11import { Context } from "hono";
22import AccountHandler from "../layout/account";
33import FooterCopyright from "../layout/helpers/footer";
44-import { BaseLayout } from "../layout/main";
54import NavTags from "../layout/helpers/navTags";
65import { TurnstileCaptcha, TurnstileCaptchaPreloads } from "../layout/helpers/turnstile";
66+import { BaseLayout } from "../layout/main";
77import { UsernameField } from "../layout/usernameField";
88+import { APP_NAME } from "../limits";
89910export default function ForgotPassword(props:any) {
1011 const ctx: Context = props.c;
1111- const botAccountURL:string = `https://bsky.app/profile/${ctx.env.RESET_BOT_USERNAME}`;
1212+ const botAccountURL: string = `https://bsky.app/profile/${ctx.env.RESET_BOT_USERNAME}`;
1213 return (
1313- <BaseLayout title="SkyScheduler - Forgot Password"
1414+ <BaseLayout title="Forgot Password"
1415 preloads={[...TurnstileCaptchaPreloads(ctx)]}>
1516 <NavTags />
1617 <AccountHandler title="Forgot Password Reset"
···2627 If you encounter errors, your <a href="https://bsky.app/messages/settings" class="secondary" rel="nofollow" target="_blank">Direct Communication settings</a> might be set to forbid
2728 Direct Messages from accounts you don't follow.<br /><br />
2829 It is <u>heavily recommended</u> to <a href={botAccountURL} target="_blank">follow the service account</a>.<br /><br />
2929- <small><b>NOTE</b>: SkyScheduler sends DMs via an one-way delivery method. No one (other than you) can see the account password reset URL.</small></p>
3030+ <small><b>NOTE</b>: {APP_NAME} sends DMs via an one-way delivery method. No one (other than you) can see the account password reset URL.</small></p>
3031 </center>
31323233 <UsernameField />
+5-4
src/pages/homepage.tsx
···22import { BaseLayout } from "../layout/main";
33import NavTags from "../layout/helpers/navTags";
44import {
55+ APP_NAME,
56 MAX_POSTS_PER_THREAD, MAX_REPOST_DAYS, MAX_REPOST_IN_HOURS,
67 MAX_REPOST_INTERVAL, R2_FILE_SIZE_LIMIT_IN_MB
78} from "../limits";
89910export default function Home() {
1011 return (
1111- <BaseLayout title="SkyScheduler - Home" mainClass="homepage">
1212+ <BaseLayout title="Home" mainClass="homepage">
1213 <NavTags />
1314 <section class="container">
1415 <article>
1516 <noscript><header>Javascript is required to use this website</header></noscript>
1617 <p>
1717- <strong>SkyScheduler</strong> is a
1818+ <strong>{APP_NAME}</strong> is a
1819 free, <a href="https://github.com/socksthewolf/skyscheduler" rel="nofollow" target="_blank">open source</a> service
1920 that lets you schedule and automatically repost your content on Bluesky!<br />
2021 Boost engagement and reach more people no matter what time of day!<br />
···2324 <img
2425 src="/dashboard.png"
2526 fetchpriority="high"
2626- alt="Picture of SkyScheduler Dashboard"
2727+ alt={`Picture of ${APP_NAME} Dashboard`}
2728 height="618px"
2829 width="1200px"
2930 />
3031 <figcaption>
3131- An amazing picture of SkyScheduler's Dashboard, wow!
3232+ An amazing picture of {APP_NAME}'s Dashboard, wow!
3233 </figcaption>
3334 </figure>
3435 </center>
···1717 "You can ask for the maintainer for it";
18181919 return (
2020- <BaseLayout title="SkyScheduler - Signup"
2020+ <BaseLayout title="Signup"
2121 preloads={[...TurnstileCaptchaPreloads(ctx)]}>
2222 <NavTags />
2323 <AccountHandler title="Create an Account"
+8-7
src/pages/tos.tsx
···11import FooterCopyright from "../layout/helpers/footer";
22import { BaseLayout } from "../layout/main";
33import NavTags from "../layout/helpers/navTags";
44+import { APP_NAME } from "../limits";
4556export default function TermsOfService() {
67 return (
77- <BaseLayout title="SkyScheduler - TOS" mainClass="homepage">
88+ <BaseLayout title="TOS" mainClass="homepage">
89 <NavTags />
910 <section class="container">
1011 <article>
1112 <header><h3>Terms of Service</h3></header>
1213 <h4>Terms</h4>
1314 <p>
1414- By signing up and using the services provided by SkyScheduler ("software") you agree to the terms set forth below.<br />
1515+ By signing up and using the services provided by {APP_NAME} ("software") you agree to the terms set forth below.<br />
1516 If you do not agree to said terms please delete your account at your earliest convenience. Said deletion will be treated
1617 as a separation of you to this agreement.
1718 </p>
1819 <h4>Usage</h4>
1919- <p>By using SkyScheduler you agree to:
2020+ <p>By using this software you agree to:
2021 <ol>
2122 <li>Not use the service to scam, spam or to otherwise violate the terms of the <a class="secondary" href="https://bsky.social/about/support/tos" rel="nofollow noindex noopener" target="_blank">Bluesky Terms of Service</a></li>
2223 <li>Not upload material that is illegal, illicit or stolen</li>
2324 <li>Not attempt to reverse engineer the software to cause damage or otherwise harm others</li>
2424- <li>Not hold SkyScheduler at fault for any damages, neither perceived nor tangible</li>
2525- <li>Grant SkyScheduler a temporary, non-exclusive, royalty-free license to the content that you schedule for the sole purpose of transmitting it on your behalf via the ATProtocol to the PDS of your choosing (default: Bluesky).</li>
2525+ <li>Not hold the software nor its developers at fault for any damages, neither perceived nor tangible</li>
2626+ <li>Grant {APP_NAME} a temporary, non-exclusive, royalty-free license to the content that you schedule for the sole purpose of transmitting it on your behalf via the ATProtocol to the PDS of your choosing (default: Bluesky).</li>
2627 <ul>
2728 <li>Upon successful transmission, content will be deleted from our temporary holding storage.</li>
2829 </ul>
2930 </ol>
3031 <hr />
3131- Violations of these agreements will allow SkyScheduler to terminate your access to the website. Upon account deletion/termination, all temporarily stored content will be deleted.<br />
3232+ Violations of these agreements will allow {APP_NAME} to terminate your access to the website. Upon account deletion/termination, all temporarily stored content will be deleted.<br />
3233 Deletions may take up to 30 days to fully cycle out of backups.
3334 </p>
3435 <h4>Disclaimer/Limitations</h4>
3535- <p>SkyScheduler IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
3636+ <p>{APP_NAME} IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
3637 IN NO EVENT SHALL THE AUTHORS, HOSTS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
3738 TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>
3839 <h4>Ammendum</h4>
+3-3
src/utils/dbQuery.ts
···77import { v4 as uuidv4, validate as uuidValid } from 'uuid';
88import { mediaFiles, posts, repostCounts, reposts } from "../db/app.schema";
99import { accounts, users } from "../db/auth.schema";
1010-import { MAX_POSTS_PER_THREAD, MAX_REPOST_POSTS, MAX_REPOST_RULES_PER_POST } from "../limits";
1010+import { APP_NAME, MAX_POSTS_PER_THREAD, MAX_REPOST_POSTS, MAX_REPOST_RULES_PER_POST } from "../limits";
1111import {
1212 AccountStatus,
1313 AllContext,
···196196 const violationData = await getViolationsForUser(db, userId);
197197 if (violationData != null) {
198198 if (violationData.tosViolation) {
199199- return {ok: false, msg: "This account is unable to use SkyScheduler services at this time"};
199199+ return {ok: false, msg: `This account is unable to use ${APP_NAME} services at this time`};
200200 } else if (violationData.userPassInvalid) {
201201 return {ok: false, msg: "The BSky account credentials is invalid, please update these in the settings"};
202202 }
···355355 const violationData = await getViolationsForUser(db, userId);
356356 if (violationData != null) {
357357 if (violationData.tosViolation) {
358358- return {ok: false, msg: "This account is unable to use SkyScheduler services at this time"};
358358+ return {ok: false, msg: `This account is unable to use ${APP_NAME} services at this time`};
359359 } else if (violationData.userPassInvalid) {
360360 return {ok: false, msg: "The BSky account credentials is invalid, please update these in the settings"};
361361 }
+9-2
src/utils/setup.ts
···66 if (await doesAdminExist(c))
77 return c.html("already created", 501);
8899- if (!has(c.env, "DEFAULT_ADMIN_USER") || !has(c.env, "DEFAULT_ADMIN_PASS") || !has(c.env, "DEFAULT_ADMIN_BSKY_PASS"))
1010- return c.html("invalid configuration, missing configs");
99+ const settingsToCheck:string[] =
1010+ ["DEFAULT_ADMIN_USER", "DEFAULT_ADMIN_PASS", "DEFAULT_ADMIN_BSKY_PASS"];
1111+1212+ // Loop through and check all of the settings that are easy to miss
1313+ for (const setting of settingsToCheck) {
1414+ if (!has(c.env, setting)) {
1515+ return c.text(`missing ${setting} setting!`);
1616+ }
1717+ }
11181219 const data = await c.get("auth").api.signUpEmail({
1320 body: {