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

trying to reorganize

mostly just trying to cutdown on giant monolithic files into stuff that's more maintainable. I'm sure I'll go crazy later.

+476 -469
+3 -1
README.md
··· 115 115 ```text 116 116 skyscheduler/ 117 117 ├── assets/ 118 + ├── migrations/ 118 119 ├── src/ 119 120 │ ├── auth/ 121 + │ ├── classes/ 120 122 │ ├── db/ 121 123 │ ├── endpoints/ 122 124 │ ├── layout/ 123 125 │ ├── middleware/ 124 126 │ ├── pages/ 127 + │ ├── types/ 125 128 │ ├── utils/ 126 129 │ └── validation/ 127 - ├── migrations/ 128 130 ├── .dev.vars 129 131 ├── .node-version 130 132 ├── .markdownlint.json
+1 -1
src/auth/index.ts
··· 6 6 import { schema } from "../db"; 7 7 import { BSKY_MAX_USERNAME_LENGTH, BSKY_MIN_USERNAME_LENGTH } from "../limits"; 8 8 import { APP_NAME } from "../siteinfo"; 9 - import { Bindings } from "../types"; 9 + import { Bindings } from "../types/settings"; 10 10 import { lookupBskyHandle } from "../utils/bskyApi"; 11 11 import { createDMWithUser } from "../utils/bskyMsg"; 12 12
+18
src/classes/bskyLogin.ts
··· 1 + import isEmpty from "just-is-empty"; 2 + 3 + export class BskyAPILoginCreds { 4 + pds: string; 5 + username: string; 6 + password: string; 7 + valid: boolean; 8 + constructor(data: any) { 9 + if (isEmpty(data)) { 10 + this.password = this.username = this.pds = ""; 11 + } else { 12 + this.pds = data.pds; 13 + this.username = data.user; 14 + this.password = data.pass; 15 + } 16 + this.valid = !isEmpty(data.user) && !isEmpty(data.pass); 17 + } 18 + };
+23
src/classes/context.ts
··· 1 + import { drizzle } from "drizzle-orm/d1"; 2 + import { ExecutionContext } from "hono"; 3 + import { Bindings } from "../types/settings"; 4 + 5 + export class ScheduledContext { 6 + executionCtx: ExecutionContext; 7 + env: Bindings; 8 + #map: Map<string, any>; 9 + constructor(env: Bindings, executionCtx: ExecutionContext) { 10 + this.#map = new Map<string, any>(); 11 + this.env = env; 12 + this.executionCtx = executionCtx; 13 + this.set("db", drizzle(env.DB)); 14 + } 15 + get(name: string) { 16 + if (this.#map.has(name)) 17 + return this.#map.get(name); 18 + return null; 19 + } 20 + set(name: string, value: any) { 21 + this.#map.set(name, value); 22 + } 23 + }
+84
src/classes/post.ts
··· 1 + import has from "just-has"; 2 + import { EmbedData, PostLabel } from "../types/posts"; 3 + import { RepostInfo } from "./repost"; 4 + import isEmpty from "just-is-empty"; 5 + 6 + // Basically a copy of the schema 7 + 8 + export class Post { 9 + // guid for post 10 + postid: string; 11 + // SkyScheduler User Id 12 + user: string; 13 + // post data 14 + text: string; 15 + embeds?: EmbedData[]; 16 + label: PostLabel; 17 + // post flags 18 + postNow: boolean; 19 + posted?: boolean; 20 + isRepost?: boolean; 21 + // repost metadata 22 + repostInfo?: RepostInfo[]; 23 + scheduledDate?: string; 24 + repostCount?: number; 25 + // atproto data 26 + cid?: string; 27 + uri?: string; 28 + // thread data 29 + isThreadRoot: boolean; 30 + isChildPost: boolean; 31 + threadOrder: number; 32 + rootPost?: string; 33 + parentPost?: string; 34 + 35 + constructor(data: any) { 36 + this.user = data.userId; 37 + this.postid = data.uuid; 38 + this.embeds = data.embedContent; 39 + this.label = data.contentLabel; 40 + this.text = data.content; 41 + this.postNow = data.postNow; 42 + this.threadOrder = data.threadOrder; 43 + 44 + if (has(data, "repostCount")) 45 + this.repostCount = data.repostCount; 46 + 47 + if (data.scheduledDate) 48 + this.scheduledDate = data.scheduledDate; 49 + 50 + if (data.repostInfo) 51 + this.repostInfo = data.repostInfo; 52 + 53 + if (data.rootPost) 54 + this.rootPost = data.rootPost; 55 + 56 + if (data.parentPost) { 57 + this.parentPost = data.parentPost; 58 + this.isChildPost = true; 59 + } else { 60 + this.isChildPost = false; 61 + } 62 + 63 + if (data.threadOrder == 0) 64 + this.isThreadRoot = true; 65 + else 66 + this.isThreadRoot = false; 67 + 68 + // ATProto data 69 + if (data.uri) 70 + this.uri = data.uri; 71 + if (data.cid) 72 + this.cid = data.cid; 73 + 74 + if (has(data, "isRepost")) 75 + this.isRepost = data.isRepost; 76 + 77 + if (has(data, "posted")) 78 + this.posted = data.posted; 79 + 80 + // if a cid flag appears for the object and it's a thread root, then the post (if marked not posted) is posted. 81 + if (this.posted == false && !isEmpty(data.cid) && this.isThreadRoot) 82 + this.posted = true; 83 + } 84 + };
+37
src/classes/repost.ts
··· 1 + import has from "just-has"; 2 + 3 + export class Repost { 4 + postid: string; 5 + uri: string; 6 + cid: string; 7 + userId: string; 8 + scheduleGuid?: string; 9 + constructor(data: any) { 10 + this.postid = data.uuid; 11 + this.cid = data.cid; 12 + this.uri = data.uri; 13 + this.userId = data.userId; 14 + if (data.scheduleGuid) 15 + this.scheduleGuid = data.scheduleGuid; 16 + } 17 + }; 18 + 19 + // Contains the repost info for a post 20 + export class RepostInfo { 21 + guid: string; 22 + time: Date; 23 + hours: number; 24 + count: number; 25 + constructor(id: string, time: Date, isRepost: boolean, repostData: any) { 26 + this.time = time; 27 + this.guid = id; 28 + if (has(repostData, "hours") && has(repostData, "times")) { 29 + this.hours = repostData.hours; 30 + this.count = repostData.times; 31 + } else { 32 + this.count = (isRepost) ? 1 : 0; 33 + this.hours = 0; 34 + } 35 + } 36 + }; 37 +
+2 -1
src/db/app.schema.ts
··· 1 1 import { sql } from "drizzle-orm"; 2 2 import { index, integer, sqliteTable, text, unique } from "drizzle-orm/sqlite-core"; 3 - import { EmbedData, PostLabel, RepostInfo } from '../types'; 3 + import { EmbedData, PostLabel } from "../types/posts"; 4 + import { RepostInfo } from "../classes/repost"; 4 5 import { users } from "./auth.schema"; 5 6 6 7 export const posts = sqliteTable('posts', {
+1 -1
src/endpoints/account.tsx
··· 6 6 import { authMiddleware } from "../middleware/auth"; 7 7 import { corsHelperMiddleware } from "../middleware/corsHelper"; 8 8 import { verifyTurnstile } from "../middleware/turnstile"; 9 - import { Bindings, LooseObj } from "../types"; 9 + import { Bindings } from "../types/settings"; 10 10 import { lookupBskyHandle, lookupBskyPDS } from "../utils/bskyApi"; 11 11 import { checkIfCanDMUser } from "../utils/bskyMsg"; 12 12 import { getAllMediaOfUser } from "../utils/db/file";
+1 -1
src/endpoints/admin.tsx
··· 5 5 import { authAdminOnlyMiddleware } from "../middleware/adminOnly"; 6 6 import { corsHelperMiddleware } from "../middleware/corsHelper"; 7 7 import { APP_NAME, SITE_URL } from "../siteinfo"; 8 - import { Bindings } from "../types"; 8 + import { Bindings } from "../types/settings"; 9 9 import { getAllAbandonedMedia } from "../utils/db/file"; 10 10 import { runMaintenanceUpdates } from "../utils/db/maintain"; 11 11 import { makeInviteKey } from "../utils/inviteKeys";
+1 -1
src/endpoints/openapi.tsx
··· 3 3 import { Context, Hono } from "hono"; 4 4 import { describeRoute, resolver, validator } from "hono-openapi"; 5 5 import { ContextVariables } from "../auth"; 6 - import { Bindings } from "../types"; 6 + import { Bindings } from "../types/settings"; 7 7 import { AccountDeleteSchema, AccountForgotSchema } from "../validation/accountForgotDeleteSchema"; 8 8 import { 9 9 AccountResetSchema, PasswordResetCheckCallbackParam,
+4 -5
src/endpoints/post.tsx
··· 3 3 import isEmpty from "just-is-empty"; 4 4 import { validate as isValid } from 'uuid'; 5 5 import { ContextVariables } from "../auth"; 6 + import { Post } from "../classes/post"; 6 7 import { PostEdit } from "../layout/editPost"; 7 8 import { PostHTML } from "../layout/post"; 8 9 import { ScheduledPostList } from "../layout/postList"; 9 10 import { authMiddleware } from "../middleware/auth"; 10 11 import { corsHelperMiddleware } from "../middleware/corsHelper"; 11 - import { 12 - Bindings, CreateObjectResponse, CreatePostQueryResponse, 13 - DeleteResponse, 14 - EmbedDataType, LooseObj, Post 15 - } from "../types"; 12 + import { EmbedDataType } from "../types/posts"; 13 + import { CreateObjectResponse, CreatePostQueryResponse, DeleteResponse } from "../types/responses"; 14 + import { Bindings } from "../types/settings"; 16 15 import { 17 16 createPost, createRepost, 18 17 deletePost, getPostById,
+1 -1
src/endpoints/preview.tsx
··· 5 5 import { BSKY_IMG_MIME_TYPES } from "../limits"; 6 6 import { authMiddleware } from "../middleware/auth"; 7 7 import { corsHelperMiddleware } from "../middleware/corsHelper"; 8 - import { Bindings } from "../types"; 8 + import { Bindings } from "../types/settings"; 9 9 import { FileContentSchema } from "../validation/mediaSchema"; 10 10 11 11 export const preview = new Hono<{ Bindings: Bindings, Variables: ContextVariables }>();
+3 -1
src/index.tsx
··· 1 1 import { drizzle } from "drizzle-orm/d1"; 2 2 import { Env, Hono } from "hono"; 3 3 import { ContextVariables, createAuth } from "./auth"; 4 + import { ScheduledContext } from "./classes/context"; 4 5 import { account } from "./endpoints/account"; 5 6 import { admin } from "./endpoints/admin"; 6 7 import { post } from "./endpoints/post"; ··· 17 18 import Signup from "./pages/signup"; 18 19 import TermsOfService from "./pages/tos"; 19 20 import { SITE_URL } from "./siteinfo"; 20 - import { Bindings, QueueTaskData, ScheduledContext, TaskType } from "./types"; 21 + import { QueueTaskData, TaskType } from "./types/queue"; 22 + import { Bindings } from "./types/settings"; 21 23 import { AgentMap } from "./utils/bskyAgents"; 22 24 import { makeConstScript } from "./utils/constScriptGen"; 23 25 import {
+2 -1
src/layout/editPost.tsx
··· 1 + import { Post } from "../classes/post"; 1 2 import { MAX_LENGTH } from "../limits"; 2 - import { EmbedDataType, Post } from "../types"; 3 + import { EmbedDataType } from "../types/posts"; 3 4 import { PostContent } from "./post"; 4 5 5 6 type EditedPostProps = {
+1 -1
src/layout/helpers/includesTags.tsx
··· 1 - import { PreloadRules } from "../../types"; 1 + import { PreloadRules } from "../../types/site"; 2 2 3 3 type DepTagsType = { 4 4 scripts?: PreloadRules[]
+1 -1
src/layout/main.tsx
··· 1 1 import { html } from 'hono/html'; 2 2 import { Child } from 'hono/jsx'; 3 3 import { APP_NAME } from "../siteinfo"; 4 - import { PreloadRules } from '../types'; 4 + import { PreloadRules } from "../types/site"; 5 5 import { mainScriptStr } from '../utils/appScripts'; 6 6 import { PreloadDependencyTags } from './helpers/includesTags'; 7 7 import MetaTags from './helpers/metaTags';
+1 -1
src/layout/makePost.tsx
··· 10 10 R2_FILE_SIZE_LIMIT_IN_MB 11 11 } from "../limits"; 12 12 import { APP_NAME } from "../siteinfo"; 13 - import { PreloadRules } from "../types"; 13 + import { PreloadRules } from "../types/site"; 14 14 import { ConstScriptPreload } from "../utils/constScriptGen"; 15 15 import { IncludeDependencyTags } from "./helpers/includesTags"; 16 16 import { ContentLabelOptions } from "./options/contentLabelOptions";
+1 -1
src/layout/passwordFields.tsx
··· 1 1 import { html } from "hono/html"; 2 2 import { BSKY_MAX_APP_PASSWORD_LENGTH, MAX_DASHBOARD_PASS, MIN_DASHBOARD_PASS } from "../limits"; 3 - import { PWAutoCompleteSettings } from "../types"; 3 + import { PWAutoCompleteSettings } from "../types/site"; 4 4 5 5 type PasswordFieldSettings = { 6 6 required?: boolean
+1 -1
src/layout/post.tsx
··· 1 1 import { html } from "hono/html"; 2 + import { Post } from "../classes/post"; 2 3 import { MAX_POSTED_LENGTH } from "../limits"; 3 - import { Post } from "../types"; 4 4 import { PostDataFooter, PostDataHeader } from "./posts/wrappers"; 5 5 6 6 type PostContentProps = {
+1 -1
src/layout/postList.tsx
··· 1 1 import { Context } from "hono"; 2 2 import isEmpty from "just-is-empty"; 3 - import { Post } from "../types"; 3 + import { Post } from "../classes/post"; 4 4 import { getPostsForUser } from "../utils/dbQuery"; 5 5 import { PostHTML } from "./post"; 6 6
+1 -1
src/layout/posts/repostData.tsx
··· 1 1 import { raw } from "hono/html"; 2 2 import isEmpty from "just-is-empty"; 3 - import { RepostInfo } from "../../types"; 3 + import { RepostInfo } from "../../classes/repost"; 4 4 5 5 type RepostIconProps = { 6 6 isRepost?: boolean;
+1 -1
src/layout/posts/wrappers.tsx
··· 1 1 import { raw } from "hono/html"; 2 2 import isEmpty from "just-is-empty"; 3 - import { Post } from "../../types"; 3 + import { Post } from "../../classes/post"; 4 4 import { AddPostToThreadButton, DeletePostButton, EditPostButton } from "./buttons"; 5 5 import { RepostCountElement, RepostIcon } from "./repostData"; 6 6
+1 -1
src/layout/settings.tsx
··· 1 1 import { MAX_DASHBOARD_PASS, MIN_DASHBOARD_PASS } from "../limits"; 2 2 import { APP_NAME } from "../siteinfo"; 3 - import { PWAutoCompleteSettings } from "../types"; 3 + import { PWAutoCompleteSettings } from "../types/site"; 4 4 import { settingsScriptStr } from "../utils/appScripts"; 5 5 import { BSkyAppPasswordField, DashboardPasswordField } from "./passwordFields"; 6 6 import { UsernameField } from "./usernameField";
+3 -3
src/pages/dashboard.tsx
··· 2 2 import { AltTextDialog } from "../layout/altTextModal"; 3 3 import FooterCopyright from "../layout/helpers/footer"; 4 4 import { IncludeDependencyTags } from "../layout/helpers/includesTags"; 5 + import { LogoImage } from "../layout/helpers/logo"; 5 6 import { BaseLayout } from "../layout/main"; 6 7 import { PostCreation, PreloadPostCreation } from "../layout/makePost"; 7 8 import { MakeRetweet } from "../layout/makeRetweet"; ··· 9 10 import { Settings, SettingsButton } from "../layout/settings"; 10 11 import { ViolationNoticeBar } from "../layout/violationsBar"; 11 12 import { APP_NAME, SHOW_SUPPORT_PROGRESS_BAR } from "../siteinfo"; 12 - import { PreloadRules } from "../types"; 13 + import { PreloadRules } from "../types/site"; 13 14 import { 14 15 dashboardScriptStr, 15 16 settingsScriptStr 16 17 } from "../utils/appScripts"; 17 - import { LogoImage } from "../layout/helpers/logo"; 18 18 19 - export default function Dashboard(props:any) { 19 + export default function Dashboard(props: any) { 20 20 const ctx: Context = props.c; 21 21 // 3rd party dependencies 22 22 const defaultDashboardPreloads: PreloadRules[] = [
+1 -1
src/pages/login.tsx
··· 3 3 import { BaseLayout } from "../layout/main"; 4 4 import { DashboardPasswordField } from "../layout/passwordFields"; 5 5 import { UsernameField } from "../layout/usernameField"; 6 - import { PWAutoCompleteSettings } from "../types"; 6 + import { PWAutoCompleteSettings } from "../types/site"; 7 7 8 8 export default function Login() { 9 9 const links = [{title: "Sign Up", url: "/signup"}, {title: "Forgot Password", url: "/forgot"}];
+1 -1
src/pages/signup.tsx
··· 7 7 import { BSkyAppPasswordField, DashboardPasswordField } from "../layout/passwordFields"; 8 8 import { UsernameField } from "../layout/usernameField"; 9 9 import { MAX_DASHBOARD_PASS, MIN_DASHBOARD_PASS } from "../limits"; 10 - import { PWAutoCompleteSettings } from "../types"; 10 + import { PWAutoCompleteSettings } from "../types/site"; 11 11 import { getInviteThread, isUsingInviteKeys } from "../utils/inviteKeys"; 12 12 13 13 export default function Signup(props:any) {
+7
src/types.d.ts
··· 1 + // Easy type declares for typescript type checking 2 + // not used otherwise 3 + declare type LooseObj = { 4 + [key: string]: any; 5 + }; 6 + declare type BatchQuery = [BatchItem<'sqlite'>, ...BatchItem<'sqlite'>[]]; 7 + declare type AllContext = Context | ScheduledContext;
-284
src/types.ts
··· 1 - import { BatchItem } from "drizzle-orm/batch"; 2 - import { drizzle } from "drizzle-orm/d1"; 3 - import { Context, ExecutionContext } from "hono"; 4 - 5 - /*** Settings config wrappers for bindings ***/ 6 - type ImageConfigSettings = { 7 - enabled: boolean; 8 - steps?: number[]; 9 - bucket_url?: string; 10 - }; 11 - 12 - type SignupConfigSettings = { 13 - use_captcha: boolean; 14 - invite_only: boolean; 15 - invite_thread?: string; 16 - invite_uses: number; 17 - } 18 - 19 - type RedirectConfigSettings = { 20 - contact: string; 21 - tip: string; 22 - } 23 - 24 - type R2ConfigSettings = { 25 - auto_prune: boolean; 26 - prune_days?: number; 27 - } 28 - 29 - type QueueConfigSettings = { 30 - enabled: boolean; 31 - repostsEnabled: boolean; 32 - threadEnabled: boolean; 33 - postNowEnabled?: boolean; 34 - delay_val: number; 35 - post_queues: string[]; 36 - repost_queues: string[]; 37 - } 38 - 39 - export type AgentConfigSettings = { 40 - use_posts: boolean; 41 - use_reposts: boolean; 42 - } 43 - 44 - /** Types, types, types **/ 45 - export interface Bindings { 46 - DB: D1Database; 47 - R2: R2Bucket; 48 - R2RESIZE: R2Bucket; 49 - KV: KVNamespace; 50 - IMAGES: ImagesBinding; 51 - ASSETS?: Fetcher; 52 - POST_QUEUE: Queue; 53 - REPOST_QUEUE: Queue; 54 - QUEUE_SETTINGS: QueueConfigSettings; 55 - INVITE_POOL?: KVNamespace; 56 - IMAGE_SETTINGS: ImageConfigSettings; 57 - SIGNUP_SETTINGS: SignupConfigSettings; 58 - TASK_SETTINGS: AgentConfigSettings; 59 - R2_SETTINGS: R2ConfigSettings; 60 - DEFAULT_ADMIN_USER: string; 61 - DEFAULT_ADMIN_PASS: string; 62 - DEFAULT_ADMIN_BSKY_PASS: string; 63 - BETTER_AUTH_SECRET: string; 64 - BETTER_AUTH_URL: string; 65 - TURNSTILE_PUBLIC_KEY: string; 66 - TURNSTILE_SECRET_KEY: string; 67 - RESIZE_SECRET_HEADER: string; 68 - RESET_BOT_USERNAME: string; 69 - RESET_BOT_APP_PASS: string; 70 - ENCRYPTED_PASS_KEY: string; 71 - IN_DEV: boolean; 72 - REDIRECTS: RedirectConfigSettings; 73 - }; 74 - 75 - export enum EmbedDataType { 76 - None = 0, 77 - Image = 1, 78 - WebLink = 2, 79 - Video = 3, 80 - Record = 4, 81 - }; 82 - 83 - export type EmbedData = { 84 - content: string; 85 - alt?: string; 86 - title?: string; 87 - uri?: string; 88 - type: EmbedDataType; 89 - description?: string; 90 - width?: number; 91 - height?: number; 92 - duration?: number; 93 - }; 94 - 95 - // Contains the repost info for a post 96 - export type RepostInfo = { 97 - guid: string, 98 - time: Date, 99 - hours: number, 100 - count: number 101 - }; 102 - 103 - export enum PostLabel { 104 - None = "None", 105 - Suggestive = "Suggestive", 106 - Nudity = "Nudity", 107 - Adult = "Adult", 108 - Graphic = "Graphic", 109 - GraphicAdult = "GraphicAdult" 110 - }; 111 - 112 - // Basically a copy of the schema 113 - export type Post = { 114 - // guid for post 115 - postid: string; 116 - // SkyScheduler User Id 117 - user: string; 118 - // post data 119 - text: string; 120 - embeds?: EmbedData[]; 121 - label: PostLabel; 122 - // post flags 123 - postNow: boolean; 124 - posted?: boolean; 125 - isRepost?: boolean; 126 - // repost metadata 127 - repostInfo?: RepostInfo[]; 128 - scheduledDate?: string; 129 - repostCount?: number; 130 - // atproto data 131 - cid?: string; 132 - uri?: string; 133 - // thread data 134 - isThreadRoot: boolean; 135 - isChildPost: boolean; 136 - threadOrder: number; 137 - rootPost?: string; 138 - parentPost?: string; 139 - }; 140 - 141 - export type Repost = { 142 - postid: string; 143 - uri: string; 144 - cid: string; 145 - userId: string; 146 - scheduleGuid?: string; 147 - }; 148 - 149 - export enum TaskType { 150 - None, 151 - Post, 152 - Repost, 153 - }; 154 - 155 - export type QueueTaskData = { 156 - type: TaskType; 157 - post?: Post; 158 - repost?: Repost; 159 - }; 160 - 161 - export type Violation = { 162 - userId: string; 163 - tosViolation: boolean; 164 - userPassInvalid: boolean; 165 - accountSuspended: boolean; 166 - accountGone: boolean; 167 - mediaTooBig: boolean; 168 - createdAt: string; 169 - }; 170 - 171 - export type PostResponseObject = { 172 - uri: string; 173 - cid: string; 174 - }; 175 - 176 - export type PostRecordResponse = PostResponseObject & { 177 - postID: string|null; 178 - content: string; 179 - embeds?: EmbedData[]; 180 - }; 181 - 182 - export type PostStatus = { 183 - records: PostRecordResponse[]; 184 - // number of expected successes 185 - expected: number; 186 - // number of successes we got 187 - got: number; 188 - }; 189 - 190 - export type DeleteResponse = { 191 - success: boolean; 192 - needsRefresh?: boolean; 193 - } 194 - 195 - export interface LooseObj { 196 - [key: string]: any; 197 - }; 198 - 199 - export enum AccountStatus { 200 - None = 0, 201 - Ok, 202 - Suspended, 203 - Deactivated, 204 - TakenDown, 205 - InvalidAccount, 206 - PlatformOutage, 207 - MediaTooBig, 208 - UnhandledError, 209 - TOSViolation, 210 - }; 211 - 212 - export enum PWAutoCompleteSettings { 213 - Off, 214 - NewPass, 215 - CurrentPass 216 - }; 217 - 218 - export type PreloadRules = { 219 - type: string; 220 - href: string; 221 - }; 222 - 223 - export class ScheduledContext { 224 - executionCtx: ExecutionContext; 225 - env: Bindings; 226 - #map: Map<string, any>; 227 - constructor(env: Bindings, executionCtx: ExecutionContext) { 228 - this.#map = new Map<string, any>(); 229 - this.env = env; 230 - this.executionCtx = executionCtx; 231 - this.set("db", drizzle(env.DB)); 232 - } 233 - get(name: string) { 234 - if (this.#map.has(name)) 235 - return this.#map.get(name); 236 - return null; 237 - } 238 - set(name: string, value: any) { 239 - this.#map.set(name, value); 240 - } 241 - }; 242 - 243 - export type AllContext = Context|ScheduledContext; 244 - 245 - export type BskyEmbedWrapper = { 246 - type: EmbedDataType; 247 - data?: any; 248 - }; 249 - 250 - export type BskyRecordWrapper = { 251 - cid?: string; 252 - uri?: string; 253 - }; 254 - 255 - export type CreateObjectResponse = { 256 - ok: boolean; 257 - msg: string; 258 - postId?: string; 259 - }; 260 - 261 - export type CreatePostQueryResponse = CreateObjectResponse & { 262 - postNow?: boolean; 263 - }; 264 - 265 - export type BskyAPILoginCreds = { 266 - pds: string; 267 - username: string; 268 - password: string; 269 - valid: boolean; 270 - }; 271 - 272 - // Used for the pruning and database operations 273 - export type GetAllPostedBatch = { 274 - id: string; 275 - uri: string|null; 276 - }; 277 - 278 - export type R2BucketObject = { 279 - name: string; 280 - user: string|null; 281 - date: Date 282 - } 283 - 284 - export type BatchQuery = [BatchItem<'sqlite'>, ...BatchItem<'sqlite'>[]];
+23
src/types/accounts.ts
··· 1 + export enum AccountStatus { 2 + None = 0, 3 + Ok, 4 + Suspended, 5 + Deactivated, 6 + TakenDown, 7 + InvalidAccount, 8 + PlatformOutage, 9 + MediaTooBig, 10 + UnhandledError, 11 + TOSViolation 12 + }; 13 + 14 + export type Violation = { 15 + userId: string; 16 + tosViolation: boolean; 17 + userPassInvalid: boolean; 18 + accountSuspended: boolean; 19 + accountGone: boolean; 20 + mediaTooBig: boolean; 21 + createdAt: string; 22 + }; 23 +
+11
src/types/bsky.ts
··· 1 + import { EmbedDataType } from "./posts"; 2 + 3 + export type BskyEmbedWrapper = { 4 + type: EmbedDataType; 5 + data?: any; 6 + }; 7 + 8 + export type BskyRecordWrapper = { 9 + cid?: string; 10 + uri?: string; 11 + };
+11
src/types/data.ts
··· 1 + // Used for the pruning and database operations 2 + export type GetAllPostedBatch = { 3 + id: string; 4 + uri: string|null; 5 + }; 6 + 7 + export type R2BucketObject = { 8 + name: string; 9 + user: string|null; 10 + date: Date 11 + };
+28
src/types/posts.ts
··· 1 + export enum EmbedDataType { 2 + None = 0, 3 + Image = 1, 4 + WebLink = 2, 5 + Video = 3, 6 + Record = 4 7 + }; 8 + 9 + export type EmbedData = { 10 + content: string; 11 + alt?: string; 12 + title?: string; 13 + uri?: string; 14 + type: EmbedDataType; 15 + description?: string; 16 + width?: number; 17 + height?: number; 18 + duration?: number; 19 + }; 20 + 21 + export enum PostLabel { 22 + None = "None", 23 + Suggestive = "Suggestive", 24 + Nudity = "Nudity", 25 + Adult = "Adult", 26 + Graphic = "Graphic", 27 + GraphicAdult = "GraphicAdult" 28 + };
+14
src/types/queue.ts
··· 1 + import { Repost } from "../classes/repost"; 2 + import { Post } from "../classes/post"; 3 + 4 + export enum TaskType { 5 + None, 6 + Post, 7 + Repost 8 + }; 9 + 10 + export type QueueTaskData = { 11 + type: TaskType; 12 + post?: Post; 13 + repost?: Repost; 14 + };
+35
src/types/responses.ts
··· 1 + import { EmbedData } from "./posts"; 2 + 3 + export type PostResponseObject = { 4 + uri: string; 5 + cid: string; 6 + }; 7 + 8 + export type PostRecordResponse = PostResponseObject & { 9 + postID: string | null; 10 + content: string; 11 + embeds?: EmbedData[]; 12 + }; 13 + 14 + export type PostStatus = { 15 + records: PostRecordResponse[]; 16 + // number of expected successes 17 + expected: number; 18 + // number of successes we got 19 + got: number; 20 + }; 21 + 22 + export type DeleteResponse = { 23 + success: boolean; 24 + needsRefresh?: boolean; 25 + }; 26 + 27 + export type CreateObjectResponse = { 28 + ok: boolean; 29 + msg: string; 30 + postId?: string; 31 + }; 32 + 33 + export type CreatePostQueryResponse = CreateObjectResponse & { 34 + postNow?: boolean; 35 + };
+69
src/types/settings.ts
··· 1 + /*** Settings config wrappers for bindings ***/ 2 + type ImageConfigSettings = { 3 + enabled: boolean; 4 + steps?: number[]; 5 + bucket_url?: string; 6 + }; 7 + 8 + type SignupConfigSettings = { 9 + use_captcha: boolean; 10 + invite_only: boolean; 11 + invite_thread?: string; 12 + invite_uses: number; 13 + }; 14 + 15 + type RedirectConfigSettings = { 16 + contact: string; 17 + tip: string; 18 + }; 19 + 20 + type R2ConfigSettings = { 21 + auto_prune: boolean; 22 + prune_days?: number; 23 + }; 24 + 25 + type QueueConfigSettings = { 26 + enabled: boolean; 27 + repostsEnabled: boolean; 28 + threadEnabled: boolean; 29 + postNowEnabled?: boolean; 30 + delay_val: number; 31 + post_queues: string[]; 32 + repost_queues: string[]; 33 + }; 34 + 35 + export type AgentConfigSettings = { 36 + use_posts: boolean; 37 + use_reposts: boolean; 38 + }; 39 + 40 + /** Types, types, types **/ 41 + export interface Bindings { 42 + DB: D1Database; 43 + R2: R2Bucket; 44 + R2RESIZE: R2Bucket; 45 + KV: KVNamespace; 46 + IMAGES: ImagesBinding; 47 + ASSETS?: Fetcher; 48 + POST_QUEUE: Queue; 49 + REPOST_QUEUE: Queue; 50 + QUEUE_SETTINGS: QueueConfigSettings; 51 + INVITE_POOL?: KVNamespace; 52 + IMAGE_SETTINGS: ImageConfigSettings; 53 + SIGNUP_SETTINGS: SignupConfigSettings; 54 + TASK_SETTINGS: AgentConfigSettings; 55 + R2_SETTINGS: R2ConfigSettings; 56 + DEFAULT_ADMIN_USER: string; 57 + DEFAULT_ADMIN_PASS: string; 58 + DEFAULT_ADMIN_BSKY_PASS: string; 59 + BETTER_AUTH_SECRET: string; 60 + BETTER_AUTH_URL: string; 61 + TURNSTILE_PUBLIC_KEY: string; 62 + TURNSTILE_SECRET_KEY: string; 63 + RESIZE_SECRET_HEADER: string; 64 + RESET_BOT_USERNAME: string; 65 + RESET_BOT_APP_PASS: string; 66 + ENCRYPTED_PASS_KEY: string; 67 + IN_DEV: boolean; 68 + REDIRECTS: RedirectConfigSettings; 69 + };
+11
src/types/site.ts
··· 1 + // Used for jsx rendering 2 + export type PreloadRules = { 3 + type: string; 4 + href: string; 5 + }; 6 + 7 + export enum PWAutoCompleteSettings { 8 + Off, 9 + NewPass, 10 + CurrentPass 11 + };
+5 -4
src/utils/bskyAgents.ts
··· 4 4 // 5 5 // Also just handles general login. 6 6 import AtpAgent from "@atproto/api"; 7 - import { 8 - AccountStatus, AgentConfigSettings, 9 - AllContext, LooseObj, Post, Repost, TaskType 10 - } from "../types"; 7 + import { Post } from "../classes/post"; 8 + import { Repost } from "../classes/repost"; 9 + import { AccountStatus } from "../types/accounts"; 10 + import { TaskType } from "../types/queue"; 11 + import { AgentConfigSettings } from "../types/settings"; 11 12 import { getBskyUserPassForId } from "./db/userinfo"; 12 13 import { createViolationForUser } from "./db/violations"; 13 14
+10 -8
src/utils/bskyApi.ts
··· 4 4 import has from 'just-has'; 5 5 import isEmpty from "just-is-empty"; 6 6 import truncate from "just-truncate"; 7 + import { Post } from "../classes/post"; 8 + import { Repost } from "../classes/repost"; 7 9 import { BSKY_IMG_SIZE_LIMIT, MAX_ALT_TEXT, MAX_EMBEDS_PER_POST } from '../limits'; 8 - import { 9 - AccountStatus, 10 - AllContext, 11 - BskyEmbedWrapper, BskyRecordWrapper, EmbedData, EmbedDataType, 12 - LooseObj, Post, PostLabel, 13 - PostRecordResponse, PostStatus, Repost 14 - } from '../types'; 10 + import { AccountStatus } from "../types/accounts"; 11 + import { BskyEmbedWrapper, BskyRecordWrapper } from "../types/bsky"; 12 + import { EmbedData, EmbedDataType, PostLabel } from "../types/posts"; 13 + import { PostRecordResponse, PostStatus } from "../types/responses"; 15 14 import { atpRecordURI } from '../validation/regexCases'; 16 15 import { makeAgentForUser } from './bskyAgents'; 17 - import { bulkUpdatePostedData, getChildPostsOfThread, isPostAlreadyPosted, setPostNowOffForPost } from './db/data'; 16 + import { 17 + bulkUpdatePostedData, getChildPostsOfThread, 18 + isPostAlreadyPosted, setPostNowOffForPost 19 + } from './db/data'; 18 20 import { getUsernameForUserId } from './db/userinfo'; 19 21 import { createViolationForUser } from './db/violations'; 20 22 import { deleteEmbedsFromR2 } from './r2Query';
+2 -1
src/utils/bskyMsg.ts
··· 1 1 import { AtpAgent, RichText } from '@atproto/api'; 2 - import { AccountStatus, Bindings } from '../types'; 2 + import { AccountStatus } from "../types/accounts"; 3 + import { Bindings } from '../types/settings'; 3 4 import { loginToBsky } from './bskyAgents'; 4 5 5 6 const chatHeaders = {headers: {
-1
src/utils/bskyPrune.ts
··· 1 1 import isEmpty from 'just-is-empty'; 2 2 import split from 'just-split'; 3 - import { AllContext } from '../types'; 4 3 import { getPostRecords } from './bskyApi'; 5 4 import { getAllPostedPosts, getAllPostedPostsOfUser } from './db/data'; 6 5
+1 -1
src/utils/constScriptGen.ts
··· 12 12 MAX_THUMBNAIL_SIZE, 13 13 R2_FILE_SIZE_LIMIT 14 14 } from "../limits"; 15 - import { PreloadRules } from "../types"; 15 + import { PreloadRules } from "../types/site"; 16 16 import { postRecordURI } from "../validation/regexCases"; 17 17 18 18 const CONST_SCRIPT_VERSION: number = 8;
+9 -13
src/utils/db/data.ts
··· 3 3 import { DrizzleD1Database } from "drizzle-orm/d1"; 4 4 import isEmpty from "just-is-empty"; 5 5 import { validate as uuidValid } from 'uuid'; 6 + import { Post } from "../../classes/post"; 7 + import { Repost } from "../../classes/repost"; 6 8 import { posts, repostCounts, reposts } from "../../db/app.schema"; 7 9 import { violations } from "../../db/enforcement.schema"; 8 10 import { MAX_HOLD_DAYS_BEFORE_PURGE, MAX_POSTED_LENGTH } from "../../limits"; 9 - import { 10 - AllContext, 11 - BatchQuery, 12 - GetAllPostedBatch, 13 - Post, 14 - PostRecordResponse, 15 - Repost 16 - } from "../../types"; 17 - import { createPostObject, createRepostObject, floorCurrentTime } from "../helpers"; 11 + import { GetAllPostedBatch } from "../../types/data"; 12 + import { PostRecordResponse } from "../../types/responses"; 13 + import { floorCurrentTime } from "../helpers"; 18 14 19 15 export const getAllPostsForCurrentTime = async (c: AllContext, removeThreads: boolean = false): Promise<Post[]> => { 20 16 // Get all scheduled posts for current time ··· 42 38 )); 43 39 const results = await db.with(postsToMake).select().from(postsToMake) 44 40 .where(notInArray(postsToMake.userId, violationUsers)).orderBy(asc(postsToMake.createdAt)).all(); 45 - return results.map((item) => createPostObject(item)); 41 + return results.map((item) => new Post(item)); 46 42 }; 47 43 48 44 export const getAllRepostsForGivenTime = async (c: AllContext, givenDate: Date): Promise<Repost[]> => { ··· 60 56 .where(and(inArray(posts.uuid, query), notInArray(posts.userId, violationsQuery))) 61 57 .all(); 62 58 63 - return results.map((item) => createRepostObject(item)); 59 + return results.map((item) => new Repost(item)); 64 60 }; 65 61 66 62 export const getAllRepostsForCurrentTime = async (c: AllContext): Promise<Repost[]> => { ··· 234 230 .where(and(isNotNull(posts.parentPost), eq(posts.rootPost, rootId))) 235 231 .orderBy(asc(posts.threadOrder), desc(posts.createdAt)).all(); 236 232 if (query.length > 0) { 237 - return query.map((child) => createPostObject(child)); 233 + return query.map((child) => new Post(child)); 238 234 } 239 235 return null; 240 236 }; ··· 304 300 .limit(1).all(); 305 301 306 302 if (!isEmpty(result)) 307 - return createPostObject(result[0]); 303 + return new Post(result[0]); 308 304 return null; 309 305 };
+1 -1
src/utils/db/file.ts
··· 2 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 { AllContext, EmbedDataType, LooseObj } from "../../types"; 5 + import { EmbedDataType } from "../../types/posts"; 6 6 import { daysAgo } from "../helpers"; 7 7 8 8 export const addFileListing = async (c: AllContext, file: string, user: string|null, createDate: Date|null=null) => {
+1 -1
src/utils/db/maintain.ts
··· 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 { AllContext, BatchQuery, R2BucketObject } from "../../types"; 8 + import { R2BucketObject } from "../../types/data"; 9 9 import { getAllFilesList } from "../r2Query"; 10 10 import { addFileListing, getAllMediaOfUser } from "./file"; 11 11
+3 -4
src/utils/db/userinfo.ts
··· 1 1 import { eq } from "drizzle-orm"; 2 2 import { DrizzleD1Database } from "drizzle-orm/d1"; 3 3 import isEmpty from "just-is-empty"; 4 + import { BskyAPILoginCreds } from "../../classes/bskyLogin"; 4 5 import { users } from "../../db/auth.schema"; 5 - import { AllContext, BskyAPILoginCreds } from "../../types"; 6 - import { createLoginCredsObj } from "../helpers"; 7 6 8 7 export const doesUserExist = async (c: AllContext, username: string): Promise<boolean> => { 9 8 const db: DrizzleD1Database = c.get("db"); ··· 33 32 export const getBskyUserPassForId = async (c: AllContext, userid: string): Promise<BskyAPILoginCreds> => { 34 33 const db: DrizzleD1Database = c.get("db"); 35 34 if (!db) 36 - return createLoginCredsObj(null); 35 + return new BskyAPILoginCreds(null); 37 36 38 37 const response = await db.select({user: users.username, pass: users.bskyAppPass, pds: users.pds}) 39 38 .from(users) 40 39 .where(eq(users.id, userid)) 41 40 .limit(1).all(); 42 - return createLoginCredsObj(response[0] || null); 41 + return new BskyAPILoginCreds(response[0] || null); 43 42 }; 44 43 45 44 export const getUsernameForUserId = async (c: AllContext, userId: string): Promise<string|null> => {
+1 -1
src/utils/db/violations.ts
··· 3 3 import flatten from "just-flatten-it"; 4 4 import isEmpty from "just-is-empty"; 5 5 import { bannedUsers, violations } from "../../db/enforcement.schema"; 6 - import { AccountStatus, AllContext, LooseObj, Violation } from "../../types"; 6 + import { AccountStatus, Violation } from "../../types/accounts"; 7 7 import { lookupBskyHandle } from "../bskyApi"; 8 8 import { getUsernameForUserId } from "./userinfo"; 9 9
+12 -17
src/utils/dbQuery.ts
··· 5 5 import has from "just-has"; 6 6 import isEmpty from "just-is-empty"; 7 7 import { v4 as uuidv4, validate as uuidValid } from 'uuid'; 8 + import { Post } from "../classes/post"; 9 + import { RepostInfo } from "../classes/repost"; 8 10 import { mediaFiles, posts, repostCounts, reposts } from "../db/app.schema"; 9 11 import { accounts, users } from "../db/auth.schema"; 10 12 import { MAX_POSTS_PER_THREAD, MAX_REPOST_POSTS, MAX_REPOST_RULES_PER_POST } from "../limits"; 11 13 import { APP_NAME } from "../siteinfo"; 12 - import { 13 - AccountStatus, 14 - AllContext, 15 - BatchQuery, 16 - CreateObjectResponse, CreatePostQueryResponse, 17 - DeleteResponse, 18 - EmbedDataType, 19 - Post, PostLabel, 20 - RepostInfo 21 - } from "../types"; 14 + import { AccountStatus } from "../types/accounts"; 15 + import { EmbedDataType, PostLabel } from "../types/posts"; 16 + import { CreateObjectResponse, CreatePostQueryResponse, DeleteResponse } from "../types/responses"; 22 17 import { PostSchema } from "../validation/postSchema"; 23 18 import { RepostSchema } from "../validation/repostSchema"; 24 19 import { getChildPostsOfThread, getPostByCID, getPostThreadCount, updatePostForGivenUser } from "./db/data"; 25 20 import { getViolationsForUser, removeViolation, removeViolations, userHasViolations } from "./db/violations"; 26 - import { createPostObject, createRepostInfo, floorGivenTime } from "./helpers"; 21 + import { floorGivenTime } from "./helpers"; 27 22 import { deleteEmbedsFromR2 } from "./r2Query"; 28 23 29 24 export const getPostsForUser = async (c: AllContext): Promise<Post[]|null> => { ··· 42 37 if (isEmpty(results)) 43 38 return null; 44 39 45 - return results.map((itm) => createPostObject(itm)); 40 + return results.map((itm) => new Post(itm)); 46 41 } 47 42 } catch(err) { 48 43 console.error(`Failed to get posts for user, session could not be fetched ${err}`); ··· 255 250 // Create repost metadata 256 251 const scheduleGUID = (!isThreadedPost) ? uuidv4() : undefined; 257 252 const repostInfo = (!isThreadedPost) ? 258 - createRepostInfo(scheduleGUID!, scheduleDate, false, repostData) : undefined; 253 + new RepostInfo(scheduleGUID!, scheduleDate, false, repostData) : undefined; 259 254 260 255 // Create the posts 261 256 const postUUID = uuidv4(); ··· 358 353 if (violationData.tosViolation) { 359 354 return {ok: false, msg: `This account is unable to use ${APP_NAME} services at this time`}; 360 355 } else if (violationData.userPassInvalid) { 361 - return {ok: false, msg: "The BSky account credentials is invalid, please update these in the settings"}; 356 + return {ok: false, msg: "The BSky account credentials is invalid, please update th/ese in the settings"}; 362 357 } 363 358 } 364 359 let postUUID; 365 360 let dbOperations: BatchItem<"sqlite">[] = []; 366 361 const scheduleGUID = uuidv4(); 367 - const repostInfo: RepostInfo = createRepostInfo(scheduleGUID, scheduleDate, true, repostData); 362 + const repostInfo: RepostInfo = new RepostInfo(scheduleGUID, scheduleDate, true, repostData); 368 363 369 364 // Check to see if the post already exists 370 365 // (check also against the userId here as well to avoid cross account data collisions) ··· 470 465 .limit(1).all(); 471 466 472 467 if (!isEmpty(result)) 473 - return createPostObject(result[0]); 468 + return new Post(result[0]); 474 469 return null; 475 470 }; 476 471 ··· 495 490 .limit(1).all(); 496 491 497 492 if (!isEmpty(result)) 498 - return createPostObject(result[0]); 493 + return new Post(result[0]); 499 494 return null; 500 495 };
-94
src/utils/helpers.ts
··· 1 1 import { startOfHour, subDays } from "date-fns"; 2 2 import { Context } from "hono"; 3 - import has from "just-has"; 4 - import isEmpty from "just-is-empty"; 5 - import { BskyAPILoginCreds, Post, Repost, RepostInfo } from "../types"; 6 - 7 - export function createPostObject(data: any) { 8 - const postData: Post = (new Object() as Post); 9 - postData.user = data.userId; 10 - postData.postid = data.uuid; 11 - postData.embeds = data.embedContent; 12 - postData.label = data.contentLabel; 13 - postData.text = data.content; 14 - postData.postNow = data.postNow; 15 - postData.threadOrder = data.threadOrder; 16 - 17 - if (has(data, "repostCount")) 18 - postData.repostCount = data.repostCount; 19 - 20 - if (data.scheduledDate) 21 - postData.scheduledDate = data.scheduledDate; 22 - 23 - if (data.repostInfo) 24 - postData.repostInfo = data.repostInfo; 25 - 26 - if (data.rootPost) 27 - postData.rootPost = data.rootPost; 28 - 29 - if (data.parentPost) { 30 - postData.parentPost = data.parentPost; 31 - postData.isChildPost = true; 32 - } else { 33 - postData.isChildPost = false; 34 - } 35 - 36 - if (data.threadOrder == 0) 37 - postData.isThreadRoot = true; 38 - else 39 - postData.isThreadRoot = false; 40 - 41 - // ATProto data 42 - if (data.uri) 43 - postData.uri = data.uri; 44 - if (data.cid) 45 - postData.cid = data.cid; 46 - 47 - if (has(data, "isRepost")) 48 - postData.isRepost = data.isRepost; 49 - 50 - if (has(data, "posted")) 51 - postData.posted = data.posted; 52 - 53 - // if a cid flag appears for the object and it's a thread root, then the post (if marked not posted) is posted. 54 - if (postData.posted == false && !isEmpty(data.cid) && postData.isThreadRoot) 55 - postData.posted = true; 56 - 57 - return postData; 58 - } 59 - 60 - export function createRepostObject(data: any) { 61 - const repostObj: Repost = (new Object() as Repost); 62 - repostObj.postid = data.uuid; 63 - repostObj.cid = data.cid; 64 - repostObj.uri = data.uri; 65 - repostObj.userId = data.userId; 66 - if (data.scheduleGuid) 67 - repostObj.scheduleGuid = data.scheduleGuid; 68 - return repostObj; 69 - } 70 - 71 - export function createRepostInfo(id: string, time: Date, isRepost: boolean, repostData: any) { 72 - const repostObj: RepostInfo = (new Object() as RepostInfo); 73 - repostObj.time = time; 74 - repostObj.guid = id; 75 - if (has(repostData, "hours") && has(repostData, "times")) { 76 - repostObj.hours = repostData.hours; 77 - repostObj.count = repostData.times; 78 - } else { 79 - repostObj.count = (isRepost) ? 1 : 0; 80 - repostObj.hours = 0; 81 - } 82 - return repostObj; 83 - } 84 - 85 - export function createLoginCredsObj(data: any) { 86 - const loginCreds: BskyAPILoginCreds = (new Object() as BskyAPILoginCreds); 87 - if (isEmpty(data)) { 88 - loginCreds.password = loginCreds.username = loginCreds.pds = ""; 89 - } else { 90 - loginCreds.pds = data.pds; 91 - loginCreds.username = data.user; 92 - loginCreds.password = data.pass; 93 - } 94 - loginCreds.valid = !isEmpty(data.user) && !isEmpty(data.pass); 95 - return loginCreds; 96 - } 97 3 98 4 export function floorCurrentTime() { 99 5 return startOfHour(new Date());
+4 -1
src/utils/queuePublisher.ts
··· 1 1 import isEmpty from 'just-is-empty'; 2 2 import random from 'just-random'; 3 3 import get from 'just-safe-get'; 4 - import { AllContext, Bindings, Post, QueueTaskData, Repost, TaskType } from "../types"; 4 + import { Post } from "../classes/post"; 5 + import { Repost } from "../classes/repost"; 6 + import { QueueTaskData, TaskType } from "../types/queue"; 7 + import { Bindings } from '../types/settings'; 5 8 6 9 const queueContentType = 'v8'; 7 10
+2 -1
src/utils/r2Query.ts
··· 15 15 R2_FILE_SIZE_LIMIT, 16 16 R2_FILE_SIZE_LIMIT_IN_MB 17 17 } from "../limits"; 18 - import { AllContext, EmbedData, EmbedDataType, R2BucketObject } from '../types'; 18 + import { R2BucketObject } from '../types/data'; 19 + import { EmbedData, EmbedDataType } from "../types/posts"; 19 20 import { addFileListing, deleteFileListings } from './db/file'; 20 21 21 22 type FileMetaData = {
+7 -2
src/utils/scheduler.ts
··· 1 1 import AtpAgent from '@atproto/api'; 2 2 import isEmpty from 'just-is-empty'; 3 - import { AllContext, Post, Repost, TaskType } from '../types'; 3 + import { Post } from "../classes/post"; 4 + import { Repost } from "../classes/repost"; 5 + import { TaskType } from "../types/queue"; 4 6 import { AgentMap } from './bskyAgents'; 5 7 import { makePost, makeRepost } from './bskyApi'; 6 8 import { pruneBskyPosts } from './bskyPrune'; ··· 10 12 setPostNowOffForPost 11 13 } from './db/data'; 12 14 import { getAllAbandonedMedia } from './db/file'; 13 - import { enqueuePost, enqueueRepost, isQueueEnabled, isRepostQueueEnabled, shouldPostNowQueue, shouldPostThreadQueue } from './queuePublisher'; 15 + import { 16 + enqueuePost, enqueueRepost, isQueueEnabled, isRepostQueueEnabled, 17 + shouldPostNowQueue, shouldPostThreadQueue 18 + } from './queuePublisher'; 14 19 import { deleteFromR2 } from './r2Query'; 15 20 16 21 export const handlePostTask = async(runtime: AllContext, postData: Post, agent: AtpAgent|null) => {
+1 -1
src/validation/embedSchema.ts
··· 1 1 import isEmpty from "just-is-empty"; 2 2 import * as z from "zod/v4"; 3 3 import { BSKY_VIDEO_LENGTH_LIMIT } from "../limits"; 4 - import { EmbedDataType } from "../types"; 4 + import { EmbedDataType } from "../types/posts"; 5 5 import { FileContentSchema } from "./mediaSchema"; 6 6 import { atpRecordURI } from "./regexCases"; 7 7 import { AltTextSchema } from "./sharedValidations";
+9 -3
src/validation/postSchema.ts
··· 1 1 import * as z from "zod/v4"; 2 - import { MAX_LENGTH, MAX_REPOST_INTERVAL_LIMIT, MAX_REPOST_IN_HOURS, MIN_LENGTH } from "../limits"; 3 - import { EmbedDataType, PostLabel } from "../types"; 4 - import { ImageEmbedSchema, LinkEmbedSchema, PostRecordSchema, VideoEmbedSchema } from "./embedSchema"; 2 + import { 3 + MAX_LENGTH, MAX_REPOST_INTERVAL_LIMIT, 4 + MAX_REPOST_IN_HOURS, MIN_LENGTH 5 + } from "../limits"; 6 + import { EmbedDataType, PostLabel } from "../types/posts"; 7 + import { 8 + ImageEmbedSchema, LinkEmbedSchema, 9 + PostRecordSchema, VideoEmbedSchema 10 + } from "./embedSchema"; 5 11 import { FileContentSchema } from "./mediaSchema"; 6 12 import { AltTextSchema } from "./sharedValidations"; 7 13
+2 -1
tsconfig.json
··· 10 10 ], 11 11 "types": [ 12 12 "node", 13 - "./src/wrangler.d.ts" 13 + "./src/wrangler.d.ts", 14 + "./src/types.d.ts" 14 15 ], 15 16 "jsx": "react-jsx", 16 17 "jsxImportSource": "hono/jsx"
+2 -2
wrangler.toml
··· 71 71 72 72 [[queues.consumers]] 73 73 queue = "skyscheduler-post-queue" 74 - max_batch_size = 3 75 - max_batch_timeout = 5 74 + max_batch_size = 2 75 + max_batch_timeout = 3 76 76 max_retries = 3 77 77 78 78 [[queues.consumers]]