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

turn on agent maps

mostly since the queues are now on, might as well to optimize multiple agency for users.

+196 -142
-3
assets/js/postHelper.js
··· 539 539 addClickKeyboardListener(el, (e) => { 540 540 setSelectDisable(e.target.parentElement, !e.target.checked); 541 541 }, keys, false); 542 - /*el.addEventListener("click", (e) => { 543 - setSelectDisable(e.target.parentElement, !e.target.checked); 544 - });*/ 545 542 if (el.getAttribute("startchecked") == "true") { 546 543 setSelectDisable(el.parentElement, false); 547 544 }
+3 -14
src/endpoints/post.tsx
··· 12 12 DeleteResponse, 13 13 EmbedDataType, LooseObj, Post 14 14 } from "../types.d"; 15 - import { makePost } from "../utils/bskyApi"; 16 15 import { 17 16 createPost, createRepost, 18 17 deletePost, getPostById, 19 18 getPostByIdWithReposts, 20 19 updatePostForUser 21 20 } from "../utils/dbQuery"; 22 - import { enqueuePost, shouldPostNowQueue } from "../utils/queuePublisher"; 23 21 import { deleteFromR2, uploadFileR2 } from "../utils/r2Query"; 22 + import { handlePostNowTask } from "../utils/scheduler"; 24 23 import { FileDeleteSchema } from "../validation/mediaSchema"; 25 24 import { EditSchema } from "../validation/postSchema"; 26 25 ··· 65 64 // Handling posting right now. 66 65 const postInfo: Post|null = await getPostById(c, response.postId); 67 66 if (!isEmpty(postInfo)) { 68 - if (shouldPostNowQueue(c.env)) { 69 - try { 70 - await enqueuePost(c, postInfo!); 71 - } catch(err) { 72 - console.error(err); 73 - return c.json({message: 'Failed to post content, will retry again soon'}, 406); 74 - } 75 - } else { 76 - if (!await makePost(c, postInfo)) 77 - return c.json({message: `Failed to post content, will try again soon.\n\n 78 - If it doesn't post, send a message with this code:\n${postInfo!.postid}`}, 406); 79 - } 67 + if (await handlePostNowTask(c, postInfo!) === false) 68 + return c.json({message: "Unable to post now, will try again during next nearest hour"}, 406); 80 69 return c.json({message: "Created Post!", id: response.postId}); 81 70 } else { 82 71 return c.json({message: "Unable to get post content, post may have been lost"}, 401);
+9 -6
src/index.tsx
··· 16 16 import ResetPassword from "./pages/reset"; 17 17 import Signup from "./pages/signup"; 18 18 import TermsOfService from "./pages/tos"; 19 - import { Bindings, QueueTaskData, QueueTaskType, ScheduledContext } from "./types.d"; 19 + import { Bindings, QueueTaskData, ScheduledContext, TaskType } from "./types.d"; 20 + import { AgentMap } from "./utils/bskyAgents"; 20 21 import { makeConstScript } from "./utils/constScriptGen"; 21 22 import { getAllAbandonedMedia } from "./utils/db/file"; 22 23 import { runMaintenanceUpdates } from "./utils/db/maintain"; ··· 159 160 async queue(batch: MessageBatch<QueueTaskData>, env: Bindings, ctx: ExecutionContext) { 160 161 const runtimeWrapper = new ScheduledContext(env, ctx); 161 162 const delay: number = env.QUEUE_SETTINGS.delay_val; 163 + const agency = new AgentMap(env.TASK_SETTINGS); 162 164 let wasSuccess: boolean = false; 163 165 for (const message of batch.messages) { 166 + const agent = await agency.getOrAddAgentFromObj(runtimeWrapper, message.body.post || message.body.repost, message.body.type); 164 167 switch (message.body.type) { 165 - case QueueTaskType.Post: 166 - wasSuccess = await handlePostTask(runtimeWrapper, message.body.post!, null); 168 + case TaskType.Post: 169 + wasSuccess = await handlePostTask(runtimeWrapper, message.body.post!, agent); 167 170 break; 168 - case QueueTaskType.Repost: 169 - wasSuccess = await handleRepostTask(runtimeWrapper, message.body.repost!, null); 171 + case TaskType.Repost: 172 + wasSuccess = await handleRepostTask(runtimeWrapper, message.body.repost!, agent); 170 173 break; 171 174 default: 172 - case QueueTaskType.None: 175 + case TaskType.None: 173 176 console.error("Got a message queue task type that was invalid"); 174 177 message.ack(); 175 178 return;
+6 -5
src/types.d.ts
··· 35 35 repost_queues: string[]; 36 36 } 37 37 38 - type SiteConfigSettings = { 39 - use_agent_map: boolean; 38 + type AgentConfigSettings = { 39 + use_posts: boolean; 40 + use_reposts: boolean; 40 41 } 41 42 42 43 /** Types, types, types **/ ··· 52 53 INVITE_POOL?: KVNamespace; 53 54 IMAGE_SETTINGS: ImageConfigSettings; 54 55 SIGNUP_SETTINGS: SignupConfigSettings; 55 - SITE_SETTINGS: SiteConfigSettings; 56 + TASK_SETTINGS: AgentConfigSettings; 56 57 R2_SETTINGS: R2ConfigSettings; 57 58 DEFAULT_ADMIN_USER: string; 58 59 DEFAULT_ADMIN_PASS: string; ··· 143 144 scheduleGuid?: string; 144 145 }; 145 146 146 - export enum QueueTaskType { 147 + export enum TaskType { 147 148 None, 148 149 Post, 149 150 Repost, 150 151 }; 151 152 152 153 export type QueueTaskData = { 153 - type: QueueTaskType; 154 + type: TaskType; 154 155 post?: Post; 155 156 repost?: Repost; 156 157 };
+114
src/utils/bskyAgents.ts
··· 1 + // this file is used to handle atpagents and their reuse during cron/queues 2 + // this is done because logging into PDSes across tasks can be extremely 3 + // expensive time wise. 4 + // 5 + // Also just handles general login. 6 + import AtpAgent from "@atproto/api"; 7 + import { AccountStatus, AllContext, LooseObj, Post, Repost, AgentConfigSettings, TaskType } from "../types.d"; 8 + import { getBskyUserPassForId } from "./db/userinfo"; 9 + import { createViolationForUser } from "./db/violations"; 10 + 11 + export const makeAgentForUser = async (c: AllContext, userId: string) => { 12 + const loginCreds = await getBskyUserPassForId(c, userId); 13 + if (loginCreds.valid === false) { 14 + console.error(`credentials for user ${userId} were invalid`); 15 + return null; 16 + } 17 + const {username, password, pds} = loginCreds; 18 + // Login to bsky 19 + const agent = new AtpAgent({ service: new URL(pds) }); 20 + 21 + const loginResponse: AccountStatus = await loginToBsky(agent, username, password); 22 + if (loginResponse != AccountStatus.Ok) { 23 + const addViolation: boolean = await createViolationForUser(c, userId, loginResponse); 24 + if (addViolation) 25 + console.error(`Unable to login to ${userId} with violation ${loginResponse}`); 26 + return null; 27 + } 28 + return agent; 29 + }; 30 + 31 + export const loginToBsky = async (agent: AtpAgent, user: string, pass: string) => { 32 + try { 33 + const loginResponse = await agent.login({ 34 + identifier: user, 35 + password: pass, 36 + allowTakendown: true 37 + }); 38 + if (!loginResponse.success) { 39 + if (loginResponse.data.active == false) { 40 + switch (loginResponse.data.status) { 41 + case "deactivated": 42 + return AccountStatus.Deactivated; 43 + case "suspended": 44 + return AccountStatus.Suspended; 45 + case "takendown": 46 + return AccountStatus.TakenDown; 47 + } 48 + return AccountStatus.InvalidAccount; 49 + } 50 + return AccountStatus.PlatformOutage; 51 + } 52 + return AccountStatus.Ok; 53 + } catch (err) { 54 + // Apparently login can rethrow as an XRPCError and completely eat the original throw. 55 + // so errors don't get handled gracefully. 56 + const errWrap: LooseObj = err as LooseObj; 57 + const errorName = errWrap.constructor.name; 58 + if (errorName === "XRPCError") { 59 + const errCode = errWrap.status; 60 + if (errCode == 401) { 61 + // app password is bad 62 + return AccountStatus.InvalidAccount; 63 + } else if (errCode >= 500) { 64 + return AccountStatus.PlatformOutage; 65 + } 66 + } else if (errorName === "XRPCNotSupported") { 67 + // handle is bad 68 + return AccountStatus.InvalidAccount; 69 + } 70 + console.error(`encountered exception on login for user ${user}, err ${err}`); 71 + } 72 + return AccountStatus.UnhandledError; 73 + }; 74 + 75 + export class AgentMap { 76 + #forPosts: boolean; 77 + #forReposts: boolean; 78 + #map: Map<string, AtpAgent>; 79 + constructor(config: AgentConfigSettings) { 80 + this.#forPosts = config.use_posts; 81 + this.#forReposts = config.use_reposts; 82 + this.#map = new Map(); 83 + } 84 + async getOrAddAgent(c: AllContext, userId: string, type: TaskType): Promise<AtpAgent|null> { 85 + const usesAgent: boolean = this.usesAgentForType(type); 86 + let agent = (usesAgent) ? this.#map.get(userId) || null : null; 87 + if (agent === null) { 88 + agent = await AgentMap.getAgentDirect(c, userId); 89 + if (usesAgent && agent !== null) 90 + this.#map.set(userId, agent); 91 + } 92 + return agent; 93 + }; 94 + async getOrAddAgentFromObj(c: AllContext, data: Post|Repost|undefined|null, type: TaskType): Promise<AtpAgent|null> { 95 + if (data === undefined || data === null) { 96 + return null; 97 + } 98 + const userId: string = (type === TaskType.Post) ? (data as Post).user : (data as Repost).userId; 99 + return await this.getOrAddAgent(c, userId, type); 100 + }; 101 + static async getAgentDirect(c: AllContext, userId: string) { 102 + return await makeAgentForUser(c, userId); 103 + }; 104 + usesAgentForType(type: TaskType) { 105 + switch(type) 106 + { 107 + case TaskType.Post: 108 + return this.#forPosts; 109 + case TaskType.Repost: 110 + return this.#forReposts; 111 + } 112 + return false; 113 + } 114 + };
+11 -80
src/utils/bskyApi.ts
··· 1 1 import { type AppBskyFeedPost, AtpAgent, RichText } from '@atproto/api'; 2 2 import { ResponseType, XRPCError } from '@atproto/xrpc'; 3 - import { Context } from 'hono'; 4 3 import { imageDimensionsFromStream } from 'image-dimensions'; 5 4 import has from 'just-has'; 6 5 import isEmpty from "just-is-empty"; 7 6 import truncate from "just-truncate"; 8 7 import { BSKY_IMG_SIZE_LIMIT, MAX_ALT_TEXT, MAX_EMBEDS_PER_POST } from '../limits'; 9 8 import { 10 - Bindings, BskyEmbedWrapper, BskyRecordWrapper, EmbedData, EmbedDataType, 11 - LooseObj, Post, PostLabel, AccountStatus, 12 - PostRecordResponse, PostStatus, Repost, ScheduledContext, 13 - AllContext 9 + AccountStatus, 10 + AllContext, 11 + BskyEmbedWrapper, BskyRecordWrapper, EmbedData, EmbedDataType, 12 + LooseObj, Post, PostLabel, 13 + PostRecordResponse, PostStatus, Repost 14 14 } from '../types.d'; 15 15 import { atpRecordURI } from '../validation/regexCases'; 16 + import { makeAgentForUser } from './bskyAgents'; 16 17 import { bulkUpdatePostedData, getChildPostsOfThread, isPostAlreadyPosted, setPostNowOffForPost } from './db/data'; 17 - import { getBskyUserPassForId, getUsernameForUserId } from './db/userinfo'; 18 + import { getUsernameForUserId } from './db/userinfo'; 18 19 import { createViolationForUser } from './db/violations'; 19 20 import { deleteEmbedsFromR2 } from './r2Query'; 20 21 ··· 61 62 return "https://bsky.social"; 62 63 }; 63 64 64 - export const loginToBsky = async (agent: AtpAgent, user: string, pass: string) => { 65 - try { 66 - const loginResponse = await agent.login({ 67 - identifier: user, 68 - password: pass, 69 - allowTakendown: true 70 - }); 71 - if (!loginResponse.success) { 72 - if (loginResponse.data.active == false) { 73 - switch (loginResponse.data.status) { 74 - case "deactivated": 75 - return AccountStatus.Deactivated; 76 - case "suspended": 77 - return AccountStatus.Suspended; 78 - case "takendown": 79 - return AccountStatus.TakenDown; 80 - } 81 - return AccountStatus.InvalidAccount; 82 - } 83 - return AccountStatus.PlatformOutage; 84 - } 85 - return AccountStatus.Ok; 86 - } catch (err) { 87 - // Apparently login can rethrow as an XRPCError and completely eat the original throw. 88 - // so errors don't get handled gracefully. 89 - const errWrap: LooseObj = err as LooseObj; 90 - const errorName = errWrap.constructor.name; 91 - if (errorName === "XRPCError") { 92 - const errCode = errWrap.status; 93 - if (errCode == 401) { 94 - // app password is bad 95 - return AccountStatus.InvalidAccount; 96 - } else if (errCode >= 500) { 97 - return AccountStatus.PlatformOutage; 98 - } 99 - } else if (errorName === "XRPCNotSupported") { 100 - // handle is bad 101 - return AccountStatus.InvalidAccount; 102 - } 103 - console.error(`encountered exception on login for user ${user}, err ${err}`); 104 - } 105 - return AccountStatus.UnhandledError; 106 - } 107 - 108 - export const makeAgentForUser = async (c: AllContext, userId: string) => { 109 - const loginCreds = await getBskyUserPassForId(c, userId); 110 - if (loginCreds.valid === false) { 111 - console.error(`credentials for user ${userId} were invalid`); 112 - return null; 113 - } 114 - const {username, password, pds} = loginCreds; 115 - // Login to bsky 116 - const agent = new AtpAgent({ service: new URL(pds) }); 117 - 118 - const loginResponse: AccountStatus = await loginToBsky(agent, username, password); 119 - if (loginResponse != AccountStatus.Ok) { 120 - const addViolation: boolean = await createViolationForUser(c, userId, loginResponse); 121 - if (addViolation) 122 - console.error(`Unable to login to ${userId} with violation ${loginResponse}`); 123 - return null; 124 - } 125 - return agent; 126 - } 127 - 128 - export const makePost = async (c: AllContext, content: Post|null, usingAgent: AtpAgent|null=null) => { 65 + export const makePost = async (c: AllContext, content: Post|null, usingAgent: AtpAgent) => { 129 66 if (content === null) { 130 67 console.warn("Dropping invocation of makePost, content was null"); 131 68 return false; ··· 138 75 return true; 139 76 } 140 77 141 - const agent: AtpAgent|null = (usingAgent === null) ? await makeAgentForUser(c, content.user) : usingAgent; 142 - if (agent === null) { 143 - console.warn(`could not make agent for post ${content.postid}`); 144 - return false; 145 - } 146 - 147 - const newPostRecords: PostStatus|null = await makePostRaw(c, content, agent); 78 + const newPostRecords: PostStatus|null = await makePostRaw(c, content, usingAgent); 148 79 if (newPostRecords !== null) { 149 80 await bulkUpdatePostedData(c, newPostRecords.records, newPostRecords.expected == newPostRecords.got); 150 81 ··· 169 100 return false; 170 101 } 171 102 172 - export const makeRepost = async (c: AllContext, content: Repost, usingAgent: AtpAgent|null=null) => { 103 + export const makeRepost = async (c: AllContext, content: Repost, usingAgent: AtpAgent) => { 173 104 let bWasSuccess = true; 174 105 const agent: AtpAgent|null = (usingAgent === null) ? await makeAgentForUser(c, content.userId) : usingAgent; 175 106 if (agent === null) { ··· 195 126 return bWasSuccess; 196 127 }; 197 128 198 - export const makePostRaw = async (c: AllContext, content: Post, agent: AtpAgent): Promise<PostStatus|null> => { 129 + const makePostRaw = async (c: AllContext, content: Post, agent: AtpAgent): Promise<PostStatus|null> => { 199 130 const username = await getUsernameForUserId(c, content.user); 200 131 // incredibly unlikely but we'll handle it 201 132 if (username === null) {
+1 -1
src/utils/bskyMsg.ts
··· 1 1 import { AtpAgent, RichText } from '@atproto/api'; 2 2 import { Bindings, AccountStatus } from '../types.d'; 3 - import { loginToBsky } from './bskyApi'; 3 + import { loginToBsky } from './bskyAgents'; 4 4 5 5 const chatHeaders = {headers: { 6 6 "atproto-proxy": "did:web:api.bsky.chat#bsky_chat"
+3 -3
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, QueueTaskType, Repost } from "../types.d"; 4 + import { AllContext, Bindings, Post, QueueTaskData, Repost, TaskType } from "../types.d"; 5 5 6 6 const queueContentType = 'v8'; 7 7 ··· 35 35 const queueConsumer: Queue|null = getRandomQueue(c.env, "post_queues"); 36 36 37 37 if (queueConsumer !== null) { 38 - await queueConsumer.send({type: QueueTaskType.Post, post: post} as QueueTaskData, { contentType: queueContentType }); 38 + await queueConsumer.send({type: TaskType.Post, post: post} as QueueTaskData, { contentType: queueContentType }); 39 39 } 40 40 } 41 41 ··· 46 46 // Pick a random consumer to handle this repost 47 47 const queueConsumer: Queue|null = getRandomQueue(c.env, "repost_queues"); 48 48 if (queueConsumer !== null) 49 - await queueConsumer.send({type: QueueTaskType.Repost, repost: post} as QueueTaskData, { contentType: queueContentType }); 49 + await queueConsumer.send({type: TaskType.Repost, repost: post} as QueueTaskData, { contentType: queueContentType }); 50 50 }
+43 -24
src/utils/scheduler.ts
··· 1 1 import AtpAgent from '@atproto/api'; 2 2 import isEmpty from 'just-is-empty'; 3 - import { AllContext, Post, Repost } from '../types.d'; 4 - import { makeAgentForUser, makePost, makeRepost } from './bskyApi'; 3 + import { AllContext, Post, Repost, TaskType } from '../types.d'; 4 + import { AgentMap } from './bskyAgents'; 5 + import { makePost, makeRepost } from './bskyApi'; 5 6 import { pruneBskyPosts } from './bskyPrune'; 6 7 import { 7 8 deleteAllRepostsBeforeCurrentTime, deletePosts, getAllPostsForCurrentTime, 8 - getAllRepostsForCurrentTime, purgePostedPosts 9 + getAllRepostsForCurrentTime, purgePostedPosts, 10 + setPostNowOffForPost 9 11 } from './db/data'; 10 12 import { getAllAbandonedMedia } from './db/file'; 11 - import { enqueuePost, enqueueRepost, isQueueEnabled, isRepostQueueEnabled, shouldPostThreadQueue } from './queuePublisher'; 13 + import { enqueuePost, enqueueRepost, isQueueEnabled, isRepostQueueEnabled, shouldPostNowQueue, shouldPostThreadQueue } from './queuePublisher'; 12 14 import { deleteFromR2 } from './r2Query'; 13 15 14 16 export const handlePostTask = async(runtime: AllContext, postData: Post, agent: AtpAgent|null) => { 17 + if (agent === null) { 18 + console.error(`Unable to make agent to post ${postData.postid}`); 19 + return false; 20 + } 15 21 const madePost = await makePost(runtime, postData, agent); 16 22 if (madePost) { 17 23 console.log(`Made post ${postData.postid} successfully`); ··· 20 26 } 21 27 return madePost; 22 28 } 29 + 30 + export const handlePostNowTask = async(c: AllContext, postData: Post) => { 31 + let postStatus = false; 32 + if (shouldPostNowQueue(c.env)) { 33 + try { 34 + await enqueuePost(c, postData); 35 + postStatus = true; 36 + } catch(err) { 37 + console.error(`Post now queue for ${postData.postid} got error: ${err}`); 38 + postStatus = false; 39 + } 40 + } else { 41 + const agent = await AgentMap.getAgentDirect(c, postData.user); 42 + if (agent === null) { 43 + console.error(`unable to get agent for user ${postData.user} to post now`); 44 + postStatus = false; 45 + } else { 46 + postStatus = await makePost(c, postData, agent); 47 + } 48 + } 49 + if (postStatus === false) 50 + c.executionCtx.waitUntil(setPostNowOffForPost(c, postData.postid)); 51 + 52 + return postStatus; 53 + }; 54 + 23 55 export const handleRepostTask = async(c: AllContext, postData: Repost, agent: AtpAgent|null) => { 56 + if (agent === null) { 57 + console.error(`Unable to make agent to post ${postData.postid}`); 58 + return false; 59 + } 24 60 const madeRepost = await makeRepost(c, postData, agent); 25 61 if (madeRepost) { 26 62 console.log(`Reposted ${postData.uri} successfully!`); ··· 36 72 const queueEnabled: boolean = isQueueEnabled(c.env); 37 73 const repostQueueEnabled: boolean = isRepostQueueEnabled(c.env); 38 74 const threadQueueEnabled: boolean = shouldPostThreadQueue(c.env); 39 - // Temporary cache of agents to make handling actions much better and easier. 40 - // The only potential downside is if we run hot on RAM with a lot of users. Before, the agents would 41 - // get freed up as a part of exiting their cycle, but this would make that worse... 42 - // 43 - // TODO: bunching as a part of queues, literally just throw an agent at a queue with instructions and go. 44 - // this requires queueing to be working properly. 45 - const AgentList = new Map(); 46 - const usesAgentMap: boolean = c.env.SITE_SETTINGS.use_agent_map; 75 + const agency = new AgentMap(c.env.TASK_SETTINGS); 47 76 48 77 // Push any posts 49 78 if (!isEmpty(scheduledPosts)) { ··· 52 81 if (queueEnabled || (post.isThreadRoot && threadQueueEnabled)) { 53 82 await enqueuePost(c, post); 54 83 } else { 55 - let agent = (usesAgentMap) ? AgentList.get(post.user) || null : null; 56 - if (agent === null) { 57 - agent = await makeAgentForUser(c, post.user); 58 - if (usesAgentMap) 59 - AgentList.set(post.user, agent); 60 - } 84 + let agent = await agency.getOrAddAgent(c, post.user, TaskType.Post); 61 85 c.executionCtx.waitUntil(handlePostTask(c, post, agent)); 62 86 } 63 87 } ··· 70 94 console.log(`handling ${scheduledReposts.length} reposts`); 71 95 for (const repost of scheduledReposts) { 72 96 if (!repostQueueEnabled) { 73 - let agent = (usesAgentMap) ? AgentList.get(repost.userId) || null : null; 74 - if (agent === null) { 75 - agent = await makeAgentForUser(c, repost.userId); 76 - if (usesAgentMap) 77 - AgentList.set(repost.userId, agent); 78 - } 97 + let agent = await agency.getOrAddAgent(c, repost.userId, TaskType.Repost); 79 98 c.executionCtx.waitUntil(handleRepostTask(c, repost, agent)); 80 99 } else { 81 100 await enqueueRepost(c, repost);
+3 -3
src/wrangler.d.ts
··· 1 1 /* eslint-disable */ 2 - // Generated by Wrangler by running `wrangler types src/wrangler.d.ts` (hash: 5b57fedc6b0faf9554443e5819d28365) 2 + // Generated by Wrangler by running `wrangler types src/wrangler.d.ts` (hash: 55b4346bed1553dbff2b8c9d511b4b56) 3 3 // Runtime types generated with workerd@1.20260212.0 2024-12-13 nodejs_compat 4 4 declare namespace Cloudflare { 5 5 interface GlobalProps { ··· 15 15 QUEUE_SETTINGS: {"enabled":false,"repostsEnabled":false,"postNowEnabled":false,"threadEnabled":true,"delay_val":100,"post_queues":["POST_QUEUE"],"repost_queues":[]}; 16 16 REDIRECTS: {"contact":"https://bsky.app/profile/skyscheduler.work","tip":"https://ko-fi.com/socksthewolf/tip"}; 17 17 R2_SETTINGS: {"auto_prune":false,"prune_days":3}; 18 - SITE_SETTINGS: {"use_agent_map":false}; 18 + TASK_SETTINGS: {"use_posts":false,"use_reposts":false}; 19 19 BETTER_AUTH_SECRET: string; 20 20 BETTER_AUTH_URL: string; 21 21 DEFAULT_ADMIN_USER: string; ··· 49 49 QUEUE_SETTINGS: {"enabled":false,"repostsEnabled":false,"postNowEnabled":false,"threadEnabled":true,"delay_val":100,"post_queues":["POST_QUEUE"],"repost_queues":[]} | {"enabled":true,"repostsEnabled":true,"postNowEnabled":false,"threadEnabled":true,"delay_val":100,"post_queues":["POST_QUEUE"],"repost_queues":["REPOST_QUEUE"]}; 50 50 REDIRECTS: {"contact":"https://bsky.app/profile/skyscheduler.work","tip":"https://ko-fi.com/socksthewolf/tip"}; 51 51 R2_SETTINGS: {"auto_prune":false,"prune_days":3} | {"auto_prune":true,"prune_days":3}; 52 - SITE_SETTINGS: {"use_agent_map":false}; 52 + TASK_SETTINGS: {"use_posts":false,"use_reposts":false}; 53 53 INVITE_POOL?: KVNamespace; 54 54 R2RESIZE?: R2Bucket; 55 55 DBStaging?: D1Database;
+3 -3
wrangler.toml
··· 104 104 # if we should be removing abandoned files from R2 storage 105 105 R2_SETTINGS={auto_prune=true, prune_days=3} 106 106 107 - # various site settings 108 - SITE_SETTINGS={use_agent_map=false} 107 + # settings about tasks 108 + TASK_SETTINGS={use_posts=true, use_reposts=true} 109 109 110 110 # set this to true in your .dev.vars to turn off turnstile 111 111 IN_DEV=false ··· 120 120 QUEUE_SETTINGS = {enabled=false, repostsEnabled=false, postNowEnabled=false, threadEnabled=true, delay_val=100, post_queues=["POST_QUEUE"], repost_queues=[]} 121 121 REDIRECTS = {contact="https://bsky.app/profile/skyscheduler.work", tip="https://ko-fi.com/socksthewolf/tip"} 122 122 R2_SETTINGS={auto_prune=false, prune_days=3} 123 - SITE_SETTINGS={use_agent_map=false} 123 + TASK_SETTINGS={use_posts=true, use_reposts=true} 124 124 IN_DEV=true 125 125 126 126 [[env.staging.d1_databases]]