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

Improve Drizzle DB performance

Closes #86

+454 -241
+7 -3
package.json
··· 3 3 "scripts": { 4 4 "dev": "wrangler dev --live-reload", 5 5 "dev:queue": "wrangler dev -c wrangler.toml --persist-to .wrangler/state --live-reload", 6 - "deploy": "wrangler deploy --minify", 6 + "deploy": "wrangler deploy --minify --env=''", 7 + "staging": "wrangler deploy --env staging --minify", 7 8 "auth": "npm run auth:generate && npm run auth:format", 8 9 "auth:generate": "npx @better-auth/cli@latest generate --config src/auth/index.ts --output src/db/auth.schema.ts -y", 9 10 "auth:format": "prettier --write src/db/auth.schema.ts", ··· 14 15 "migrate:prod:db": "wrangler d1 migrations apply skyposts --remote", 15 16 "migrate:prod:pragma": "wrangler d1 execute skyposts --command \"PRAGMA optimize\" --remote -y", 16 17 "migrate:prod": "run-s migrate:prod:*", 17 - "migrate:remote": "npm run migrate:prod", 18 - "migrate:optimize": "npm run migrate:prod:pragma && npm run migrate:local:pragma", 18 + "migrate:staging:db": "wrangler d1 migrations apply skyposts-dev --remote", 19 + "migrate:staging:pragma": "wrangler d1 execute skyposts-dev --command \"PRAGMA optimize\" --remote -y", 20 + "migrate:staging": "run-s migrate:staging:*", 21 + "migrate:remote": "npm run migrate:prod && npm run migrate:staging", 22 + "migrate:optimize": "npm run migrate:prod:pragma && npm run migrate:staging:pragma && npm run migrate:local:pragma", 19 23 "migrate:all": "npm run migrate:local && npm run migrate:prod", 20 24 "minify:repost": "minify assets/js/repostHelper.js > assets/js/repostHelper.min.js", 21 25 "minify:post": "minify assets/js/postHelper.js > assets/js/postHelper.min.js",
+2 -1
src/auth/index.ts
··· 2 2 import { withCloudflare } from "better-auth-cloudflare"; 3 3 import { drizzleAdapter } from "better-auth/adapters/drizzle"; 4 4 import { username } from "better-auth/plugins"; 5 - import { drizzle } from "drizzle-orm/d1"; 5 + import { drizzle, DrizzleD1Database } from "drizzle-orm/d1"; 6 6 import { schema } from "../db"; 7 7 import { BSKY_MAX_USERNAME_LENGTH, BSKY_MIN_USERNAME_LENGTH } from "../limits"; 8 8 import { Bindings } from "../types.d"; ··· 160 160 userId: string; 161 161 isAdmin: boolean; 162 162 session: Session; 163 + db: DrizzleD1Database; 163 164 pds: string; 164 165 }; 165 166
+3 -3
src/endpoints/account.tsx
··· 163 163 } 164 164 165 165 // Check if the user has violated TOS. 166 - if (await userHasBan(c.env, profileDID)) { 166 + if (await userHasBan(c, profileDID)) { 167 167 return c.json({ok: false, message: "your account has been forbidden from using this service"}, 400); 168 168 } 169 169 ··· 214 214 return c.json({ok: false, message: "user doesn't exist"}, 401); 215 215 } 216 216 217 - const userEmail = await getUserEmailForHandle(c.env, username); 217 + const userEmail = await getUserEmailForHandle(c, username); 218 218 if (isEmpty(userEmail)) { 219 219 return c.json({ok: false, message: "user data is missing"}, 401); 220 220 } ··· 305 305 password: password 306 306 }); 307 307 if (verify) { 308 - c.executionCtx.waitUntil(getAllMediaOfUser(c.env, userId) 308 + c.executionCtx.waitUntil(getAllMediaOfUser(c, userId) 309 309 .then((media) => deleteFromR2(c, media)) 310 310 .then(() => authCtx.internalAdapter.deleteSessions(userId)) 311 311 .then(() => authCtx.internalAdapter.deleteUser(userId)));
+13 -14
src/index.tsx
··· 1 + import { drizzle } from "drizzle-orm/d1"; 1 2 import { Env, Hono } from "hono"; 2 3 import { ContextVariables, createAuth } from "./auth"; 3 4 import { account } from "./endpoints/account"; ··· 36 37 app.use("*", async (c, next) => { 37 38 const auth = createAuth(c.env, (c.req.raw as any).cf || {}); 38 39 c.set("auth", auth); 40 + c.set("db", drizzle(c.env.DB)); 39 41 await next(); 40 42 }); 41 43 ··· 103 105 104 106 // Admin Maintenance Cleanup 105 107 app.get("/cron", authAdminOnlyMiddleware, async (c) => { 106 - await schedulePostTask(c.env, c.executionCtx); 108 + await schedulePostTask(c); 107 109 return c.text("ran"); 108 110 }); 109 111 110 112 app.get("/cron-clean", authAdminOnlyMiddleware, (c) => { 111 - c.executionCtx.waitUntil(cleanUpPostsTask(c.env, c.executionCtx)); 113 + c.executionCtx.waitUntil(cleanUpPostsTask(c)); 112 114 return c.text("ran"); 113 115 }); 114 116 115 117 app.get("/db-update", authAdminOnlyMiddleware, (c) => { 116 - c.executionCtx.waitUntil(runMaintenanceUpdates(c.env)); 118 + c.executionCtx.waitUntil(runMaintenanceUpdates(c)); 117 119 return c.text("ran"); 118 120 }); 119 121 120 122 app.get("/abandoned", authAdminOnlyMiddleware, async (c) => { 121 123 let returnHTML = ""; 122 - const abandonedFiles: string[] = await getAllAbandonedMedia(c.env); 124 + const abandonedFiles: string[] = await getAllAbandonedMedia(c); 123 125 // print out all abandoned files 124 126 for (const file of abandonedFiles) { 125 127 returnHTML += `${file}\n`; 126 128 } 127 129 if (c.env.R2_SETTINGS.auto_prune == true) { 128 130 console.log("pruning abandoned files..."); 129 - await cleanupAbandonedFiles(c.env, c.executionCtx); 131 + await cleanupAbandonedFiles(c); 130 132 } 131 133 132 134 if (returnHTML.length == 0) { ··· 143 145 144 146 export default { 145 147 scheduled(event: ScheduledEvent, env: Bindings, ctx: ExecutionContext) { 148 + const runtimeWrapper = new ScheduledContext(env, ctx); 146 149 switch (event.cron) { 147 150 case "30 17 * * sun": 148 - ctx.waitUntil(cleanUpPostsTask(env, ctx)); 151 + ctx.waitUntil(cleanUpPostsTask(runtimeWrapper)); 149 152 break; 150 153 default: 151 154 case "0 * * * *": 152 - ctx.waitUntil(schedulePostTask(env, ctx)); 155 + ctx.waitUntil(schedulePostTask(runtimeWrapper)); 153 156 break; 154 157 } 155 158 }, 156 - async queue(batch: MessageBatch<QueueTaskData>, environment: Bindings, ctx: ExecutionContext) { 157 - const runtimeWrapper: ScheduledContext = { 158 - executionCtx: ctx, 159 - env: environment 160 - }; 161 - 162 - const delay: number = environment.QUEUE_SETTINGS.delay_val; 159 + async queue(batch: MessageBatch<QueueTaskData>, env: Bindings, ctx: ExecutionContext) { 160 + const runtimeWrapper = new ScheduledContext(env, ctx); 161 + const delay: number = env.QUEUE_SETTINGS.delay_val; 163 162 let wasSuccess: boolean = false; 164 163 for (const message of batch.messages) { 165 164 switch (message.body.type) {
+5 -1
src/layout/violationsBar.tsx
··· 1 1 import { Context } from "hono"; 2 2 import { getViolationsForCurrentUser } from "../utils/db/violations"; 3 3 4 - export async function ViolationNoticeBar(props: any) { 4 + type ViolationNoticeProps = { 5 + ctx: Context; 6 + } 7 + 8 + export async function ViolationNoticeBar(props: ViolationNoticeProps) { 5 9 const ctx: Context = props.ctx; 6 10 const violationData = await getViolationsForCurrentUser(ctx); 7 11 if (violationData !== null) {
+6 -3
src/pages/dashboard.tsx
··· 1 1 import { Context } from "hono"; 2 2 import { AltTextDialog } from "../layout/altTextModal"; 3 - import { IncludeDependencyTags, ScriptTags } from "../layout/helpers/includesTags"; 4 3 import FooterCopyright from "../layout/helpers/footer"; 4 + import { IncludeDependencyTags, ScriptTags } from "../layout/helpers/includesTags"; 5 5 import { BaseLayout } from "../layout/main"; 6 6 import { PostCreation, PreloadPostCreation } from "../layout/makePost"; 7 7 import { MakeRetweet } from "../layout/makeRetweet"; 8 8 import { ScheduledPostList } from "../layout/postList"; 9 9 import { Settings, SettingsButton } from "../layout/settings"; 10 10 import { ViolationNoticeBar } from "../layout/violationsBar"; 11 + import { SHOW_PROGRESS_BAR } from "../progress"; 11 12 import { PreloadRules } from "../types.d"; 12 - import { altTextScriptStr, appScriptStr, appScriptStrs, postHelperScriptStr, repostHelperScriptStr, tributeScriptStr } from "../utils/appScripts"; 13 - import { SHOW_PROGRESS_BAR } from "../progress"; 13 + import { 14 + altTextScriptStr, appScriptStr, appScriptStrs, 15 + postHelperScriptStr, repostHelperScriptStr, tributeScriptStr 16 + } from "../utils/appScripts"; 14 17 15 18 export default function Dashboard(props:any) { 16 19 const ctx: Context = props.c;
+21 -4
src/types.d.ts
··· 4 4 /*** Settings config wrappers for bindings ***/ 5 5 type ImageConfigSettings = { 6 6 enabled: boolean; 7 - steps: number[]; 8 - bucket_url: string; 7 + steps?: number[]; 8 + bucket_url?: string; 9 9 }; 10 10 11 11 type SignupConfigSettings = { ··· 47 47 IMAGES: ImagesBinding; 48 48 POST_QUEUE1: Queue; 49 49 QUEUE_SETTINGS: QueueConfigSettings; 50 - INVITE_POOL: KVNamespace; 50 + INVITE_POOL?: KVNamespace; 51 51 IMAGE_SETTINGS: ImageConfigSettings; 52 52 SIGNUP_SETTINGS: SignupConfigSettings; 53 53 SITE_SETTINGS: SiteConfigSettings; ··· 215 215 href: string; 216 216 }; 217 217 218 - export type ScheduledContext = { 218 + export class ScheduledContext { 219 219 executionCtx: ExecutionContext; 220 220 env: Bindings; 221 + #map: Map<string, any>; 222 + constructor(env: Bindings, executionCtx: ExecutionContext) { 223 + this.#map = new Map<string, any>(); 224 + this.env = env; 225 + this.executionCtx = executionCtx; 226 + this.set("db", drizzle(env.DB)); 227 + } 228 + get(name: string) { 229 + if (this.#map.has(name)) 230 + return this.#map.get(name); 231 + return null; 232 + } 233 + set(name: string, value: any) { 234 + this.#map.set(name, value); 235 + } 221 236 }; 237 + 238 + export type AllContext = Context|ScheduledContext; 222 239 223 240 export type BskyEmbedWrapper = { 224 241 type: EmbedDataType;
+19 -20
src/utils/bskyApi.ts
··· 9 9 import { 10 10 Bindings, BskyEmbedWrapper, BskyRecordWrapper, EmbedData, EmbedDataType, 11 11 LooseObj, Post, PostLabel, AccountStatus, 12 - PostRecordResponse, PostStatus, Repost, ScheduledContext 12 + PostRecordResponse, PostStatus, Repost, ScheduledContext, 13 + AllContext 13 14 } from '../types.d'; 14 15 import { atpRecordURI } from '../validation/regexCases'; 15 16 import { bulkUpdatePostedData, getChildPostsOfThread, isPostAlreadyPosted, setPostNowOffForPost } from './db/data'; ··· 104 105 return AccountStatus.UnhandledError; 105 106 } 106 107 107 - export const makeAgentForUser = async (env: Bindings, userId: string) => { 108 - const loginCreds = await getBskyUserPassForId(env, userId); 108 + export const makeAgentForUser = async (c: AllContext, userId: string) => { 109 + const loginCreds = await getBskyUserPassForId(c, userId); 109 110 if (loginCreds.valid === false) { 110 111 console.error(`credentials for user ${userId} were invalid`); 111 112 return null; ··· 116 117 117 118 const loginResponse: AccountStatus = await loginToBsky(agent, username, password); 118 119 if (loginResponse != AccountStatus.Ok) { 119 - const addViolation: boolean = await createViolationForUser(env, userId, loginResponse); 120 + const addViolation: boolean = await createViolationForUser(c, userId, loginResponse); 120 121 if (addViolation) 121 122 console.error(`Unable to login to ${userId} with violation ${loginResponse}`); 122 123 return null; ··· 124 125 return agent; 125 126 } 126 127 127 - export const makePost = async (c: Context|ScheduledContext, content: Post|null, usingAgent: AtpAgent|null=null) => { 128 + export const makePost = async (c: AllContext, content: Post|null, usingAgent: AtpAgent|null=null) => { 128 129 if (content === null) { 129 130 console.warn("Dropping invocation of makePost, content was null"); 130 131 return false; 131 132 } 132 133 133 - const env = c.env; 134 134 // make a check to see if the post has already been posted onto bsky 135 135 // skip over this check if we are a threaded post, as we could have had a child post that didn't make it. 136 - if (!content.isThreadRoot && await isPostAlreadyPosted(env, content.postid)) { 136 + if (!content.isThreadRoot && await isPostAlreadyPosted(c, content.postid)) { 137 137 console.log(`Dropped handling make post for post ${content.postid}, already posted.`); 138 138 return true; 139 139 } 140 140 141 - const agent: AtpAgent|null = (usingAgent === null) ? await makeAgentForUser(env, content.user) : usingAgent; 141 + const agent: AtpAgent|null = (usingAgent === null) ? await makeAgentForUser(c, content.user) : usingAgent; 142 142 if (agent === null) { 143 143 console.warn(`could not make agent for post ${content.postid}`); 144 144 return false; 145 145 } 146 146 147 - const newPostRecords: PostStatus|null = await makePostRaw(env, content, agent); 147 + const newPostRecords: PostStatus|null = await makePostRaw(c, content, agent); 148 148 if (newPostRecords !== null) { 149 - await bulkUpdatePostedData(env, newPostRecords.records, newPostRecords.expected == newPostRecords.got); 149 + await bulkUpdatePostedData(c, newPostRecords.records, newPostRecords.expected == newPostRecords.got); 150 150 151 151 // Delete any embeds if they exist. 152 152 for (const record of newPostRecords.records) { ··· 164 164 165 165 // Turn off the post now flag if we failed. 166 166 if (content.postNow) { 167 - c.executionCtx.waitUntil(setPostNowOffForPost(env, content.postid)); 167 + c.executionCtx.waitUntil(setPostNowOffForPost(c, content.postid)); 168 168 } 169 169 return false; 170 170 } 171 171 172 - export const makeRepost = async (c: Context|ScheduledContext, content: Repost, usingAgent: AtpAgent|null=null) => { 173 - const env = c.env; 172 + export const makeRepost = async (c: AllContext, content: Repost, usingAgent: AtpAgent|null=null) => { 174 173 let bWasSuccess = true; 175 - const agent: AtpAgent|null = (usingAgent === null) ? await makeAgentForUser(env, content.userId) : usingAgent; 174 + const agent: AtpAgent|null = (usingAgent === null) ? await makeAgentForUser(c, content.userId) : usingAgent; 176 175 if (agent === null) { 177 176 console.warn(`could not make agent for repost ${content.postid}`); 178 177 return false; ··· 196 195 return bWasSuccess; 197 196 }; 198 197 199 - export const makePostRaw = async (env: Bindings, content: Post, agent: AtpAgent): Promise<PostStatus|null> => { 200 - const username = await getUsernameForUserId(env, content.user); 198 + export const makePostRaw = async (c: AllContext, content: Post, agent: AtpAgent): Promise<PostStatus|null> => { 199 + const username = await getUsernameForUserId(c, content.user); 201 200 // incredibly unlikely but we'll handle it 202 201 if (username === null) { 203 202 console.warn(`username for post ${content.postid} was invalid`); ··· 294 293 // embed thumbnails of any size 295 294 // it will fail when you try to make the post record, saying the 296 295 // post record is invalid. 297 - const imgTransform = (await env.IMAGES.input(imageBlob.stream()) 296 + const imgTransform = (await c.env.IMAGES.input(imageBlob.stream()) 298 297 .transform({width: 1280, height: 720, fit: "scale-down"}) 299 298 .output({ format: "image/jpeg", quality: 85 })).response(); 300 299 if (imgTransform.ok) { ··· 429 428 } 430 429 431 430 // Otherwise pull files from storage 432 - const file = await env.R2.get(currentEmbed.content); 431 + const file = await c.env.R2.get(currentEmbed.content); 433 432 if (!file) { 434 433 console.warn(`Could not get the file ${currentEmbed.content} from R2 for post!`); 435 434 return false; ··· 447 446 } 448 447 } 449 448 // Give violation mediaTooBig if the file is too large. 450 - await createViolationForUser(env, postData.user, AccountStatus.MediaTooBig); 449 + await createViolationForUser(c, postData.user, AccountStatus.MediaTooBig); 451 450 console.warn(`Unable to upload ${currentEmbed.content} for post ${postData.postid} with err ${err}`); 452 451 return false; 453 452 } ··· 609 608 610 609 // If this is a post thread root 611 610 if (content.isThreadRoot) { 612 - const childPosts = await getChildPostsOfThread(env, content.postid) || []; 611 + const childPosts = await getChildPostsOfThread(c, content.postid) || []; 613 612 expected += childPosts.length; 614 613 // get the thread children. 615 614 for (const child of childPosts) {
+3 -3
src/utils/bskyPrune.ts
··· 1 1 import isEmpty from 'just-is-empty'; 2 2 import split from 'just-split'; 3 - import { Bindings } from '../types.d'; 3 + import { AllContext } from '../types.d'; 4 4 import { getPostRecords } from './bskyApi'; 5 5 import { getAllPostedPosts, getAllPostedPostsOfUser } from './db/data'; 6 6 ··· 8 8 // are still on the network or not. If they are not, then this prunes the posts from 9 9 // the database. This call is quite expensive and should only be ran on a weekly 10 10 // cron job. 11 - export const pruneBskyPosts = async (env: Bindings, userId?: string) => { 12 - const allPostedPosts = (userId !== undefined) ? await getAllPostedPostsOfUser(env, userId) : await getAllPostedPosts(env); 11 + export const pruneBskyPosts = async (c: AllContext, userId?: string) => { 12 + const allPostedPosts = (userId !== undefined) ? await getAllPostedPostsOfUser(c, userId) : await getAllPostedPosts(c); 13 13 let removePostIds: string[] = []; 14 14 let postedGroups = split(allPostedPosts, 25); 15 15 while (!isEmpty(postedGroups)) {
+82 -29
src/utils/db/data.ts
··· 1 1 import { and, asc, desc, eq, inArray, isNotNull, lte, ne, notInArray, sql } from "drizzle-orm"; 2 2 import { BatchItem } from "drizzle-orm/batch"; 3 - import { drizzle, DrizzleD1Database } from "drizzle-orm/d1"; 3 + import { DrizzleD1Database } from "drizzle-orm/d1"; 4 4 import isEmpty from "just-is-empty"; 5 5 import { validate as uuidValid } from 'uuid'; 6 6 import { posts, repostCounts, reposts } from "../../db/app.schema"; 7 7 import { violations } from "../../db/enforcement.schema"; 8 8 import { MAX_HOLD_DAYS_BEFORE_PURGE, MAX_POSTED_LENGTH } from "../../limits"; 9 9 import { 10 + AllContext, 10 11 BatchQuery, 11 - Bindings, 12 12 GetAllPostedBatch, 13 13 Post, 14 14 PostRecordResponse, ··· 16 16 } from "../../types.d"; 17 17 import { createPostObject, createRepostObject, floorCurrentTime } from "../helpers"; 18 18 19 - export const getAllPostsForCurrentTime = async (env: Bindings, removeThreads: boolean = false): Promise<Post[]> => { 19 + export const getAllPostsForCurrentTime = async (c: AllContext, removeThreads: boolean = false): Promise<Post[]> => { 20 20 // Get all scheduled posts for current time 21 - const db: DrizzleD1Database = drizzle(env.DB); 21 + const db: DrizzleD1Database = c.get("db"); 22 + if (!db) { 23 + console.error("Could not get all posts for current time, db was null"); 24 + return []; 25 + } 22 26 const currentTime: Date = floorCurrentTime(); 23 27 24 28 const violationUsers = db.select({violators: violations.userId}).from(violations); ··· 41 45 return results.map((item) => createPostObject(item)); 42 46 }; 43 47 44 - export const getAllRepostsForGivenTime = async (env: Bindings, givenDate: Date): Promise<Repost[]> => { 48 + export const getAllRepostsForGivenTime = async (c: AllContext, givenDate: Date): Promise<Repost[]> => { 45 49 // Get all scheduled posts for the given time 46 - const db: DrizzleD1Database = drizzle(env.DB); 50 + const db: DrizzleD1Database = c.get("db"); 51 + if (!db) { 52 + console.error("could not get all reposts for given timeframe, db was null"); 53 + return []; 54 + } 47 55 const query = db.select({uuid: reposts.uuid}).from(reposts) 48 56 .where(lte(reposts.scheduledDate, givenDate)); 49 57 const violationsQuery = db.select({data: violations.userId}).from(violations); ··· 55 63 return results.map((item) => createRepostObject(item)); 56 64 }; 57 65 58 - export const getAllRepostsForCurrentTime = async (env: Bindings): Promise<Repost[]> => { 59 - return await getAllRepostsForGivenTime(env, floorCurrentTime()); 66 + export const getAllRepostsForCurrentTime = async (c: AllContext): Promise<Repost[]> => { 67 + return await getAllRepostsForGivenTime(c, floorCurrentTime()); 60 68 }; 61 69 62 - export const deleteAllRepostsBeforeCurrentTime = async (env: Bindings) => { 63 - const db: DrizzleD1Database = drizzle(env.DB); 70 + export const deleteAllRepostsBeforeCurrentTime = async (c: AllContext) => { 71 + const db: DrizzleD1Database = c.get("db"); 72 + if (!db) { 73 + console.error("unable to delete all reposts before current time, db was null"); 74 + return; 75 + } 64 76 const currentTime = floorCurrentTime(); 65 77 const deletedPosts = await db.delete(reposts).where(lte(reposts.scheduledDate, currentTime)) 66 78 .returning({id: reposts.uuid, scheduleGuid: reposts.scheduleGuid}); ··· 107 119 } 108 120 }; 109 121 110 - export const bulkUpdatePostedData = async (env: Bindings, records: PostRecordResponse[], allPosted: boolean) => { 111 - const db: DrizzleD1Database = drizzle(env.DB); 122 + export const bulkUpdatePostedData = async (c: AllContext, records: PostRecordResponse[], allPosted: boolean) => { 123 + const db: DrizzleD1Database = c.get("db"); 124 + if (!db) { 125 + console.error("unable to bulk update posted data, db was null"); 126 + return; 127 + } 112 128 let dbOperations: BatchItem<"sqlite">[] = []; 113 129 114 130 for (let i = 0; i < records.length; ++i) { ··· 128 144 await db.batch(dbOperations as BatchQuery); 129 145 } 130 146 131 - export const setPostNowOffForPost = async (env: Bindings, id: string) => { 147 + export const setPostNowOffForPost = async (c: AllContext, id: string) => { 148 + const db: DrizzleD1Database = c.get("db"); 132 149 if (!uuidValid(id)) 133 150 return false; 134 151 135 - const db: DrizzleD1Database = drizzle(env.DB); 152 + if (!db) { 153 + console.warn(`cannot set off post now for post ${id}`); 154 + return false; 155 + } 156 + 136 157 const {success} = await db.update(posts).set({postNow: false}).where(eq(posts.uuid, id)); 137 158 if (!success) 138 159 console.error(`Unable to set PostNow to off for post ${id}`); 139 160 }; 140 161 141 - export const updatePostForGivenUser = async (env: Bindings, userId: string, id: string, newData: Object) => { 162 + export const updatePostForGivenUser = async (c: AllContext, userId: string, id: string, newData: Object) => { 163 + const db: DrizzleD1Database = c.get("db"); 142 164 if (isEmpty(userId) || !uuidValid(id)) 143 165 return false; 144 166 145 - const db: DrizzleD1Database = drizzle(env.DB); 167 + if (!db) { 168 + console.error(`unable to update post ${id} for user ${userId}, db was null`); 169 + return false; 170 + } 171 + 146 172 const {success} = await db.update(posts).set(newData).where( 147 173 and(eq(posts.uuid, id), eq(posts.userId, userId))); 148 174 return success; 149 175 }; 150 176 151 - export const getAllPostedPostsOfUser = async(env: Bindings, userId: string): Promise<GetAllPostedBatch[]> => { 177 + export const getAllPostedPostsOfUser = async(c: AllContext, userId: string): Promise<GetAllPostedBatch[]> => { 178 + const db: DrizzleD1Database = c.get("db"); 152 179 if (isEmpty(userId)) 153 180 return []; 154 181 155 - const db: DrizzleD1Database = drizzle(env.DB); 182 + if (!db) { 183 + console.error(`unable to get all posted posts of user ${userId}, db was null`); 184 + return []; 185 + } 186 + 156 187 return await db.select({id: posts.uuid, uri: posts.uri}) 157 188 .from(posts) 158 189 .where(and(eq(posts.userId, userId), eq(posts.posted, true))) 159 190 .all(); 160 191 }; 161 192 162 - export const getAllPostedPosts = async (env: Bindings): Promise<GetAllPostedBatch[]> => { 163 - const db: DrizzleD1Database = drizzle(env.DB); 193 + export const getAllPostedPosts = async (c: AllContext): Promise<GetAllPostedBatch[]> => { 194 + const db: DrizzleD1Database = c.get("db"); 195 + if (!db) { 196 + console.error("unable to get all posted posts, db was null"); 197 + return []; 198 + } 164 199 return await db.select({id: posts.uuid, uri: posts.uri}) 165 200 .from(posts) 166 201 .where(eq(posts.posted, true)) 167 202 .all(); 168 203 }; 169 204 170 - export const isPostAlreadyPosted = async (env: Bindings, postId: string): Promise<boolean> => { 205 + export const isPostAlreadyPosted = async (c: AllContext, postId: string): Promise<boolean> => { 206 + const db: DrizzleD1Database = c.get("db"); 171 207 if (!uuidValid(postId)) 172 208 return true; 173 209 174 - const db: DrizzleD1Database = drizzle(env.DB); 210 + if (!db) { 211 + console.error(`unable to get database to tell if ${postId} has been posted`); 212 + return true; 213 + } 214 + 175 215 const query = await db.select({posted: posts.posted}).from(posts).where(eq(posts.uuid, postId)).all(); 176 216 if (isEmpty(query) || query[0].posted === null) { 177 217 // if the post does not exist, return true anyways ··· 180 220 return query[0].posted; 181 221 }; 182 222 183 - export const getChildPostsOfThread = async (env: Bindings, rootId: string): Promise<Post[]|null> => { 223 + export const getChildPostsOfThread = async (c: AllContext, rootId: string): Promise<Post[]|null> => { 224 + const db: DrizzleD1Database = c.get("db"); 184 225 if (!uuidValid(rootId)) 185 226 return null; 186 227 187 - const db: DrizzleD1Database = drizzle(env.DB); 228 + if (!db) { 229 + console.error(`unable to get child posts of root ${rootId}, db was null`); 230 + return null; 231 + } 232 + 188 233 const query = await db.select().from(posts) 189 234 .where(and(isNotNull(posts.parentPost), eq(posts.rootPost, rootId))) 190 235 .orderBy(asc(posts.threadOrder), desc(posts.createdAt)).all(); ··· 204 249 } 205 250 206 251 // deletes multiple posted posts from a database. 207 - export const deletePosts = async (env: Bindings, postsToDelete: string[]): Promise<number> => { 252 + export const deletePosts = async (c: AllContext, postsToDelete: string[]): Promise<number> => { 208 253 // Don't do anything on empty arrays. 209 254 if (isEmpty(postsToDelete)) 210 255 return 0; 211 256 212 - const db: DrizzleD1Database = drizzle(env.DB); 257 + const db: DrizzleD1Database = c.get("db"); 258 + if (!db) { 259 + console.error(`could not delete posts ${postsToDelete}, db was null`); 260 + return 0; 261 + } 213 262 let deleteQueries: BatchItem<"sqlite">[] = []; 214 263 postsToDelete.forEach((itm) => { 215 264 deleteQueries.push(db.delete(posts).where(and(eq(posts.uuid, itm), eq(posts.posted, true)))); ··· 224 273 return 0; 225 274 }; 226 275 227 - export const purgePostedPosts = async (env: Bindings): Promise<number> => { 228 - const db: DrizzleD1Database = drizzle(env.DB); 276 + export const purgePostedPosts = async (c: AllContext): Promise<number> => { 277 + const db: DrizzleD1Database = c.get("db"); 278 + if (!db) { 279 + console.error("could not purge posted posts, got error"); 280 + return 0; 281 + } 229 282 const dateString = `datetime('now', '-${MAX_HOLD_DAYS_BEFORE_PURGE} days')`; 230 283 const dbQuery = await db.select({ data: posts.uuid }).from(posts) 231 284 .leftJoin(repostCounts, eq(posts.uuid, repostCounts.uuid)) ··· 242 295 if (isEmpty(postsToDelete)) 243 296 return 0; 244 297 245 - return await deletePosts(env, postsToDelete); 298 + return await deletePosts(c, postsToDelete); 246 299 }; 247 300 248 301 export const getPostByCID = async(db: DrizzleD1Database, userId: string, cid: string): Promise<Post|null> => {
+27 -11
src/utils/db/file.ts
··· 1 1 import { and, eq, inArray, lte } from "drizzle-orm"; 2 - import { drizzle, DrizzleD1Database } from "drizzle-orm/d1"; 2 + import { DrizzleD1Database } from "drizzle-orm/d1"; 3 3 import flatten from "just-flatten-it"; 4 4 import { mediaFiles, posts } from "../../db/app.schema"; 5 - import { Bindings, EmbedDataType, LooseObj } from "../../types.d"; 5 + import { AllContext, EmbedDataType, LooseObj } from "../../types.d"; 6 6 import { daysAgo } from "../helpers"; 7 7 8 - export const addFileListing = async (env: Bindings, file: string, user: string|null, createDate: Date|null=null) => { 9 - const db: DrizzleD1Database = drizzle(env.DB); 8 + export const addFileListing = async (c: AllContext, file: string, user: string|null, createDate: Date|null=null) => { 9 + const db: DrizzleD1Database = c.get("db"); 10 + if (!db) { 11 + console.error(`unable to create file listing for file ${file}, db was null`); 12 + return; 13 + } 10 14 let insertData:LooseObj = {}; 11 15 if (createDate !== null) { 12 16 insertData.createdAt = createDate; ··· 18 22 .onConflictDoNothing({target: mediaFiles.fileName}); 19 23 }; 20 24 21 - export const deleteFileListings = async (env: Bindings, files: string|string[]) => { 22 - const db: DrizzleD1Database = drizzle(env.DB); 25 + export const deleteFileListings = async (c: AllContext, files: string|string[]) => { 26 + const db: DrizzleD1Database = c.get("db"); 27 + if (!db) { 28 + console.error(`unable to delete file listings ${files}, db was null`); 29 + return; 30 + } 23 31 let filesToDelete = []; 24 32 filesToDelete.push(files); 25 33 const filesToWorkOn = flatten(filesToDelete); 26 34 await db.delete(mediaFiles).where(inArray(mediaFiles.fileName, filesToWorkOn)); 27 35 }; 28 36 29 - export const getAllAbandonedMedia = async(env: Bindings) => { 30 - const db: DrizzleD1Database = drizzle(env.DB); 31 - const numDaysAgo = daysAgo(env.R2_SETTINGS.prune_days); 37 + export const getAllAbandonedMedia = async(c: AllContext): Promise<string[]> => { 38 + const db: DrizzleD1Database = c.get("db"); 39 + if (!db) { 40 + console.error("could not get all abandoned media, db was null"); 41 + return []; 42 + } 43 + const numDaysAgo = daysAgo(c.env.R2_SETTINGS.prune_days); 32 44 33 45 const results = await db.select().from(mediaFiles) 34 46 .where( ··· 38 50 return results.map((item) => item.fileName); 39 51 }; 40 52 41 - export const getAllMediaOfUser = async (env: Bindings, userId: string): Promise<string[]> => { 42 - const db: DrizzleD1Database = drizzle(env.DB); 53 + export const getAllMediaOfUser = async (c: AllContext, userId: string): Promise<string[]> => { 54 + const db: DrizzleD1Database = c.get("db"); 55 + if (!db) { 56 + console.warn(`could not get all media of user ${userId}, db was null`); 57 + return []; 58 + } 43 59 const mediaList = await db.select({embeds: posts.embedContent}).from(posts) 44 60 .where(and(eq(posts.posted, false), eq(posts.userId, userId))).all(); 45 61
+11 -7
src/utils/db/maintain.ts
··· 1 1 import { eq, getTableColumns, gt, inArray, isNull, sql } from "drizzle-orm"; 2 2 import { BatchItem } from "drizzle-orm/batch"; 3 - import { drizzle, DrizzleD1Database } from "drizzle-orm/d1"; 3 + import { DrizzleD1Database } from "drizzle-orm/d1"; 4 4 import flatten from "just-flatten-it"; 5 5 import { mediaFiles, posts, repostCounts, reposts } from "../../db/app.schema"; 6 6 import { users } from "../../db/auth.schema"; 7 7 import { MAX_POSTED_LENGTH } from "../../limits"; 8 - import { BatchQuery, Bindings, R2BucketObject } from "../../types.d"; 8 + import { AllContext, BatchQuery, Bindings, R2BucketObject } from "../../types.d"; 9 9 import { getAllFilesList } from "../r2Query"; 10 10 import { addFileListing, getAllMediaOfUser } from "./file"; 11 11 12 12 /** Maintenance operations **/ 13 - export const runMaintenanceUpdates = async (env: Bindings) => { 14 - const db: DrizzleD1Database = drizzle(env.DB); 13 + export const runMaintenanceUpdates = async (c: AllContext) => { 14 + const db: DrizzleD1Database = c.get("db"); 15 + if (!db) { 16 + console.error("unable to get database to run maintenance"); 17 + return; 18 + } 15 19 // Create a posted query that also checks for valid json and content length 16 20 const postedQuery = db.select({ 17 21 ...getTableColumns(posts), ··· 44 48 await db.update(posts).set({updatedAt: sql`CURRENT_TIMESTAMP`}).where(isNull(posts.updatedAt)); 45 49 46 50 // populate existing media table with post data 47 - const allBucketFiles:R2BucketObject[] = await getAllFilesList(env); 51 + const allBucketFiles:R2BucketObject[] = await getAllFilesList(c); 48 52 try { 49 53 for (const bucketFile of allBucketFiles) { 50 - await addFileListing(env, bucketFile.name, bucketFile.user, bucketFile.date); 54 + await addFileListing(c, bucketFile.name, bucketFile.user, bucketFile.date); 51 55 } 52 56 } catch(err) { 53 57 console.error(`Adding file listings got error ${err}`); ··· 57 61 // Flag if the media file has embed data 58 62 const allUsers = await db.select({id: users.id}).from(users).all(); 59 63 for (const user of allUsers) { 60 - const userMedia = await getAllMediaOfUser(env, user.id); 64 + const userMedia = await getAllMediaOfUser(c, user.id); 61 65 batchedQueries.push(db.update(mediaFiles).set({hasPost: true}) 62 66 .where(inArray(mediaFiles.fileName, flatten(userMedia)))); 63 67 }
+33 -16
src/utils/db/userinfo.ts
··· 1 1 import { eq } from "drizzle-orm"; 2 - import { drizzle, DrizzleD1Database } from "drizzle-orm/d1"; 3 - import { Context } from "hono"; 2 + import { DrizzleD1Database } from "drizzle-orm/d1"; 4 3 import isEmpty from "just-is-empty"; 5 4 import { users } from "../../db/auth.schema"; 6 - import { Bindings, BskyAPILoginCreds } from "../../types.d"; 5 + import { AllContext, BskyAPILoginCreds } from "../../types.d"; 7 6 import { createLoginCredsObj } from "../helpers"; 8 7 9 - export const doesUserExist = async (c: Context, username: string) => { 10 - const db: DrizzleD1Database = drizzle(c.env.DB); 8 + export const doesUserExist = async (c: AllContext, username: string): Promise<boolean> => { 9 + const db: DrizzleD1Database = c.get("db"); 10 + if (!db) { 11 + console.error("Unable to check database for user existence"); 12 + return true; 13 + } 11 14 const result = await db.select().from(users) 12 15 .where(eq(users.username, username)) 13 16 .limit(1).all(); 14 17 return result.length > 0; 15 18 }; 16 19 17 - export const doesAdminExist = async (c: Context) => { 18 - const db: DrizzleD1Database = drizzle(c.env.DB); 20 + export const doesAdminExist = async (c: AllContext) => { 21 + const db: DrizzleD1Database = c.get("db"); 22 + if (!db) { 23 + console.error("unable to check database for admin account"); 24 + return false; 25 + } 26 + 19 27 const result = await db.select().from(users) 20 28 .where(eq(users.name, "admin")) 21 29 .limit(1).all(); 22 30 return result.length > 0; 23 31 }; 24 32 25 - export const getBskyUserPassForId = async (env: Bindings, userid: string): Promise<BskyAPILoginCreds> => { 26 - const db: DrizzleD1Database = drizzle(env.DB); 33 + export const getBskyUserPassForId = async (c: AllContext, userid: string): Promise<BskyAPILoginCreds> => { 34 + const db: DrizzleD1Database = c.get("db"); 35 + if (!db) 36 + return createLoginCredsObj(null); 37 + 27 38 const response = await db.select({user: users.username, pass: users.bskyAppPass, pds: users.pds}) 28 39 .from(users) 29 40 .where(eq(users.id, userid)) 30 41 .limit(1).all(); 31 - return createLoginCredsObj(env, response[0] || null); 42 + return createLoginCredsObj(response[0] || null); 32 43 }; 33 44 34 - export const getUsernameForUserId = async (env: Bindings, userId: string): Promise<string|null> => { 35 - const db: DrizzleD1Database = drizzle(env.DB); 45 + export const getUsernameForUserId = async (c: AllContext, userId: string): Promise<string|null> => { 46 + const db: DrizzleD1Database = c.get("db"); 47 + if (!db) 48 + return null; 49 + 36 50 const result = await db.select({username: users.username}).from(users) 37 51 .where(eq(users.id, userId)).limit(1); 38 52 if (result !== null && result.length > 0) ··· 40 54 return null; 41 55 }; 42 56 43 - export const getUsernameForUser = async (c: Context): Promise<string|null> => { 57 + export const getUsernameForUser = async (c: AllContext): Promise<string|null> => { 44 58 const userId = c.get("userId"); 45 59 if (!userId) 46 60 return null; 47 61 48 - return await getUsernameForUserId(c.env, userId); 62 + return await getUsernameForUserId(c, userId); 49 63 }; 50 64 51 65 // This is a super dumb query that's needed to get around better auth's forgot password system 52 66 // because you cannot make the call with just an username, you need to also have the email 53 67 // but we never update the email past the original time you first signed up, so instead 54 68 // we use big brain tactics to spoof the email 55 - export const getUserEmailForHandle = async (env: Bindings, userhandle: string): Promise<string|null> => { 56 - const db: DrizzleD1Database = drizzle(env.DB); 69 + export const getUserEmailForHandle = async (c: AllContext, userhandle: string): Promise<string|null> => { 70 + const db: DrizzleD1Database = c.get("db"); 71 + if (!db) 72 + return null; 73 + 57 74 const result = await db.select({email: users.email}).from(users).where(eq(users.username, userhandle)).limit(1); 58 75 if (!isEmpty(result)) 59 76 return result[0].email;
+33 -20
src/utils/db/violations.ts
··· 1 1 import { and, eq, ne } from "drizzle-orm"; 2 - import { drizzle, DrizzleD1Database } from "drizzle-orm/d1"; 3 - import { Context } from "hono"; 2 + import { DrizzleD1Database } from "drizzle-orm/d1"; 4 3 import { bannedUsers, violations } from "../../db/enforcement.schema"; 5 - import { Bindings, LooseObj, AccountStatus, Violation } from "../../types.d"; 4 + import { AccountStatus, AllContext, LooseObj, Violation } from "../../types.d"; 6 5 import { lookupBskyHandle } from "../bskyApi"; 7 6 import { getUsernameForUserId } from "./userinfo"; 8 7 ··· 18 17 } 19 18 } 20 19 21 - export const userHasBan = async (env: Bindings, userDid: string): Promise<boolean> => { 22 - const db: DrizzleD1Database = drizzle(env.DB); 23 - return (await db.select().from(bannedUsers).where(eq(bannedUsers.did, userDid)).limit(1).all()).length > 0; 20 + export const userHasBan = async (c: AllContext, userDid: string): Promise<boolean> => { 21 + const db: DrizzleD1Database = c.get("db"); 22 + if (!db) { 23 + console.error("unable to check if user has ban, db was null"); 24 + return false; 25 + } 26 + const usersBanned = await db.$count(bannedUsers, eq(bannedUsers.did, userDid)); 27 + return (usersBanned > 0); 24 28 }; 25 29 26 - export const userHandleHasBan = async (env: Bindings, userName: string) => { 30 + export const userHandleHasBan = async (c: AllContext, userName: string) => { 27 31 if (userName !== null) { 28 32 const didHandle = await lookupBskyHandle(userName); 29 33 if (didHandle !== null) 30 - return await userHasBan(env, didHandle); 34 + return await userHasBan(c, didHandle); 31 35 } 32 36 return false; 33 37 }; ··· 56 60 return valuesUpdate; 57 61 } 58 62 59 - export const createViolationForUser = async(env: Bindings, userId: string, violationType: AccountStatus): Promise<boolean> => { 63 + export const createViolationForUser = async(c: AllContext, userId: string, violationType: AccountStatus): Promise<boolean> => { 60 64 const NoHandleState: AccountStatus[] = [AccountStatus.Ok, AccountStatus.PlatformOutage, 61 65 AccountStatus.None, AccountStatus.UnhandledError]; 62 66 // Don't do anything in these cases ··· 65 69 return false; 66 70 } 67 71 68 - const db: DrizzleD1Database = drizzle(env.DB); 72 + const db: DrizzleD1Database = c.get("db"); 73 + if (!db) { 74 + console.error("unable to get database to create violations for"); 75 + return false; 76 + } 69 77 const valuesUpdate:LooseObj = createObjForValuesChange([violationType], true); 70 78 if (violationType === AccountStatus.TOSViolation) { 71 - const bskyUsername = await getUsernameForUserId(env, userId); 79 + const bskyUsername = await getUsernameForUserId(c, userId); 72 80 if (bskyUsername !== null) { 73 81 await createBanForUser(db, bskyUsername, "tos violation"); 74 82 } else { ··· 87 95 )); 88 96 }; 89 97 90 - export const removeViolation = async(env: Bindings, userId: string, violationType: AccountStatus) => { 91 - await removeViolations(env, userId, [violationType]); 98 + export const removeViolation = async(c: AllContext, userId: string, violationType: AccountStatus) => { 99 + await removeViolations(c, userId, [violationType]); 92 100 }; 93 101 94 - export const removeViolations = async(env: Bindings, userId: string, violationType: AccountStatus[]) => { 95 - const db: DrizzleD1Database = drizzle(env.DB); 102 + export const removeViolations = async(c: AllContext, userId: string, violationType: AccountStatus[]) => { 103 + const db: DrizzleD1Database = c.get("db"); 104 + if (!db) { 105 + console.warn(`unable to remove violations for user ${userId}, db was null`); 106 + return; 107 + } 96 108 // Check if they have a violation first 97 109 if ((await userHasViolations(db, userId)) == false) { 98 110 return; ··· 113 125 } 114 126 115 127 export const getViolationsForUser = async(db: DrizzleD1Database, userId: string) => { 116 - const {results} = await db.select().from(violations).where(eq(violations.userId, userId)).limit(1).run(); 128 + const {results} = await db.select().from(violations) 129 + .where(eq(violations.userId, userId)).limit(1).run(); 117 130 if (results.length > 0) 118 131 return (results[0] as Violation); 119 132 return null; 120 133 }; 121 134 122 - export const getViolationsForCurrentUser = async(c: Context): Promise<Violation|null> => { 135 + export const getViolationsForCurrentUser = async(c: AllContext): Promise<Violation|null> => { 123 136 const userId = c.get("userId"); 124 - if (userId) { 125 - const db: DrizzleD1Database = drizzle(c.env.DB); 137 + const db: DrizzleD1Database = c.get("db"); 138 + if (userId && db) { 126 139 return await getViolationsForUser(db, userId); 127 140 } 128 141 return null; 129 - }; 142 + };
+51 -25
src/utils/dbQuery.ts
··· 1 1 import { addHours, isAfter, isEqual } from "date-fns"; 2 2 import { and, asc, desc, eq, getTableColumns, gt, gte, sql } from "drizzle-orm"; 3 3 import { BatchItem } from "drizzle-orm/batch"; 4 - import { drizzle, DrizzleD1Database } from "drizzle-orm/d1"; 5 - import { Context } from "hono"; 4 + import { DrizzleD1Database } from "drizzle-orm/d1"; 6 5 import has from "just-has"; 7 6 import isEmpty from "just-is-empty"; 8 7 import { v4 as uuidv4, validate as uuidValid } from 'uuid'; ··· 11 10 import { MAX_POSTS_PER_THREAD, MAX_REPOST_POSTS, MAX_REPOST_RULES_PER_POST } from "../limits"; 12 11 import { 13 12 AccountStatus, 13 + AllContext, 14 14 BatchQuery, 15 15 CreateObjectResponse, CreatePostQueryResponse, 16 16 DeleteResponse, ··· 25 25 import { createPostObject, createRepostInfo, floorGivenTime } from "./helpers"; 26 26 import { deleteEmbedsFromR2 } from "./r2Query"; 27 27 28 - export const getPostsForUser = async (c: Context): Promise<Post[]|null> => { 28 + export const getPostsForUser = async (c: AllContext): Promise<Post[]|null> => { 29 29 try { 30 30 const userId = c.get("userId"); 31 - if (userId) { 32 - const db: DrizzleD1Database = drizzle(c.env.DB); 31 + const db: DrizzleD1Database = c.get("db"); 32 + if (userId && db) { 33 33 const results = await db.select({ 34 34 ...getTableColumns(posts), 35 35 repostCount: repostCounts.count ··· 49 49 return null; 50 50 }; 51 51 52 - export const updateUserData = async (c: Context, newData: any): Promise<boolean> => { 52 + export const updateUserData = async (c: AllContext, newData: any): Promise<boolean> => { 53 53 const userId = c.get("userId"); 54 + const db: DrizzleD1Database = c.get("db"); 54 55 try { 56 + if (!db) { 57 + console.error("Unable to update user data, no database object"); 58 + return false; 59 + } 55 60 if (userId) { 56 - const db: DrizzleD1Database = drizzle(c.env.DB); 57 61 let queriesToExecute:BatchItem<"sqlite">[] = []; 58 62 59 63 if (has(newData, "password")) { ··· 73 77 // check if the user has violations 74 78 if (await userHasViolations(db, userId)) { 75 79 // they do, so clear them out 76 - await removeViolations(c.env, userId, [AccountStatus.InvalidAccount, AccountStatus.Deactivated]); 80 + await removeViolations(c, userId, [AccountStatus.InvalidAccount, AccountStatus.Deactivated]); 77 81 } 78 82 } 79 83 ··· 92 96 return false; 93 97 }; 94 98 95 - export const deletePost = async (c: Context, id: string): Promise<DeleteResponse> => { 99 + export const deletePost = async (c: AllContext, id: string): Promise<DeleteResponse> => { 96 100 const userId = c.get("userId"); 97 101 const returnObj: DeleteResponse = {success: false}; 98 102 if (!userId) { 99 103 return returnObj; 100 104 } 101 105 102 - const db: DrizzleD1Database = drizzle(c.env.DB); 106 + const db: DrizzleD1Database = c.get("db"); 107 + if (!db) { 108 + console.error(`unable to delete post ${id}, db was null`); 109 + return returnObj; 110 + } 111 + 103 112 const postObj = await getPostById(c, id); 104 113 if (postObj !== null) { 105 114 let queriesToExecute:BatchItem<"sqlite">[] = []; ··· 109 118 await deleteEmbedsFromR2(c, postObj.embeds); 110 119 if (await userHasViolations(db, userId)) { 111 120 // Remove the media too big violation if it's been given 112 - await removeViolation(c.env, userId, AccountStatus.MediaTooBig); 121 + await removeViolation(c, userId, AccountStatus.MediaTooBig); 113 122 } 114 123 } 115 124 ··· 129 138 130 139 // We'll need to delete all of the child embeds then, a costly, annoying experience. 131 140 if (postObj.isThreadRoot) { 132 - const childPosts = await getChildPostsOfThread(c.env, postObj.postid); 141 + const childPosts = await getChildPostsOfThread(c, postObj.postid); 133 142 if (childPosts !== null) { 134 143 for (const childPost of childPosts) { 135 144 c.executionCtx.waitUntil(deleteEmbedsFromR2(c, childPost.embeds)); ··· 155 164 return returnObj; 156 165 }; 157 166 158 - export const createPost = async (c: Context, body: any): Promise<CreatePostQueryResponse> => { 159 - const db: DrizzleD1Database = drizzle(c.env.DB); 160 - 167 + export const createPost = async (c: AllContext, body: any): Promise<CreatePostQueryResponse> => { 168 + const db: DrizzleD1Database = c.get("db"); 161 169 const userId = c.get("userId"); 162 170 if (!userId) 163 171 return { ok: false, msg: "Your user session has expired, please login again"}; 164 172 173 + if (!db) { 174 + console.error("unable to create post, db became null"); 175 + return { ok: false, msg: "An application error has occurred please refresh" }; 176 + } 177 + 165 178 const validation = PostSchema.safeParse(body); 166 179 if (!validation.success) { 167 180 return { ok: false, msg: validation.error.toString() }; ··· 313 326 return { ok: success, postNow: makePostNow, postId: postUUID, msg: success ? "success" : "fail" }; 314 327 }; 315 328 316 - export const createRepost = async (c: Context, body: any): Promise<CreateObjectResponse> => { 317 - const db: DrizzleD1Database = drizzle(c.env.DB); 329 + export const createRepost = async (c: AllContext, body: any): Promise<CreateObjectResponse> => { 330 + const db: DrizzleD1Database = c.get("db"); 318 331 319 332 const userId = c.get("userId"); 320 333 if (!userId) 321 334 return { ok: false, msg: "Your user session has expired, please login again"}; 335 + 336 + if (!db) { 337 + console.error("unable to create repost db became null"); 338 + return {ok: false, msg: "Invalid server operation occurred, please refresh"}; 339 + } 322 340 323 341 const validation = RepostSchema.safeParse(body); 324 342 if (!validation.success) { ··· 430 448 return { ok: success, msg: success ? "success" : "fail", postId: postUUID }; 431 449 }; 432 450 433 - export const updatePostForUser = async (c: Context, id: string, newData: Object): Promise<boolean> => { 451 + export const updatePostForUser = async (c: AllContext, id: string, newData: Object): Promise<boolean> => { 434 452 const userId = c.get("userId"); 435 - return await updatePostForGivenUser(c.env, userId, id, newData); 453 + return await updatePostForGivenUser(c, userId, id, newData); 436 454 }; 437 455 438 - export const getPostById = async(c: Context, id: string): Promise<Post|null> => { 456 + export const getPostById = async(c: AllContext, id: string): Promise<Post|null> => { 439 457 const userId = c.get("userId"); 440 458 if (!userId || !uuidValid(id)) 441 459 return null; 442 460 443 - const env = c.env; 444 - const db: DrizzleD1Database = drizzle(env.DB); 461 + const db: DrizzleD1Database = c.get("db"); 462 + if (!db) { 463 + console.error(`unable to get post ${id}, db was null`); 464 + return null; 465 + } 466 + 445 467 const result = await db.select().from(posts) 446 468 .where(and(eq(posts.uuid, id), eq(posts.userId, userId))) 447 469 .limit(1).all(); ··· 452 474 }; 453 475 454 476 // used for post editing, acts very similar to getPostsForUser 455 - export const getPostByIdWithReposts = async(c: Context, id: string): Promise<Post|null> => { 477 + export const getPostByIdWithReposts = async(c: AllContext, id: string): Promise<Post|null> => { 456 478 const userId = c.get("userId"); 457 479 if (!userId || !uuidValid(id)) 458 480 return null; 459 481 460 - const env = c.env; 461 - const db: DrizzleD1Database = drizzle(env.DB); 482 + const db: DrizzleD1Database = c.get("db"); 483 + if (!db) { 484 + console.error(`unable to get post ${id} with reposts, db was null`); 485 + return null; 486 + } 487 + 462 488 const result = await db.select({ 463 489 ...getTableColumns(posts), 464 490 repostCount: repostCounts.count,
+3 -3
src/utils/helpers.ts
··· 1 1 import { startOfHour, subDays } from "date-fns"; 2 + import { Context } from "hono"; 2 3 import has from "just-has"; 3 4 import isEmpty from "just-is-empty"; 4 - import { Bindings, BskyAPILoginCreds, Post, Repost, RepostInfo } from "../types.d"; 5 - import { Context } from "hono"; 5 + import { BskyAPILoginCreds, Post, Repost, RepostInfo } from "../types.d"; 6 6 7 7 export function createPostObject(data: any) { 8 8 const postData: Post = (new Object() as Post); ··· 82 82 return repostObj; 83 83 } 84 84 85 - export function createLoginCredsObj(env: Bindings, data: any) { 85 + export function createLoginCredsObj(data: any) { 86 86 const loginCreds: BskyAPILoginCreds = (new Object() as BskyAPILoginCreds); 87 87 if (isEmpty(data)) { 88 88 loginCreds.password = loginCreds.username = loginCreds.pds = "";
+5 -5
src/utils/inviteKeys.ts
··· 15 15 if (inviteKey === undefined) 16 16 return false; 17 17 18 - const value = await c.env.INVITE_POOL.get(inviteKey); 18 + const value = await c.env.INVITE_POOL!.get(inviteKey); 19 19 // Key does not exist 20 20 if (value === null) 21 21 return false; ··· 41 41 if (inviteKey === undefined) 42 42 return; 43 43 44 - const value = await c.env.INVITE_POOL.get(inviteKey); 44 + const value = await c.env.INVITE_POOL!.get(inviteKey); 45 45 if (value === null) { 46 46 console.error(`attempted to use invite key ${inviteKey} but is invalid`); 47 47 return; ··· 62 62 let newValue: number = amount - 1; 63 63 // Delete any keys that fall to 0, they should be removed from the db 64 64 if (newValue <= 0) { 65 - await c.env.INVITE_POOL.delete(inviteKey); 65 + await c.env.INVITE_POOL!.delete(inviteKey); 66 66 return; 67 67 } 68 68 69 69 // put the new value on the stack 70 - await c.env.INVITE_POOL.put(inviteKey, newValue.toString()); 70 + await c.env.INVITE_POOL!.put(inviteKey, newValue.toString()); 71 71 } 72 72 } 73 73 ··· 80 80 separator: '-', 81 81 capitalize: false, 82 82 }); 83 - c.executionCtx.waitUntil(c.env.INVITE_POOL.put(newKey, "10")); 83 + c.executionCtx.waitUntil(c.env.INVITE_POOL!.put(newKey, "10")); 84 84 return newKey; 85 85 }
+21 -22
src/utils/r2Query.ts
··· 15 15 R2_FILE_SIZE_LIMIT, 16 16 R2_FILE_SIZE_LIMIT_IN_MB 17 17 } from "../limits"; 18 - import { Bindings, EmbedData, EmbedDataType, R2BucketObject, ScheduledContext } from '../types.d'; 18 + import { AllContext, EmbedData, EmbedDataType, R2BucketObject } from '../types.d'; 19 19 import { addFileListing, deleteFileListings } from './db/file'; 20 20 21 21 type FileMetaData = { ··· 26 26 qualityLevel?: number; 27 27 }; 28 28 29 - export const deleteEmbedsFromR2 = async (c: Context|ScheduledContext, embeds: EmbedData[]|undefined, isQueued: boolean=false) => { 29 + export const deleteEmbedsFromR2 = async (c: AllContext, embeds: EmbedData[]|undefined, isQueued: boolean=false) => { 30 30 let itemsToDelete:string[] = []; 31 31 32 32 if (embeds !== undefined && embeds.length > 0) { ··· 42 42 return itemsToDelete; 43 43 }; 44 44 45 - export const deleteFromR2 = async (c: Context|ScheduledContext, embeds: string[]|string, isQueued: boolean=false) => { 45 + export const deleteFromR2 = async (c: AllContext, embeds: string[]|string, isQueued: boolean=false) => { 46 46 if (embeds.length <= 0) 47 47 return; 48 48 49 49 console.log(`Deleting ${embeds}`); 50 50 const killFilesPromise = c.env.R2.delete(embeds); 51 - const deleteFileListingPromise = deleteFileListings(c.env, embeds); 51 + const deleteFileListingPromise = deleteFileListings(c, embeds); 52 52 if (isQueued) { 53 53 await killFilesPromise; 54 54 await deleteFileListingPromise; ··· 58 58 } 59 59 }; 60 60 61 - const rawUploadToR2 = async (env: Bindings, buffer: ArrayBuffer|ReadableStream, metaData: FileMetaData) => { 61 + const rawUploadToR2 = async (c: AllContext, buffer: ArrayBuffer|ReadableStream, metaData: FileMetaData) => { 62 62 const fileExt:string|undefined = metaData.name.split(".").pop(); 63 63 if (fileExt === undefined) { 64 64 return {"success": false, "error": "unable to upload, file name is invalid"}; 65 65 } 66 66 67 67 const fileName = `${uuidv4()}.${fileExt.toLowerCase()}`; 68 - const R2UploadRes = await env.R2.put(fileName, buffer, { 68 + const R2UploadRes = await c.env.R2.put(fileName, buffer, { 69 69 customMetadata: {"user": metaData.user, "type": metaData.type } 70 70 }); 71 71 if (R2UploadRes) { 72 - await addFileListing(env, fileName, metaData.user); 72 + await addFileListing(c, fileName, metaData.user); 73 73 return {"success": true, "data": R2UploadRes.key, 74 74 "originalName": metaData.name, "fileSize": metaData.size, 75 75 "qualityLevel": metaData.qualityLevel}; ··· 80 80 81 81 const uploadImageToR2 = async(c: Context, file: File, userId: string) => { 82 82 const originalName = file.name; 83 - const env: Bindings = c.env; 84 83 // The maximum size of CF Image transforms. 85 84 if (file.size > CF_IMAGES_FILE_SIZE_LIMIT) { 86 85 return {"success": false, "error": `An image has a maximum file size of ${CF_IMAGES_FILE_SIZE_LIMIT_IN_MB}MB`}; ··· 108 107 if (file.size > BSKY_IMG_SIZE_LIMIT) { 109 108 let failedToResize = true; 110 109 111 - if (env.IMAGE_SETTINGS.enabled) { 110 + if (c.env.IMAGE_SETTINGS.enabled) { 112 111 const resizeFilename = uuidv4(); 113 - const resizeBucketPush = await env.R2RESIZE.put(resizeFilename, await file.bytes(), { 112 + const resizeBucketPush = await c.env.R2RESIZE.put(resizeFilename, await file.bytes(), { 114 113 customMetadata: {"user": userId }, 115 114 httpMetadata: { contentType: file.type } 116 115 }); ··· 121 120 } 122 121 123 122 // TODO: use the image wrangler binding 124 - for (var i = 0; i < env.IMAGE_SETTINGS.steps.length; ++i) { 125 - const qualityLevel = env.IMAGE_SETTINGS.steps[i]; 126 - const response = await fetch(new URL(resizeFilename, env.IMAGE_SETTINGS.bucket_url), { 123 + for (var i = 0; i < c.env.IMAGE_SETTINGS.steps.length; ++i) { 124 + const qualityLevel = c.env.IMAGE_SETTINGS.steps[i]; 125 + const response = await fetch(new URL(resizeFilename, c.env.IMAGE_SETTINGS.bucket_url!), { 127 126 headers: { 128 - "x-skyscheduler-helper": env.RESIZE_SECRET_HEADER 127 + "x-skyscheduler-helper": c.env.RESIZE_SECRET_HEADER 129 128 }, 130 129 cf: { 131 130 image: { ··· 170 169 } 171 170 } 172 171 // Delete the file from the resize bucket. 173 - c.executionCtx.waitUntil(env.R2RESIZE.delete(resizeFilename)); 172 + c.executionCtx.waitUntil(c.env.R2RESIZE.delete(resizeFilename)); 174 173 } 175 174 176 175 if (failedToResize) { ··· 189 188 190 189 if (fileToProcess === null) 191 190 fileToProcess = await file.arrayBuffer(); 192 - return await rawUploadToR2(env, fileToProcess, fileMetaData); 191 + return await rawUploadToR2(c, fileToProcess, fileMetaData); 193 192 }; 194 193 195 - const uploadVideoToR2 = async (env: Bindings, file: File, userId: string) => { 194 + const uploadVideoToR2 = async (c: Context, file: File, userId: string) => { 196 195 // Technically this will never hit because it is greater than our own internal limits 197 196 if (file.size > BSKY_VIDEO_SIZE_LIMIT) { 198 197 return {"success": false, "error": `max video size is ${BSKY_VIDEO_SIZE_LIMIT}MB`}; ··· 204 203 type: file.type, 205 204 user: userId 206 205 }; 207 - return await rawUploadToR2(env, await file.stream(), fileMetaData); 206 + return await rawUploadToR2(c, await file.stream(), fileMetaData); 208 207 }; 209 208 210 209 export const uploadFileR2 = async (c: Context, file: File|string, userId: string) => { ··· 227 226 if (BSKY_IMG_MIME_TYPES.includes(fileType)) { 228 227 return await uploadImageToR2(c, file, userId); 229 228 } else if (BSKY_VIDEO_MIME_TYPES.includes(fileType)) { 230 - return await uploadVideoToR2(c.env, file, userId); 229 + return await uploadVideoToR2(c, file, userId); 231 230 } else if (GIF_UPLOAD_ALLOWED && BSKY_GIF_MIME_TYPES.includes(fileType)) { 232 231 // TODO: modify this in the future to transform the image to a webm 233 232 // then push to uploadVideo 234 - return await uploadVideoToR2(c.env, file, userId); 233 + return await uploadVideoToR2(c, file, userId); 235 234 } 236 235 return {"success": false, "error": "unable to push to R2"}; 237 236 }; 238 237 239 - export const getAllFilesList = async (env: Bindings) => { 238 + export const getAllFilesList = async (c: AllContext) => { 240 239 let options: R2ListOptions = { 241 240 limit: 1000, 242 241 include: ["customMetadata"] ··· 244 243 let values:R2BucketObject[] = []; 245 244 246 245 while (true) { 247 - const response = await env.R2.list(options); 246 + const response = await c.env.R2.list(options); 248 247 for (const file of response.objects) { 249 248 values.push({ 250 249 name: file.key,
+27 -37
src/utils/scheduler.ts
··· 1 1 import AtpAgent from '@atproto/api'; 2 2 import isEmpty from 'just-is-empty'; 3 - import { Bindings, Post, Repost, ScheduledContext } from '../types.d'; 3 + import { AllContext, Post, Repost } from '../types.d'; 4 4 import { makeAgentForUser, makePost, makeRepost } from './bskyApi'; 5 5 import { pruneBskyPosts } from './bskyPrune'; 6 6 import { ··· 11 11 import { enqueuePost, enqueueRepost, isQueueEnabled, isRepostQueueEnabled, shouldPostThreadQueue } from './queuePublisher'; 12 12 import { deleteFromR2 } from './r2Query'; 13 13 14 - export const handlePostTask = async(runtime: ScheduledContext, postData: Post, agent: AtpAgent|null) => { 14 + export const handlePostTask = async(runtime: AllContext, postData: Post, agent: AtpAgent|null) => { 15 15 const madePost = await makePost(runtime, postData, agent); 16 16 if (madePost) { 17 17 console.log(`Made post ${postData.postid} successfully`); ··· 20 20 } 21 21 return madePost; 22 22 } 23 - export const handleRepostTask = async(runtime: ScheduledContext, postData: Repost, agent: AtpAgent|null) => { 24 - const madeRepost = await makeRepost(runtime, postData, agent); 23 + export const handleRepostTask = async(c: AllContext, postData: Repost, agent: AtpAgent|null) => { 24 + const madeRepost = await makeRepost(c, postData, agent); 25 25 if (madeRepost) { 26 26 console.log(`Reposted ${postData.uri} successfully!`); 27 27 } else { ··· 30 30 return madeRepost; 31 31 }; 32 32 33 - export const schedulePostTask = async (env: Bindings, ctx: ExecutionContext) => { 34 - const scheduledPosts: Post[] = await getAllPostsForCurrentTime(env); 35 - const scheduledReposts: Repost[] = await getAllRepostsForCurrentTime(env); 36 - const queueEnabled: boolean = isQueueEnabled(env); 37 - const repostQueueEnabled: boolean = isRepostQueueEnabled(env); 38 - const threadQueueEnabled: boolean = shouldPostThreadQueue(env); 39 - 40 - const runtimeWrapper: ScheduledContext = { 41 - executionCtx: ctx, 42 - env: env 43 - }; 44 - 33 + export const schedulePostTask = async (c: AllContext) => { 34 + const scheduledPosts: Post[] = await getAllPostsForCurrentTime(c); 35 + const scheduledReposts: Repost[] = await getAllRepostsForCurrentTime(c); 36 + const queueEnabled: boolean = isQueueEnabled(c.env); 37 + const repostQueueEnabled: boolean = isRepostQueueEnabled(c.env); 38 + const threadQueueEnabled: boolean = shouldPostThreadQueue(c.env); 45 39 // Temporary cache of agents to make handling actions much better and easier. 46 40 // The only potential downside is if we run hot on RAM with a lot of users. Before, the agents would 47 41 // get freed up as a part of exiting their cycle, but this would make that worse... ··· 49 43 // TODO: bunching as a part of queues, literally just throw an agent at a queue with instructions and go. 50 44 // this requires queueing to be working properly. 51 45 const AgentList = new Map(); 52 - const usesAgentMap: boolean = (env.SITE_SETTINGS.use_agent_map) || false; 46 + const usesAgentMap: boolean = (c.env.SITE_SETTINGS.use_agent_map) || false; 53 47 54 48 // Push any posts 55 49 if (!isEmpty(scheduledPosts)) { 56 50 console.log(`handling ${scheduledPosts.length} posts...`); 57 51 for (const post of scheduledPosts) { 58 52 if (queueEnabled || (post.isThreadRoot && threadQueueEnabled)) { 59 - await enqueuePost(env, post); 53 + await enqueuePost(c, post); 60 54 } else { 61 55 let agent = (usesAgentMap) ? AgentList.get(post.user) || null : null; 62 56 if (agent === null) { 63 - agent = await makeAgentForUser(env, post.user); 57 + agent = await makeAgentForUser(c, post.user); 64 58 if (usesAgentMap) 65 59 AgentList.set(post.user, agent); 66 60 } 67 - ctx.waitUntil(handlePostTask(runtimeWrapper, post, agent)); 61 + c.ctx.waitUntil(handlePostTask(c, post, agent)); 68 62 } 69 63 } 70 64 } else { ··· 78 72 if (!repostQueueEnabled) { 79 73 let agent = (usesAgentMap) ? AgentList.get(repost.userId) || null : null; 80 74 if (agent === null) { 81 - agent = await makeAgentForUser(env, repost.userId); 75 + agent = await makeAgentForUser(c, repost.userId); 82 76 if (usesAgentMap) 83 77 AgentList.set(repost.userId, agent); 84 78 } 85 - ctx.waitUntil(handleRepostTask(runtimeWrapper, repost, agent)); 79 + c.ctx.waitUntil(handleRepostTask(c, repost, agent)); 86 80 } else { 87 - await enqueueRepost(env, repost); 81 + await enqueueRepost(c, repost); 88 82 } 89 83 }; 90 - ctx.waitUntil(deleteAllRepostsBeforeCurrentTime(env)); 84 + c.ctx.waitUntil(deleteAllRepostsBeforeCurrentTime(c)); 91 85 } else { 92 86 console.log("no reposts scheduled for this time"); 93 87 } 94 88 }; 95 89 96 - export const cleanUpPostsTask = async(env: Bindings, ctx: ExecutionContext) => { 97 - const purgedPosts: number = await purgePostedPosts(env); 90 + export const cleanUpPostsTask = async(c: AllContext) => { 91 + const purgedPosts: number = await purgePostedPosts(c); 98 92 console.log(`Purged ${purgedPosts} old posts from the database`); 99 93 100 - const removedIds: string[] = await pruneBskyPosts(env); 94 + const removedIds: string[] = await pruneBskyPosts(c); 101 95 if (!isEmpty(removedIds)) { 102 - const deletedItems: number = await deletePosts(env, removedIds); 96 + const deletedItems: number = await deletePosts(c, removedIds); 103 97 console.log(`Deleted ${deletedItems} missing posts from the db`); 104 98 } 105 - if (env.R2_SETTINGS.auto_prune === true) 106 - await cleanupAbandonedFiles(env, ctx); 99 + if (c.env.R2_SETTINGS.auto_prune === true) 100 + await cleanupAbandonedFiles(c); 107 101 }; 108 102 109 - export const cleanupAbandonedFiles = async(env: Bindings, ctx: ExecutionContext) => { 110 - const abandonedFiles: string[] = await getAllAbandonedMedia(env); 111 - const runtimeWrapper: ScheduledContext = { 112 - executionCtx: ctx, 113 - env: env 114 - }; 103 + export const cleanupAbandonedFiles = async(c: AllContext) => { 104 + const abandonedFiles: string[] = await getAllAbandonedMedia(c); 115 105 if (!isEmpty(abandonedFiles)) { 116 - await deleteFromR2(runtimeWrapper, abandonedFiles); 106 + await deleteFromR2(c, abandonedFiles); 117 107 } 118 108 };
+32 -8
src/wrangler.d.ts
··· 1 1 /* eslint-disable */ 2 - // Generated by Wrangler by running `wrangler types src/wrangler.d.ts` (hash: 3f068bddc4c3f62a89ba111fafb7b28e) 2 + // Generated by Wrangler by running `wrangler types src/wrangler.d.ts` (hash: d6fc3c7066eb7e2adc3a6e0ae96bae05) 3 3 // Runtime types generated with workerd@1.20260212.0 2024-12-13 nodejs_compat 4 4 declare namespace Cloudflare { 5 5 interface GlobalProps { 6 6 mainModule: typeof import("./index"); 7 7 } 8 - interface Env { 8 + interface StagingEnv { 9 9 KV: KVNamespace; 10 - INVITE_POOL: KVNamespace; 11 10 R2: R2Bucket; 12 - R2RESIZE: R2Bucket; 13 11 DB: D1Database; 14 - POST_QUEUE1: Queue; 15 12 IMAGES: ImagesBinding; 16 - IMAGE_SETTINGS: {"enabled":true,"steps":[95,85,75],"bucket_url":"https://resize.skyscheduler.work/"}; 17 - SIGNUP_SETTINGS: {"use_captcha":true,"invite_only":false,"invite_thread":"https://bsky.app/profile/skyscheduler.work/post/3ltsfnzdmkk2l"}; 13 + IMAGE_SETTINGS: {"enabled":false}; 14 + SIGNUP_SETTINGS: {"use_captcha":false,"invite_only":false,"invite_thread":""}; 18 15 QUEUE_SETTINGS: {"enabled":false,"repostsEnabled":false,"postNowEnabled":false,"threadEnabled":true,"delay_val":100,"post_queues":["POST_QUEUE1"],"repost_queues":[]}; 19 16 REDIRECTS: {"contact":"https://bsky.app/profile/skyscheduler.work","tip":"https://ko-fi.com/socksthewolf/tip"}; 20 - R2_SETTINGS: {"auto_prune":true,"prune_days":3}; 17 + R2_SETTINGS: {"auto_prune":false,"prune_days":3}; 21 18 SITE_SETTINGS: {"use_agent_map":false}; 19 + BETTER_AUTH_SECRET: string; 20 + BETTER_AUTH_URL: string; 22 21 DEFAULT_ADMIN_USER: string; 23 22 DEFAULT_ADMIN_PASS: string; 24 23 DEFAULT_ADMIN_BSKY_PASS: string; 24 + TURNSTILE_PUBLIC_KEY: string; 25 + TURNSTILE_SECRET_KEY: string; 26 + RESET_BOT_USERNAME: string; 27 + RESET_BOT_APP_PASS: string; 28 + RESIZE_SECRET_HEADER: string; 29 + IN_DEV: string; 30 + } 31 + interface Env { 25 32 BETTER_AUTH_SECRET: string; 26 33 BETTER_AUTH_URL: string; 34 + DEFAULT_ADMIN_USER: string; 35 + DEFAULT_ADMIN_PASS: string; 36 + DEFAULT_ADMIN_BSKY_PASS: string; 27 37 TURNSTILE_PUBLIC_KEY: string; 28 38 TURNSTILE_SECRET_KEY: string; 29 39 RESET_BOT_USERNAME: string; 30 40 RESET_BOT_APP_PASS: string; 31 41 RESIZE_SECRET_HEADER: string; 32 42 IN_DEV: string; 43 + KV: KVNamespace; 44 + R2: R2Bucket; 45 + DB: D1Database; 46 + IMAGES: ImagesBinding; 47 + IMAGE_SETTINGS: {"enabled":false} | {"enabled":true,"steps":[95,85,75],"bucket_url":"https://resize.skyscheduler.work/"}; 48 + SIGNUP_SETTINGS: {"use_captcha":false,"invite_only":false,"invite_thread":""} | {"use_captcha":true,"invite_only":false,"invite_thread":""}; 49 + QUEUE_SETTINGS: {"enabled":false,"repostsEnabled":false,"postNowEnabled":false,"threadEnabled":true,"delay_val":100,"post_queues":["POST_QUEUE1"],"repost_queues":[]}; 50 + REDIRECTS: {"contact":"https://bsky.app/profile/skyscheduler.work","tip":"https://ko-fi.com/socksthewolf/tip"}; 51 + R2_SETTINGS: {"auto_prune":false,"prune_days":3} | {"auto_prune":true,"prune_days":3}; 52 + SITE_SETTINGS: {"use_agent_map":false}; 53 + INVITE_POOL?: KVNamespace; 54 + R2RESIZE?: R2Bucket; 55 + DBStaging?: D1Database; 56 + POST_QUEUE1?: Queue; 33 57 } 34 58 } 35 59 interface Env extends Cloudflare.Env {}
+50 -6
wrangler.toml
··· 1 + "$schema" = "./node_modules/wrangler/config-schema.json" 1 2 name = "skyscheduler" 2 3 main = "src/index.tsx" 3 4 compatibility_date = "2024-12-13" ··· 10 11 assets = { run_worker_first = false, directory = "./assets/", not_found_handling = "single-page-application" } 11 12 routes = [{ pattern = "skyscheduler.work", custom_domain = true }] 12 13 13 - kv_namespaces = [ 14 - { binding = "KV", id = "0ecd892014ac47eea38c72be94586e7b" }, # Redis for sessions 15 - { binding = "INVITE_POOL", id = "45e1cff45cad45c28b22c4fbcd30db00" } # Invite key pool 16 - ] 14 + # Redis for sessions 15 + [[kv_namespaces]] 16 + binding = "KV" 17 + id = "0ecd892014ac47eea38c72be94586e7b" 18 + 19 + # Invite key pool 20 + [[kv_namespaces]] 21 + binding = "INVITE_POOL" 22 + id = "45e1cff45cad45c28b22c4fbcd30db00" 17 23 18 24 [[d1_databases]] 19 25 binding = "DB" ··· 22 28 migrations_table = "migrations" 23 29 migrations_dir = "migrations" 24 30 31 + [[d1_databases]] 32 + binding = "DBStaging" 33 + database_name = "skyposts-dev" 34 + database_id = "9e9e3275-c12e-4209-9b8c-a1ee57f7bb8d" 35 + migrations_table = "migrations" 36 + migrations_dir = "migrations" 37 + 25 38 # media storage 26 39 [[r2_buckets]] 27 40 binding = "R2" ··· 71 84 IMAGE_SETTINGS={enabled=true, steps=[95, 85, 75], bucket_url="https://resize.skyscheduler.work/"} 72 85 73 86 # Signup options and if keys should be used 74 - SIGNUP_SETTINGS = {use_captcha=true, invite_only=false, invite_thread="https://bsky.app/profile/skyscheduler.work/post/3ltsfnzdmkk2l"} 87 + SIGNUP_SETTINGS = {use_captcha=true, invite_only=false, invite_thread=""} 75 88 76 89 # queue handling, pushing information. This is experimental. 77 90 QUEUE_SETTINGS = {enabled=false, repostsEnabled=false, postNowEnabled=false, threadEnabled=true, delay_val=100, post_queues=["POST_QUEUE1"], repost_queues=[]} ··· 86 99 SITE_SETTINGS={use_agent_map=false} 87 100 88 101 # set this to true in your .dev.vars to turn off turnstile 89 - IN_DEV=false 102 + IN_DEV=false 103 + 104 + [env.staging] 105 + workers_dev = true 106 + 107 + [env.staging.vars] 108 + BETTER_AUTH_URL="*" 109 + IMAGE_SETTINGS={enabled=false} 110 + SIGNUP_SETTINGS = {use_captcha=false, invite_only=false, invite_thread=""} 111 + QUEUE_SETTINGS = {enabled=false, repostsEnabled=false, postNowEnabled=false, threadEnabled=true, delay_val=100, post_queues=["POST_QUEUE1"], repost_queues=[]} 112 + REDIRECTS = {contact="https://bsky.app/profile/skyscheduler.work", tip="https://ko-fi.com/socksthewolf/tip"} 113 + R2_SETTINGS={auto_prune=false, prune_days=3} 114 + SITE_SETTINGS={use_agent_map=false} 115 + IN_DEV=true 116 + 117 + [[env.staging.d1_databases]] 118 + binding = "DB" 119 + database_name = "skyposts-dev" 120 + database_id = "9e9e3275-c12e-4209-9b8c-a1ee57f7bb8d" 121 + migrations_table = "migrations" 122 + migrations_dir = "migrations" 123 + 124 + [[env.staging.kv_namespaces]] 125 + binding = "KV" 126 + id = "9b9c6cdfd40e405aa3df63544a553b08" 127 + 128 + [env.staging.images] 129 + binding = "IMAGES" 130 + 131 + [[env.staging.r2_buckets]] 132 + binding = "R2" 133 + bucket_name = "skyembeds-dev"