Something went wrong. Try again.
Schedule posts and reposts to Bluesky (and other PDSes) with Cloudflare workers. skyscheduler.work
scheduling social-media cloudflare bsky service cloudflare-workers bsky-tool bluesky
Something went wrong. Try again.
20 kB · 825 lines
TSX
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825// Mediocre file to help with automatically generating endpoint bindings so that we can dump them to the// Cloudflare WAF to protect/log against abuseimport { Hono } from "hono";import { describeRoute, generateSpecs, resolver, validator } from "hono-openapi";import { APP_HOSTNAME_INFO, APP_NAME, SITE_URL } from "../appInfo";import type { HonoBase } from "../types";import { AccountDeleteSchema, AccountForgotSchema } from "../validation/accountForgotDeleteSchema";import { AccountResetSchema } from "../validation/accountResetSchema";import { AccountUpdateSchema } from "../validation/accountUpdateSchema";import { FileUploadSchema } from "../validation/fileUploadSchema";import { LoginSchema } from "../validation/loginSchema";import { FileDeleteSchema } from "../validation/mediaSchema";import { EditSchema, PostSchema } from "../validation/postSchema";import { RepostSchema } from "../validation/repostSchema";import { CheckFileSchema, CheckGUIDSchema, CheckRepostGUIDSchema, CreatePostResponseSchema, FileUploadFailSchema, FileUploadSuccessSchema, GenericResponseSchema, ResetCallbackQuery, ResetTokenValid} from "../validation/responseSchema";import { SignupSchema } from "../validation/signupSchema";
// Easy access change for the openapi string versionconst CURRENT_OPENAPI_VERSION: string = '1.3.3';
const openapiRoutes = new Hono<HonoBase>();
openapiRoutes.post("/post/create", describeRoute({ description: 'Makes a post', responses: { 200: { description: 'Successful post', content: { 'application/json': { schema: resolver(CreatePostResponseSchema) }, }, }, 400: { description: "Post failed to be created", content: { 'application/json': { schema: resolver(GenericResponseSchema) }, } }, 401: { description: "Post unable to be created now, will try again soon", content: { 'application/json': { schema: resolver(GenericResponseSchema) }, } }, 406: { description: 'Resource busy', content: { 'application/json': { schema: resolver(GenericResponseSchema) }, } }, 429: { description: 'In maintenance', content: { 'application/json': { schema: resolver(GenericResponseSchema) }, } }, },}),validator('json', PostSchema));
openapiRoutes.post("/post/create/repost", describeRoute({ description: 'Makes a repost post', responses: { 200: { description: 'Successful post', content: { 'application/json': { schema: resolver(CreatePostResponseSchema) }, }, }, 400: { description: "Repost failed to be created", content: { 'application/json': { schema: resolver(GenericResponseSchema) }, } }, 401: { description: "not logged in", content: { 'application/json': { schema: resolver(GenericResponseSchema) }, } }, 429: { description: 'In maintenance', content: { 'application/json': { schema: resolver(GenericResponseSchema) }, } }, },}),validator('json', RepostSchema));
// Get all postsopenapiRoutes.get("/post/all", describeRoute({ description: 'Gets all posts for the current account', responses: { 200: { description: 'post list', content: { 'text/html': { schema: resolver(CreatePostResponseSchema) }, } }, 401: { description: "not logged in", content: { 'text/html': {} } } }}));
openapiRoutes.post("/post/all", describeRoute({ description: 'Gets all posts for the current account', responses: { 200: { description: 'post list', content: { 'text/html': { schema: resolver(CreatePostResponseSchema) }, } }, 401: { description: "not logged in", content: { 'text/html': {} } } }}));
openapiRoutes.delete("/post/all", describeRoute({ description: 'Gets all posts for the current account', responses: { 200: { description: 'post list', content: { 'text/html': { schema: resolver(CreatePostResponseSchema) }, } }, 401: { description: "not logged in", content: { 'text/html': {} } } }}));
// Edit postsopenapiRoutes.get("/post/edit/:id", describeRoute({ description: 'Get the post editor for the given post', responses: { 200: { description: 'the post editor component', content: { 'text/html': {} } }, 203: { description: "post invalid or missing", content: { 'text/html': {} } }, 401: { description: "not logged in", content: { 'text/html': {} } }, 404: { description: "post doesn't exist for your account", content: { 'text/html': {} } }, 429: { description: 'In maintenance', content: { 'text/html': {} } }, }}), validator('param', CheckGUIDSchema));
openapiRoutes.post("/post/edit/:id", describeRoute({ description: "Pushes edits to the given post", responses: { 200: { description: "Edit gotten, check return for valid response", content: { 'text/html': {} } }, 403: { description: "Invalid data provided, or already posted", content: { 'text/html': {} } }, 401: { description: "not logged in", content: { 'text/html': {} } }, 404: { description: "post does not exist on your account", content: { 'text/html': {} } }, 429: { description: 'In maintenance', content: { 'text/html': {} } }, 500: { description: "internal error occurred", content: { 'text/html': {} } } }}), validator("param", CheckGUIDSchema), validator("json", EditSchema));
openapiRoutes.get("/post/edit/:id/cancel", describeRoute({ description: "Cancel editing a post", responses: { 200: { description: "Cancelation processed", content: { 'text/html': {} } }, 403: { description: "Invalid data passed", content: { 'text/html': {} } }, 401: { description: "not logged in", content: { 'text/html': {} } }, 429: { description: 'In maintenance', content: { 'text/html': {} } }, 500: { description: "internal error", content: { 'text/html': {} } } }}), validator("param", CheckGUIDSchema));
// delete a postopenapiRoutes.delete("/post/delete/:id", describeRoute({ description: "Delete the given post", responses: { 200: { description: "command processed", content: { 'text/html': {} } }, 403: { description: "an error occurred or the post doesn't exist", content: { 'text/html': {} } }, 429: { description: 'In maintenance', content: { 'text/html': {} } }, 401: { description: "not logged in", content: { 'text/html': {} } } }}), validator("param", CheckGUIDSchema));
openapiRoutes.get("/post/:id/repost", describeRoute({ description: "Get the repost editor for the given post", responses: { 200: { description: "request accepted", content: { 'text/html': {} } }, 404: { description: "an error occurred", content: { 'text/html': {} } }, 401: { description: "not logged in", content: { 'text/html': {} } }, 429: { description: "rate limited/maintenance mode", content: { 'text/html': {} } } }}), validator("param", CheckGUIDSchema));
openapiRoutes.delete("/post/:id/repost/:schedule", describeRoute({ description: "Delete the given schedule for the given post", responses: { 200: { description: "request processed", content: { 'text/html': {} } }, 403: { description: "an error occurred", content: { 'text/html': {} } }, 401: { description: "not logged in", content: { 'text/html': {} } }, 429: { description: "rate limited/maintenance mode", content: { 'text/html': {} } } }}), validator("param", CheckRepostGUIDSchema));
// Create media uploadopenapiRoutes.post("/post/upload", describeRoute({ description: "Uploads a file to the service", responses: { 200: { description: "Successfully uploaded", content: { 'application/json': { schema: resolver(FileUploadSuccessSchema)} }, }, 400: { description: "Failed to upload", content: { "application/json": { schema: resolver(FileUploadFailSchema)} } }, 401: { description: "not logged in", content: { "application/json": { schema: resolver(GenericResponseSchema)} } }, 429: { description: 'In maintenance', content: { 'application/json': { schema: resolver(GenericResponseSchema) }, } }, }}), validator("form", FileUploadSchema));
// Delete an uploadopenapiRoutes.delete("/post/upload", describeRoute({ description: "Deletes a file to the service", responses: { 200: { description: "File deleted successfully", content: { "application/json": { schema: resolver(GenericResponseSchema)} } }, 400: { description: "File unable to be deleted", content: { "application/json": { schema: resolver(GenericResponseSchema)} } }, 401: { description: "not logged in", content: { "application/json": { schema: resolver(GenericResponseSchema)} } }, 402: { description: "Invalid operation performed", content: { "application/json": { schema: resolver(GenericResponseSchema)} } }, 429: { description: 'In maintenance', content: { 'application/json': { schema: resolver(GenericResponseSchema) }, } }, }}), validator("json", FileDeleteSchema));
// wrapper to loginopenapiRoutes.post("/account/login", describeRoute({ description: "Logs in the user", responses: { 200: { description: "Login Success", content: { "application/json": { schema: resolver(GenericResponseSchema)} } }, 400: { description: "Invalid data", content: { "application/json": { schema: resolver(GenericResponseSchema)} } }, 403: { description: "Failed to login", content: { "application/json": { schema: resolver(GenericResponseSchema)} } }, 429: { description: "rate limited/maintenance mode", content: { "application/json": { schema: resolver(GenericResponseSchema)} } } }}), validator("json", LoginSchema));
openapiRoutes.post("/account/update", describeRoute({ description: "Updates account settings", responses: { 200: { description: "operation return", content: { 'text/html': { }, } }, 201: { description: "no changes made", content: { 'text/html': { }, } }, 401: { description: "not logged in", content: { 'text/html': { }, } }, 403: { description: "invalid data", content: { 'text/html': { }, } }, 409: { description: "failed to update, try again", content: { 'text/html': { }, } }, 422: { description: "invalid username", content: { 'text/html': { }, } }, 429: { description: "rate limited/maintenance mode", content: { 'text/html': { }, } } }}), validator("form", AccountUpdateSchema, undefined, { media: "application/x-www-form-urlencoded" }));
openapiRoutes.get("/account/data", describeRoute({ description: "Returns the user's current dashboard data with HTMX swap bands", responses: { 200: { description: "success", content: { 'text/html': { }, } }, 401: { description: "not logged in", content: { 'text/html': { }, } }, 403: { description: "invalid user data", content: { 'text/plain': { }, } } }}));
// endpoint that returns any violationsopenapiRoutes.get("/account/violations", describeRoute({ description: "Gets the current violations for the user", responses: { 200: { description: "success", content: { 'text/html': { }, } }, 401: { description: "not logged in", content: { 'text/html': { }, } } }}));
// endpoint that returns any violationsopenapiRoutes.post("/account/violations/resolve", describeRoute({ description: "Clears any BSky account violations that the user might have", responses: { 200: { description: "success", content: { 'text/html': { }, } }, 401: { description: "not logged in", content: { 'text/html': { }, } }, 429: { description: 'In maintenance', content: { 'text/html': {} } }, }}));
// proxy the logout call because of course this wouldn't work properly anywaysopenapiRoutes.post("/account/logout", describeRoute({ description: "Logs out the user via HTMX", responses: { 200: { description: "logged out", content: { 'text/plain': {} } }, 401: { description: "not logged in", content: { 'text/html': { }, } } }}));
openapiRoutes.get("/account/logout", describeRoute({ description: "Logs out the user (redirects)", responses: { 200: { description: "log out", content: { 'text/html': { }, } }, 401: { description: "not logged in", content: { 'text/html': { }, } } }}));
openapiRoutes.post("/account/signup", describeRoute({ description: "sign up for an account", responses: { 200: { description: "successfully signed up", content: { 'application/json': { schema: resolver(GenericResponseSchema) }, } }, 400: { description: "invalid data", content: { 'application/json': { schema: resolver(GenericResponseSchema) }, } }, 401: { description: "failed turnstile", content: { 'application/json': { schema: resolver(GenericResponseSchema) }, } }, 429: { description: 'rate limit/in maintenance', content: { 'application/json': { schema: resolver(GenericResponseSchema) }, } }, 500: { description: "internal error", content: { 'application/json': { schema: resolver(GenericResponseSchema) }, } } }}), validator("json", SignupSchema));
openapiRoutes.post("/account/forgot", describeRoute({ description: "attempt to get a password reset message", responses: { 200: { description: "successfully sent message", content: { 'application/json': { schema: resolver(GenericResponseSchema) }, } }, 400: { description: "invalid data", content: { 'application/json': { schema: resolver(GenericResponseSchema) }, } }, 401: { description: "failed turnstile", content: { 'application/json': { schema: resolver(GenericResponseSchema) }, } }, 429: { description: 'In maintenance', content: { 'application/json': { schema: resolver(GenericResponseSchema) }, } }, 500: { description: "internal error", content: { 'application/json': { schema: resolver(GenericResponseSchema) }, } } }}), validator("json", AccountForgotSchema));
openapiRoutes.post("/account/reset", describeRoute({ description: "attempt to reset password", responses: { 200: { description: "successfully reset password", content: { 'application/json': { schema: resolver(GenericResponseSchema) }, } }, 400: { description: "invalid data", content: { 'application/json': { schema: resolver(GenericResponseSchema) }, } }, 401: { description: "incorrect user/pw", content: { 'application/json': { schema: resolver(GenericResponseSchema) }, } }, 429: { description: "rate limited/in maintenance", content: { 'application/json': { schema: resolver(GenericResponseSchema) }, } }, 500: { description: "internal error", content: { 'application/json': { schema: resolver(GenericResponseSchema) }, } } }}), validator("json", AccountResetSchema));
openapiRoutes.post("/account/delete", describeRoute({ description: "attempt to delete the current account", responses: { 200: { description: "account deleted", content: { 'text/html': { }, } }, 401: { description: "incorrect user/pw", content: { 'text/html': { }, } }, 403: { description: "invalid data", content: { 'text/html': { }, } }, 429: { description: 'In maintenance', content: { 'text/html': {} } }, 500: { description: "internal error", content: { 'text/html': { }, } } }}), validator("form", AccountDeleteSchema, undefined, { media: "application/x-www-form-urlencoded" }));
openapiRoutes.get("/preview/file/:id", describeRoute({ description: "preview a file", responses: { 200: { description: "displays file" }, 301: { description: "file not found" }, 303: { description: "auth cannot be verified" }, 307: { description: "preview cannot be displayed due to site settings" }, 401: { description: "not logged in" } }}), validator("param", CheckFileSchema));
openapiRoutes.get("/reset-password/:token", describeRoute({ description: "Hands off reset token validation, cannot fail directly", responses: { 302: { description: "handed off" } }}), validator("param", ResetTokenValid));
openapiRoutes.get("/api/auth/reset-password/:token", describeRoute({ description: "Validates password reset tokens", responses: { 302: { description: "handed off" } }}), validator("param", ResetTokenValid), validator("query", ResetCallbackQuery));
openapiRoutes.get("/funding", describeRoute({ description: "Gets current server funding progress", responses: { 200: { description: "current funding goal progress", content: { 'text/html': { }, } }, }}));
export async function generateOpenAPI() { return generateSpecs(openapiRoutes, { documentation: { info: { title: `${APP_NAME} API Routes`, version: CURRENT_OPENAPI_VERSION, description: `The API Routes for ${APP_NAME} for access or addition to the Cloudflare API Shield`, termsOfService: `${SITE_URL}/tos`, license: { name: "MIT", } }, openapi: "3.0.3", servers: APP_HOSTNAME_INFO.hostnames.map((itm) => { return {url: `https://${itm}`, description: "Server"} }) }, });};