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

push up random queue selection

could be round robined in the future but eh

+74 -14
+1 -1
README.md
··· 26 26 27 27 - Node.js (v24.x or later) 28 28 - Package Manager 29 - - Cloudflare Pro Workers account (for CPU and Queues [can be disabled with `USE_QUEUES` set to false]) 29 + - Cloudflare Pro Workers account (for CPU and Queues [can be disabled with `QUEUE_SETTINGS.enabled` set to false]) 30 30 31 31 ### Installation 32 32
+14
package-lock.json
··· 17 17 "just-flatten-it": "^5.2.0", 18 18 "just-has": "^2.3.0", 19 19 "just-is-empty": "^3.4.1", 20 + "just-random": "^3.2.0", 21 + "just-safe-get": "^4.2.0", 20 22 "just-split": "^3.2.0", 21 23 "just-truncate": "^2.2.0", 22 24 "just-unique": "^4.2.0", ··· 3972 3974 "version": "3.4.1", 3973 3975 "resolved": "https://registry.npmjs.org/just-is-empty/-/just-is-empty-3.4.1.tgz", 3974 3976 "integrity": "sha512-bweyqPPl3HReVyVzLJvCJn+cfXJKh6dy4kSLoW9YLqwFz+2vZbWLN38AOK7esEIVPQwY1W48HaNvOFZqYAjLpw==", 3977 + "license": "MIT" 3978 + }, 3979 + "node_modules/just-random": { 3980 + "version": "3.2.0", 3981 + "resolved": "https://registry.npmjs.org/just-random/-/just-random-3.2.0.tgz", 3982 + "integrity": "sha512-RMf8vbtCfLIbAEHvIPu2FwMkpB/JudGyk/VPfqPItcRgt7k8QnV+Aa7s7kRFPo+bavQkUi8Yg1x/ooW6Ttyb9A==", 3983 + "license": "MIT" 3984 + }, 3985 + "node_modules/just-safe-get": { 3986 + "version": "4.2.0", 3987 + "resolved": "https://registry.npmjs.org/just-safe-get/-/just-safe-get-4.2.0.tgz", 3988 + "integrity": "sha512-+tS4Bvgr/FnmYxOGbwziJ8I2BFk+cP1gQHm6rm7zo61w1SbxBwWGEq/Ryy9Gb6bvnloPq6pz7Bmm4a0rjTNlXA==", 3975 3989 "license": "MIT" 3976 3990 }, 3977 3991 "node_modules/just-split": {
+2
package.json
··· 34 34 "just-flatten-it": "^5.2.0", 35 35 "just-has": "^2.3.0", 36 36 "just-is-empty": "^3.4.1", 37 + "just-random": "^3.2.0", 38 + "just-safe-get": "^4.2.0", 37 39 "just-split": "^3.2.0", 38 40 "just-truncate": "^2.2.0", 39 41 "just-unique": "^4.2.0",
+10 -2
src/types.d.ts
··· 18 18 tip: string; 19 19 } 20 20 21 + type QueueConfigSettings = { 22 + enabled: boolean; 23 + post_queues: string[]; 24 + repost_queues: string[]; 25 + } 26 + 21 27 /** Types, types, types **/ 22 28 export interface Bindings { 23 - USE_QUEUES: boolean; 24 29 DB: D1Database; 25 30 R2: R2Bucket; 26 31 R2RESIZE: R2Bucket; 27 32 KV: KVNamespace; 28 - POST_QUEUE: Queue; 33 + POST_QUEUE1: Queue; 34 + POST_QUEUE2: Queue; 35 + REPOST_QUEUE: Queue; 36 + QUEUE_SETTINGS: QueueConfigSettings; 29 37 INVITE_POOL: KVNamespace; 30 38 IMAGE_SETTINGS: ImageConfigSettings; 31 39 SIGNUP_SETTINGS: SignupConfigSettings;
+25 -4
src/utils/scheduler.ts
··· 4 4 import { getAllPostsForCurrentTime, deleteAllRepostsBeforeCurrentTime, getAllRepostsForCurrentTime, deletePosts, purgePostedPosts } from './dbQuery'; 5 5 import { createPostObject, createRepostObject } from './helpers'; 6 6 import isEmpty from 'just-is-empty'; 7 + import random from 'just-random'; 8 + import get from 'just-safe-get'; 7 9 8 10 export const handlePostTask = async(runtime: ScheduledContext, postData: Post, isQueued: boolean = false) => { 9 11 const madePost = await makePost(runtime, postData, isQueued); ··· 24 26 return madeRepost; 25 27 }; 26 28 29 + // picks a random queue to publish data to 30 + const getRandomQueue = (env: Bindings, listName: string): Queue|null => { 31 + const queueListNames: string[] = get(env.QUEUE_SETTINGS, listName, []); 32 + if (isEmpty(queueListNames)) 33 + return null; 34 + 35 + const queueName: string = random(queueListNames) || ""; 36 + console.log(`Picked ${queueName} from ${listName}`); 37 + return get(env, queueName, null); 38 + }; 39 + 27 40 export const schedulePostTask = async(env: Bindings, ctx: ExecutionContext) => { 28 41 const scheduledPosts = await getAllPostsForCurrentTime(env); 29 42 const scheduledReposts = await getAllRepostsForCurrentTime(env); ··· 38 51 console.log(`handling ${scheduledPosts.length} posts...`); 39 52 scheduledPosts.forEach(async (post) => { 40 53 const postData: Post = createPostObject(post); 41 - if (env.USE_QUEUES) 42 - env.POST_QUEUE.send({type: QueueTaskType.Post, post: postData} as QueueTaskData, { contentType: queueContentType }); 54 + if (env.QUEUE_SETTINGS.enabled) { 55 + // Pick a random consumer to handle this post 56 + const queueConsumer: Queue|null = getRandomQueue(env, "post_queues"); 57 + if (queueConsumer !== null) 58 + queueConsumer.send({type: QueueTaskType.Post, post: postData} as QueueTaskData, { contentType: queueContentType }); 59 + } 43 60 else 44 61 ctx.waitUntil(handlePostTask(runtimeWrapper, postData)); 45 62 }); ··· 52 69 console.log(`handling ${scheduledReposts.length} reposts`); 53 70 scheduledReposts.forEach(async (post) => { 54 71 const postData: Repost = createRepostObject(post); 55 - if (env.USE_QUEUES) 56 - env.POST_QUEUE.send({type: QueueTaskType.Repost, repost: postData} as QueueTaskData, { contentType: queueContentType }); 72 + if (env.QUEUE_SETTINGS.enabled) { 73 + // Pick a random consumer to handle this repost 74 + const queueConsumer: Queue|null = getRandomQueue(env, "repost_queues"); 75 + if (queueConsumer !== null) 76 + queueConsumer.send({type: QueueTaskType.Repost, repost: postData} as QueueTaskData, { contentType: queueContentType }); 77 + } 57 78 else 58 79 ctx.waitUntil(handleRepostTask(runtimeWrapper, postData)); 59 80 });
+7 -5
src/wrangler.d.ts
··· 1 1 /* eslint-disable */ 2 - // Generated by Wrangler by running `wrangler types src/wrangler.d.ts` (hash: ea3001925ecc8e95f0f699968644d698) 2 + // Generated by Wrangler by running `wrangler types src/wrangler.d.ts` (hash: a25bbc9209cdfdbc4cf8f49898227f6f) 3 3 // Runtime types generated with workerd@1.20251210.0 2024-12-13 nodejs_compat 4 4 declare namespace Cloudflare { 5 5 interface GlobalProps { ··· 10 10 INVITE_POOL: KVNamespace; 11 11 IMAGE_SETTINGS: {"enabled":true,"steps":[95,85,75],"bucket_url":"https://resize.skyscheduler.work/"}; 12 12 SIGNUP_SETTINGS: {"use_captcha":true,"invite_only":false,"invite_thread":"https://bsky.app/profile/skyscheduler.work/post/3ltsfnzdmkk2l"}; 13 - USE_QUEUES: true; 13 + QUEUE_SETTINGS: {"enabled":true,"post_queues":["POST_QUEUE1","POST_QUEUE2"],"repost_queues":["REPOST_QUEUE"]}; 14 14 REDIRECTS: {"contact":"https://bsky.app/profile/skyscheduler.work","tip":"https://ko-fi.com/socksthewolf/tip"}; 15 + BETTER_AUTH_SECRET: string; 16 + BETTER_AUTH_URL: string; 15 17 DEFAULT_ADMIN_USER: string; 16 18 DEFAULT_ADMIN_PASS: string; 17 19 DEFAULT_ADMIN_BSKY_PASS: string; 18 - BETTER_AUTH_SECRET: string; 19 - BETTER_AUTH_URL: string; 20 20 TURNSTILE_PUBLIC_KEY: string; 21 21 TURNSTILE_SECRET_KEY: string; 22 22 RESET_BOT_USERNAME: string; ··· 25 25 R2: R2Bucket; 26 26 R2RESIZE: R2Bucket; 27 27 DB: D1Database; 28 - POST_QUEUE: Queue; 28 + POST_QUEUE1: Queue; 29 + POST_QUEUE2: Queue; 30 + REPOST_QUEUE: Queue; 29 31 } 30 32 } 31 33 interface Env extends Cloudflare.Env {}
+15 -2
wrangler.toml
··· 46 46 [observability.traces] 47 47 enabled = true 48 48 head_sampling_rate = 0.5 49 + persist = true 49 50 50 51 [[queues.producers]] 51 52 queue = "skyscheduler-post-queue" 52 - binding = "POST_QUEUE" 53 + binding = "POST_QUEUE1" 54 + [[queues.producers]] 55 + queue = "skyscheduler-post-queue2" 56 + binding = "POST_QUEUE2" 57 + [[queues.producers]] 58 + queue = "skyscheduler-repost-queue" 59 + binding = "REPOST_QUEUE" 60 + 53 61 [[queues.consumers]] 54 62 queue = "skyscheduler-post-queue" 63 + [[queues.consumers]] 64 + queue = "skyscheduler-post-queue2" 65 + [[queues.consumers]] 66 + queue = "skyscheduler-repost-queue" 55 67 56 68 [vars] 57 69 # the domain and protocol that this application is hosted on. note this value is also used for CORS ··· 63 75 # Signup options and if keys should be used 64 76 SIGNUP_SETTINGS = {use_captcha=true, invite_only=false, invite_thread="https://bsky.app/profile/skyscheduler.work/post/3ltsfnzdmkk2l"} 65 77 66 - USE_QUEUES=false 78 + # queue handling, pushing information 79 + QUEUE_SETTINGS={enabled=false, post_queues=["POST_QUEUE1", "POST_QUEUE2"], repost_queues=["REPOST_QUEUE"]} 67 80 68 81 # redirect links 69 82 REDIRECTS = {contact="https://bsky.app/profile/skyscheduler.work", tip="https://ko-fi.com/socksthewolf/tip"}