···1616 <AccountHandler title="Forgot Password Reset"
1717 submitText="Request Password Reset"
1818 loadingText="Requesting Password Reset..." endpoint="/account/forgot"
1919- successText="Success! Check your bsky dms for info. Redirecting to home.."
1919+ successText="Success! Check your DMs for info. Redirecting to home.."
2020 redirect="/"
2121 footerHTML={<FooterCopyright />}>
22222323 <center hx-history="false">
2424 <p>You will receive a Direct Message from <code>@{ctx.env.RESET_BOT_USERNAME}</code> on Bluesky with a link to reset your password.<br /><br />
2525 If you encounter errors, your Bluesky Communication settings might be set to forbid contact via Direct Messages from accounts you don't follow.<br />
2626- It is <u>heavily recommended</u> that <a href={botAccountURL} target="_blank">you follow the service account</a>.</p>
2626+ It is <u>heavily recommended</u> that <a href={botAccountURL} target="_blank">you follow the service account</a>.<br />
2727+ <b>NOTE</b>: DMs are sent one way. Your account reset URL can only be seen by you.</p>
2728 </center>
28292930 <UsernameField />
+1-1
src/pages/login.tsx
···2121 <label hx-history="false">
2222 Dashboard Password
2323 <DashboardPasswordField autocomplete={PWAutoCompleteSettings.CurrentPass} required={true} />
2424- <small><b>NOTE</b>: This password is not related to your bluesky account!</small>
2424+ <small><b>NOTE</b>: This password is not related to your Bluesky account!</small>
2525 </label>
2626 </AccountHandler>
2727 </BaseLayout>
+2-2
src/pages/privacy.tsx
···4444 <div style="margin-left: 15px">
4545 <strong>Note that</strong>:
4646 <ul>
4747- <li>Data is not accessible to the maintainers of the website</li>
4747+ <li>Data is not accessible to the maintainers of this service</li>
4848 <li>We do not sell your data to any third party</li>
4949 <li>No data is used for genAI purposes nor for training generative AI models</li>
5050- <li>You can verify all of this by just looking at <a href="https://github.com/socksthewolf/skyscheduler" class="secondary" ref="noopener nofollow">the source code</a></li>
5050+ <li>You can verify this by just looking at <a href="https://github.com/socksthewolf/skyscheduler" class="secondary" ref="noopener nofollow">the source code</a></li>
5151 </ul>
5252 </div>
5353 </p>
+4-3
src/pages/tos.tsx
···1919 <h4>Usage</h4>
2020 <p>By using SkyScheduler you agree to:
2121 <ol>
2222- <li>Not use the service to 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>
2222+ <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>
2323 <li>Not upload material that is illegal, illicit or stolen</li>
2424 <li>Not attempt to reverse engineer the software to cause damage or otherwise harm others</li>
2525 <li>Not hold SkyScheduler at fault for any damages, neither perceived nor tangible</li>
2626- <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 to the ATProtocol of the PDS of your choosing (default: Bluesky).</li>
2626+ <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>
2727 <ul>
2828 <li>Upon successful transmission, content will be deleted from our temporary holding storage.</li>
2929 </ul>
3030 </ol>
3131 <hr />
3232- 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.
3232+ 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 />
3333+ Deletions may take up to 30 days to fully cycle out of backups.
3334 </p>
3435 <h4>Disclaimer/Limitations</h4>
3536 <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.
+1-1
src/utils/bskyMsg.ts
···3333 try {
3434 await agent.chat.bsky.convo.deleteMessageForSelf({convoId: convoId, messageId: messageId}, chatHeaders);
3535 } catch(err) {
3636- console.error(`failed to delete message for self, got error ${err}`);
3636+ console.error(`failed to delete reset message for self, got error ${err}`);
3737 }
3838 // Message has been sent.
3939 return true;
+4
src/utils/bskyPrune.ts
···44import split from 'just-split';
55import isEmpty from 'just-is-empty';
6677+// This looks for a bunch of posts that are posted and determines if the posts
88+// are still on the network or not. If they are not, then this prunes the posts from
99+// the database. This call is quite expensive and should only be ran on a weekly
1010+// cron job.
711export const pruneBskyPosts = async (env: Bindings, userId?:string) => {
812 const allPostedPosts = (userId !== undefined) ? await getAllPostedPostsOfUser(env, userId) : await getAllPostedPosts(env);
913 let removePostIds: string[] = [];
+1-1
src/utils/constScriptGen.ts
···33 MAX_LENGTH, MAX_ALT_TEXT, R2_FILE_SIZE_LIMIT, MAX_THUMBNAIL_SIZE } from "../limits.d";
44import { PreloadRules } from "../types.d";
5566-export const CONST_SCRIPT_VERSION: number = 5;
66+const CONST_SCRIPT_VERSION: number = 5;
7788const makeFileTypeStr = (typeMap: string[]) => {
99 return typeMap.map((type) => `"${type}"`).join()
+1-1
src/utils/dbQuery.ts
···391391};
392392393393/** Maintenance operations **/
394394-export const runMaintenenceUpdates = async (env: Bindings) => {
394394+export const runMaintenanceUpdates = async (env: Bindings) => {
395395 const db: DrizzleD1Database = drizzle(env.DB);
396396 // Create a posted query that also checks for valid json and content length
397397 const postedQuery = db.select({
···11-import * as z from "zod/v4";
21import { MAX_DASHBOARD_PASS, MIN_DASHBOARD_PASS } from "../limits.d";
22+import * as z from "zod/v4";
3344export const AccountResetSchema = z.object({
55- resetToken: z.string().nonempty(),
55+ resetToken: z.string().nonempty("reset token is missing!"),
66 password: z.string().trim()
77 .min(MIN_DASHBOARD_PASS, "password too short")
88 .max(MAX_DASHBOARD_PASS, "password too long")
99 .nonempty("password cannot be empty")
1010 .nonoptional(),
1111 confirmPassword: z.string().trim()
1212- .min(MIN_DASHBOARD_PASS, "password too short")
1313- .max(MAX_DASHBOARD_PASS, "password too long")
1414- .nonempty("password cannot be empty")
1212+ .min(MIN_DASHBOARD_PASS, "confirm password too short")
1313+ .max(MAX_DASHBOARD_PASS, "confirm password too long")
1414+ .nonempty("confirm password cannot be empty")
1515 .nonoptional(),
1616}).refine((schema) => schema.confirmPassword === schema.password, "Passwords do not match");
+4-9
src/validation/embedSchema.ts
···11-import { MAX_ALT_TEXT, BSKY_VIDEO_LENGTH_LIMIT } from "../limits.d";
11+import { BSKY_VIDEO_LENGTH_LIMIT } from "../limits.d";
22import { EmbedDataType } from "../types.d";
33+import { AltTextSchema } from "./sharedValidations";
34import { FileContentSchema } from "./mediaSchema";
45import { postRecordURI } from "./regexCases";
56import * as z from "zod/v4";
67import isEmpty from "just-is-empty";
77-88-export const AltTextSchema = z.object({
99- alt: z.string().trim()
1010- .max(MAX_ALT_TEXT, "alt text is too long")
1111- .prefault("")
1212-});
138149export const ImageEmbedSchema = z.object({
1510 ...FileContentSchema.shape,
···4641 return false;
4742 }
4843 }, {
4949- message: "The link embed contained invalid data, please check your URL and try again",
4444+ message: "the link to embed failed to parse, is it accessible?",
5045 path: ["content"]
5146 }),
5247 type: z.literal(EmbedDataType.WebLink),
···5752 normalize: true,
5853 protocol: /^https?$/,
5954 hostname: z.regexes.domain,
6060- error: "provided weblink is not in the correct form of an url"
5555+ error: "provided link is not an URL, please check URL and try again"
6156 }).trim()
6257 .nonoptional("link embeds require a url"),
6358 description: z.string().trim().default("")
+1-1
src/validation/loginSchema.ts
···11-import * as z from "zod/v4";
21import { PasswordSchema, UsernameSchema } from "./sharedValidations";
22+import * as z from "zod/v4";
3344// Schema for login validation
55export const LoginSchema = z.object({
+3-1
src/validation/mediaSchema.ts
···22import { fileKeyRegex } from "./regexCases";
3344export const FileContentSchema = z.object({
55- content: z.string().toLowerCase().regex(fileKeyRegex, "file key is invalid").nonempty("file key was empty")
55+ content: z.string().toLowerCase()
66+ .regex(fileKeyRegex, "file key is invalid")
77+ .nonempty("file key was empty")
68});
79810export const FileDeleteSchema = FileContentSchema;
+3-1
src/validation/postSchema.ts
···11import { MIN_LENGTH, MAX_REPOST_INTERVAL_LIMIT, MAX_REPOST_IN_HOURS, MAX_LENGTH } from "../limits.d";
22-import { AltTextSchema, ImageEmbedSchema, LinkEmbedSchema, PostRecordSchema, VideoEmbedSchema } from "./embedSchema";
22+import { ImageEmbedSchema, LinkEmbedSchema, PostRecordSchema, VideoEmbedSchema } from "./embedSchema";
33import { FileContentSchema } from "./mediaSchema";
44import { EmbedDataType, PostLabel } from "../types.d";
55import * as z from "zod/v4";
66+import { AltTextSchema } from "./sharedValidations";
6778const TextContent = z.object({
89 content: z.string().trim()
···3536 }
3637 }, "Invalid date format. Please use ISO 8601 format (e.g. 2024-12-14T07:17:05+01:00)"),
3738}).superRefine(({embeds, label}, ctx) => {
3939+ // Check that labels are properly set if we have embed data
3840 if (embeds !== undefined && embeds.length > 0 && label === undefined) {
3941 // If it's only a quote post and nothing else, then no content label is required.
4042 if (embeds.length == 1 && embeds[0].type == EmbedDataType.Record)
+3
src/validation/regexCases.ts
···11+// passwords are 4 groups of 4 char separated by dashes
12export const appPasswordRegex = /(?:[0-9a-z]{4}-){3}[0-9a-z]{4}/i;
33+// GUID + file extensions
24export const fileKeyRegex = /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})\.(png|jp[e]?g|bmp|webp|heic|svg|mp[4|4v|2]|qt|mpg4|m4v|[a]?gif|webm|mp[e]?g|m[1-2]v|mov)$/i;
55+// Given a link to a post/profile record
36export const postRecordURI = /(?:^.*\/profile\/)(?<account>[0-9a-zA-Z\-\.\:]+)\/(?<type>post|feed|lists|follows)\/(?<postid>[a-z0-9]+)(?:\/)?$/i;
+7-1
src/validation/sharedValidations.ts
···11import * as z from "zod/v4";
22import { appPasswordRegex } from "./regexCases";
33-import { BSKY_MAX_APP_PASSWORD_LENGTH, BSKY_MIN_USERNAME_LENGTH, BSKY_MAX_USERNAME_LENGTH, MAX_DASHBOARD_PASS, MIN_DASHBOARD_PASS } from "../limits.d";
33+import { BSKY_MAX_APP_PASSWORD_LENGTH, BSKY_MIN_USERNAME_LENGTH, BSKY_MAX_USERNAME_LENGTH, MAX_DASHBOARD_PASS, MIN_DASHBOARD_PASS, MAX_ALT_TEXT } from "../limits.d";
4455export const UsernameSchema = z.object({
66 username: z.string().trim().toLowerCase()
···2424 .nonempty("missing bsky app password")
2525 .max(BSKY_MAX_APP_PASSWORD_LENGTH, "app password too long")
2626 .regex(appPasswordRegex, "please go back and recreate your app password from your bsky settings")
2727+});
2828+2929+export const AltTextSchema = z.object({
3030+ alt: z.string().trim()
3131+ .max(MAX_ALT_TEXT, "alt text is too long")
3232+ .prefault("")
2733});