Schedule posts to Bluesky with Cloudflare workers. skyscheduler.work
cf tool bsky-tool cloudflare bluesky schedule bsky service social-media cloudflare-workers
at main 72 lines 2.5 kB view raw
1import isEmpty from "just-is-empty"; 2import * as z from "zod/v4"; 3import { BSKY_VIDEO_LENGTH_LIMIT } from "../limits"; 4import { EmbedDataType } from "../types"; 5import { FileContentSchema } from "./mediaSchema"; 6import { atpRecordURI } from "./regexCases"; 7import { AltTextSchema } from "./sharedValidations"; 8 9export const ImageEmbedSchema = z.object({ 10 ...FileContentSchema.shape, 11 type: z.literal(EmbedDataType.Image), 12 ...AltTextSchema.shape, 13}); 14 15export const VideoEmbedSchema = z.object({ 16 ...FileContentSchema.shape, 17 type: z.literal(EmbedDataType.Video), 18 width: z.number("media width is not a number") 19 .min(1, "media width value is below 1") 20 .nonoptional("media width is required"), 21 height: z.number("media height is not a number") 22 .min(1, "media height value is below 1") 23 .nonoptional("media height is required"), 24 duration: z.number("media duration is invalid") 25 .min(0, "media must be over 0 seconds long") 26 .max(BSKY_VIDEO_LENGTH_LIMIT, `media must be less than ${BSKY_VIDEO_LENGTH_LIMIT} seconds long`) 27 .nonoptional("media duration is required") 28}); 29 30export const LinkEmbedSchema = z.object({ 31 /* content is the thumbnail */ 32 content: z.string().trim().prefault("").refine((value) => { 33 if (isEmpty(value)) 34 return true; 35 // So the idea here is to try to encode the string into an URL object, and if that fails 36 // then you just fail out the string. 37 try { 38 const urlWrap = new URL(value); 39 return urlWrap.protocol === "https:" || urlWrap.protocol === "http:"; 40 } catch (err) { 41 return false; 42 } 43 }, { 44 message: "the link to embed failed to parse, is it accessible?", 45 path: ["content"] 46 }), 47 type: z.literal(EmbedDataType.WebLink), 48 title: z.string().trim().default(""), 49 /* NOTE: uri is the link to the website here, 50 content is used as the thumbnail */ 51 uri: z.url({ 52 normalize: true, 53 protocol: /^https?$/, 54 hostname: z.regexes.domain, 55 error: "provided link is not an URL, please check URL and try again" 56 }).trim() 57 .nonoptional("link embeds require a url"), 58 description: z.string().trim().default("") 59}); 60 61export const PostRecordSchema = z.object({ 62 content: z.url({ 63 normalize: true, 64 protocol: /^https?$/, 65 hostname: z.regexes.domain, 66 error: "post/feed/list record url is invalid" 67 }).trim() 68 .toLowerCase() 69 .regex(atpRecordURI, "url is not a post/feed/list record") 70 .nonoptional("post/feed/list records require a url"), 71 type: z.literal(EmbedDataType.Record), 72});