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

add special resize operations

+101 -42
-1
assets/css/dashboard.css
··· 1 /** SIDEBAR **/ 2 - 3 .postItemHeader { 4 min-height: 50px; 5 button:has(+ button) {
··· 1 /** SIDEBAR **/ 2 .postItemHeader { 3 min-height: 50px; 4 button:has(+ button) {
+4
assets/css/stylesheet.css
··· 67 [data-tooltip]:before, .pico [data-tooltip]:before { 68 white-space: preserve-breaks !important; 69 }
··· 67 [data-tooltip]:before, .pico [data-tooltip]:before { 68 white-space: preserve-breaks !important; 69 } 70 + 71 + .toastify-avatar { 72 + margin-left: 0px; 73 + }
+1
assets/icons/success.svg
···
··· 1 + <svg fill="#ffffff" width="24px" height="24px" viewBox="-2.04 -2.04 24.48 24.48" xmlns="http://www.w3.org/2000/svg" class="cf-icon-svg" stroke="#ffffff" stroke-width="0.00020400000000000003" transform="rotate(0)matrix(1, 0, 0, 1, 0, 0)"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" stroke="#CCCCCC" stroke-width="0.040799999999999996"></g><g id="SVGRepo_iconCarrier"><path d="M16.417 10.283A7.917 7.917 0 1 1 8.5 2.366a7.916 7.916 0 0 1 7.917 7.917zm-4.105-4.498a.791.791 0 0 0-1.082.29l-3.828 6.63-1.733-2.08a.791.791 0 1 0-1.216 1.014l2.459 2.952a.792.792 0 0 0 .608.285.83.83 0 0 0 .068-.003.791.791 0 0 0 .618-.393L12.6 6.866a.791.791 0 0 0-.29-1.081z"></path></g></svg>
+8
assets/icons/warning.svg
···
··· 1 + <svg width="30px" height="30px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 + <g id="SVGRepo_bgCarrier" stroke-width="0"></g> 3 + <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g> 4 + <g id="SVGRepo_iconCarrier"> 5 + <path d="M12 7V13M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> 6 + <circle cx="12" cy="16.5" r="1" fill="#ffffff"></circle> 7 + </g> 8 + </svg>
+12 -3
assets/js/main.js
··· 1 /* Functions that can be used anywhere on the website. */ 2 function pushToast(msg, isSuccess) { 3 - Toastify({ 4 text: msg, 5 duration: !isSuccess ? 10000 : Toastify.defaults.duration, 6 style: { 7 background: isSuccess ? 'green' : 'red' 8 - } 9 - }).showToast(); 10 } 11 12 function scrollTop() {
··· 1 /* Functions that can be used anywhere on the website. */ 2 function pushToast(msg, isSuccess) { 3 + var newToast = Toastify({ 4 text: msg, 5 + stopOnFocus: false, 6 + ariaLive: true, 7 + avatar: !isSuccess ? "/icons/warning.svg" : "/icons/success.svg", 8 duration: !isSuccess ? 10000 : Toastify.defaults.duration, 9 style: { 10 + "padding-left": "12px", 11 background: isSuccess ? 'green' : 'red' 12 + }, 13 + close: false, 14 + onClick: () => { 15 + newToast.hideToast(); 16 + }, 17 + }); 18 + newToast.showToast(); 19 } 20 21 function scrollTop() {
+37 -19
assets/js/postHelper.js
··· 10 const postFormTitle = document.getElementById('postFormTitle') 11 let hasFileLimit = false; 12 let fileData = new Map(); 13 14 /* Sections for handling UI changes and modifications */ 15 const sectionRetweet = document.getElementById('section-retweet'); ··· 20 function addOnUnloadBlocker() { 21 window.onbeforeunload = function() { 22 document.querySelectorAll(".fileDel").forEach((el) => {el.click();}); 23 return undefined; 24 } 25 } 26 function clearOnUnloadBlocker() { 27 window.onbeforeunload = null; 28 } 29 30 let fileDropzone = new Dropzone("#fileUploads", { 31 url: "/post/upload", 32 autoProcessQueue: true, 33 - /* We process this ourselves */ 34 addRemoveLinks: false, 35 maxFiles: FILE_DROP_MAX_FILES, 36 dictMaxFilesExceeded: "max files", ··· 80 fileDropzone.removeAllFiles(); 81 // Clear the file data map 82 fileData.clear(); 83 }); 84 85 fileDropzone.on("reset", () => { 86 hasFileLimit = false; 87 clearOnUnloadBlocker(); 88 showContentLabeler(false); 89 setElementVisible(sectionLinkAttach, true); ··· 95 pushToast("Maximum number of files reached", false); 96 return; 97 } 98 setElementVisible(sectionLinkAttach, false); 99 const buttonHolder = Dropzone.createElement("<fieldset role='group' class='file-item-group'></fieldset>"); 100 const removeButton = Dropzone.createElement("<button class='fileDel outline btn-error' disabled><small>Remove file</small></button>"); ··· 148 fileDropzone.on("success", function(file, response) { 149 const deleteFileOnError = () => { 150 const delButton = file.previewElement.querySelectorAll(".fileDel")[0]; 151 delButton.setAttribute("bad", true); 152 delButton.click(); 153 }; 154 // show the labels 155 showContentLabeler(true); 156 const fileIsImage = imageTypes.includes(file.type); ··· 170 videoTag.setAttribute("src", videoObjectURL); 171 videoTag.addEventListener("loadeddata", () => { 172 const videoDuration = videoTag.duration; 173 - if (videoDuration > MAX_VIDEO_LENGTH) { 174 - pushToast(`${file.name} is too long for bsky by ${(videoDuration - MAX_VIDEO_LENGTH).toFixed(2)} seconds`, false); 175 - deleteFileOnError(); 176 - } else { 177 - fileData.set(file.name, {content: response.data, type: 3, 178 height: videoTag.videoHeight, width: videoTag.videoWidth, duration: videoDuration }); 179 hasFileLimit = true; 180 } ··· 196 let duration = 0; 197 try { 198 for (let i = 0, len = uint8.length; i < len; i++) { 199 - if (uint8[i] == 0x21 200 - && uint8[i + 1] == 0xF9 201 - && uint8[i + 2] == 0x04 202 - && uint8[i + 7] == 0x00) 203 { 204 const delay = (uint8[i + 5] << 8) | (uint8[i + 4] & 0xFF) 205 duration += delay < 2 ? 10 : delay ··· 224 if (videoDuration === null) { 225 pushToast(`${file.name} duration could not be processed`, false); 226 deleteFileOnError(); 227 - } else if (videoDuration > MAX_VIDEO_LENGTH) { 228 - pushToast(`${file.name} is over the maximum video duration by ${(videoDuration - MAX_VIDEO_LENGTH).toFixed(2)} seconds`, false); 229 - deleteFileOnError(); 230 - } else if (videoDuration >= MAX_GIF_LENGTH) { 231 - pushToast(`${file.name} is over the maximum length for a gif by ${(videoDuration - MAX_GIF_LENGTH).toFixed(2)} seconds`, false); 232 - deleteFileOnError(); 233 } else { 234 - fileData.set(file.name, { content: response.data, type: 3, height: imageHeight, width: imageWidth, duration: videoDuration }); 235 hasFileLimit = true; 236 } 237 }; 238 // Force the file to load. 239 imgObj.src = gifImgURL; 240 } else { 241 - fileData.set(file.name, {content: response.data, type: 1}); 242 } 243 244 // Make the buttons pressable ··· 280 pushToast(`Error: ${file.name} had an unexpected error`, false); 281 } 282 283 fileDropzone.removeFile(file); 284 if (fileData.length == 0) { 285 setElementVisible(sectionLinkAttach, true); ··· 288 289 fileDropzone.on("uploadprogress", function(file, progress, bytesSent) { 290 const progressObject = file.previewElement.querySelector(".dz-upload"); 291 - progressObject.innerHTML = `${progress}%`; 292 if ((progress === 100 || bytesSent == file.size) && progressObject) { 293 progressObject.innerHTML = "Processing...please wait. This may take a bit!"; 294 } ··· 301 // Handle form submission 302 postForm.addEventListener('submit', async (e) => { 303 e.preventDefault(); 304 showPostProgress(true); 305 const contentVal = content.value; 306 const postNow = postNowCheckbox.checked;
··· 10 const postFormTitle = document.getElementById('postFormTitle') 11 let hasFileLimit = false; 12 let fileData = new Map(); 13 + let waitingFiles = 0; 14 15 /* Sections for handling UI changes and modifications */ 16 const sectionRetweet = document.getElementById('section-retweet'); ··· 21 function addOnUnloadBlocker() { 22 window.onbeforeunload = function() { 23 document.querySelectorAll(".fileDel").forEach((el) => {el.click();}); 24 + // only way to get the alert box to not show up 25 return undefined; 26 } 27 } 28 function clearOnUnloadBlocker() { 29 window.onbeforeunload = null; 30 + } 31 + 32 + function setFileData(fileName, data) { 33 + fileData.set(fileName, data); 34 + --waitingFiles; 35 } 36 37 let fileDropzone = new Dropzone("#fileUploads", { 38 url: "/post/upload", 39 autoProcessQueue: true, 40 + /* We add remove links ourselves */ 41 addRemoveLinks: false, 42 maxFiles: FILE_DROP_MAX_FILES, 43 dictMaxFilesExceeded: "max files", ··· 87 fileDropzone.removeAllFiles(); 88 // Clear the file data map 89 fileData.clear(); 90 + waitingFiles = 0; 91 }); 92 93 fileDropzone.on("reset", () => { 94 hasFileLimit = false; 95 + waitingFiles = 0; 96 clearOnUnloadBlocker(); 97 showContentLabeler(false); 98 setElementVisible(sectionLinkAttach, true); ··· 104 pushToast("Maximum number of files reached", false); 105 return; 106 } 107 + // Increase the number of waiting to be processed files. 108 + ++waitingFiles; 109 setElementVisible(sectionLinkAttach, false); 110 const buttonHolder = Dropzone.createElement("<fieldset role='group' class='file-item-group'></fieldset>"); 111 const removeButton = Dropzone.createElement("<button class='fileDel outline btn-error' disabled><small>Remove file</small></button>"); ··· 159 fileDropzone.on("success", function(file, response) { 160 const deleteFileOnError = () => { 161 const delButton = file.previewElement.querySelectorAll(".fileDel")[0]; 162 + --waitingFiles; 163 delButton.setAttribute("bad", true); 164 delButton.click(); 165 }; 166 + const deleteFileIfLengthOver = (length, max) => { 167 + if (length > max) { 168 + pushToast(`${file.name} is over the max duration by ${(length - max).toFixed(2)} seconds`, false); 169 + deleteFileOnError(); 170 + return true; 171 + } 172 + return false; 173 + } 174 // show the labels 175 showContentLabeler(true); 176 const fileIsImage = imageTypes.includes(file.type); ··· 190 videoTag.setAttribute("src", videoObjectURL); 191 videoTag.addEventListener("loadeddata", () => { 192 const videoDuration = videoTag.duration; 193 + if (!deleteFileIfLengthOver(videoDuration, MAX_VIDEO_LENGTH)) { 194 + setFileData(file.name, {content: response.data, type: 3, 195 height: videoTag.videoHeight, width: videoTag.videoWidth, duration: videoDuration }); 196 hasFileLimit = true; 197 } ··· 213 let duration = 0; 214 try { 215 for (let i = 0, len = uint8.length; i < len; i++) { 216 + if (uint8[i] == 0x21 && uint8[i + 1] == 0xF9 217 + && uint8[i + 2] == 0x04 && uint8[i + 7] == 0x00) 218 { 219 const delay = (uint8[i + 5] << 8) | (uint8[i + 4] & 0xFF) 220 duration += delay < 2 ? 10 : delay ··· 239 if (videoDuration === null) { 240 pushToast(`${file.name} duration could not be processed`, false); 241 deleteFileOnError(); 242 + } else if (deleteFileIfLengthOver(videoDuration, MAX_GIF_LENGTH)) { 243 + return; 244 } else { 245 + setFileData(file.name, { content: response.data, type: 3, height: imageHeight, width: imageWidth, duration: videoDuration }); 246 hasFileLimit = true; 247 } 248 }; 249 // Force the file to load. 250 imgObj.src = gifImgURL; 251 } else { 252 + setFileData(file.name, {content: response.data, type: 1}); 253 } 254 255 // Make the buttons pressable ··· 291 pushToast(`Error: ${file.name} had an unexpected error`, false); 292 } 293 294 + // file failed to upload, so drop the value here too 295 + --waitingFiles; 296 fileDropzone.removeFile(file); 297 if (fileData.length == 0) { 298 setElementVisible(sectionLinkAttach, true); ··· 301 302 fileDropzone.on("uploadprogress", function(file, progress, bytesSent) { 303 const progressObject = file.previewElement.querySelector(".dz-upload"); 304 + progressObject.innerHTML = `${progress.toFixed(2)}%`; 305 if ((progress === 100 || bytesSent == file.size) && progressObject) { 306 progressObject.innerHTML = "Processing...please wait. This may take a bit!"; 307 } ··· 314 // Handle form submission 315 postForm.addEventListener('submit', async (e) => { 316 e.preventDefault(); 317 + // prevent submissions if we're currently uploading files. 318 + if (waitingFiles > 0) { 319 + pushToast("Files are currently pending upload, please wait...", false); 320 + return; 321 + } 322 showPostProgress(true); 323 const contentVal = content.value; 324 const postNow = postNowCheckbox.checked;
+12 -5
src/layout/makePost.tsx
··· 26 {type: "script", href: "/dep/tribute.min.js"} 27 ]; 28 29 - export function PostCreation() { 30 const bskyImageLimits = `Max file size of ${BSKY_IMG_SIZE_LIMIT_IN_MB}MB`; 31 return ( 32 <section> ··· 63 <li><span data-tooltip={BSKY_IMG_FILE_EXTS}>Images</span>: 64 <ul> 65 <li>must be less than {CF_IMAGES_MAX_DIMENSION}x{CF_IMAGES_MAX_DIMENSION} pixels</li> 66 - <li>must have a file size smaller than {CF_IMAGES_FILE_SIZE_LIMIT_IN_MB}MB ({APP_NAME} will attempt to compress images to fit <span data-tooltip={bskyImageLimits}>BlueSky's requirements</span>)</li> 67 <li>thumbnails will only be shown here for images that are smaller than {MAX_THUMBNAIL_SIZE}MB</li> 68 - <li>don't upload and fail, it's recommended to use a lower resolution file instead</li> 69 </ul></li> 70 <li><span data-tooltip={BSKY_VIDEO_FILE_EXTS}>Videos</span>: 71 <ul> 72 <li>must be shorter than {BSKY_VIDEO_MAX_DURATION} minutes</li> 73 <li>must be smaller than {R2_FILE_SIZE_LIMIT_IN_MB}MB</li> 74 - <li>will be processed on BSky after they're posted. This may show a temporary "Video not Found"/black screen for a bit after posting.</li> 75 </ul></li> 76 </ul></small></div> 77 </footer> ··· 95 <section> 96 <article> 97 <header>Insert Post/Feed/List Link</header> 98 - <input id="recordBox" placeholder="https://" title="Must be a link to a ATProto powered record" /> 99 <small>Posts must be quotable and all record types must exist upon the scheduled time. If it does not exist, it will not be attached to your post.</small> 100 </article> 101 </section>
··· 26 {type: "script", href: "/dep/tribute.min.js"} 27 ]; 28 29 + export function PostCreation({ctx}: any) { 30 + const maxWidth: number|undefined = ctx.env.IMAGE_SETTINGS.max_width; 31 const bskyImageLimits = `Max file size of ${BSKY_IMG_SIZE_LIMIT_IN_MB}MB`; 32 return ( 33 <section> ··· 64 <li><span data-tooltip={BSKY_IMG_FILE_EXTS}>Images</span>: 65 <ul> 66 <li>must be less than {CF_IMAGES_MAX_DIMENSION}x{CF_IMAGES_MAX_DIMENSION} pixels</li> 67 + <li>must have a file size smaller than {CF_IMAGES_FILE_SIZE_LIMIT_IN_MB}MB ({APP_NAME} will attempt to compress images to fit <span data-tooltip={bskyImageLimits}>BlueSky's requirements</span>) 68 + {maxWidth ? 69 + <ol> 70 + <li>images over {BSKY_IMG_SIZE_LIMIT_IN_MB}MB with a width greater than <b>{maxWidth}px</b> will also <u data-tooltip="will preserve aspect ratio">be resized</u> in addition to being compressed</li> 71 + </ol> : null} 72 + </li> 73 + 74 <li>thumbnails will only be shown here for images that are smaller than {MAX_THUMBNAIL_SIZE}MB</li> 75 + <li>if an image fails to upload, you'll need to manually adjust the file to fit it properly</li> 76 </ul></li> 77 <li><span data-tooltip={BSKY_VIDEO_FILE_EXTS}>Videos</span>: 78 <ul> 79 <li>must be shorter than {BSKY_VIDEO_MAX_DURATION} minutes</li> 80 <li>must be smaller than {R2_FILE_SIZE_LIMIT_IN_MB}MB</li> 81 + <li>will be processed on your PDS after they're posted. This may show a temporary <i>"Video not Found"</i> message for a bit after posting.</li> 82 </ul></li> 83 </ul></small></div> 84 </footer> ··· 102 <section> 103 <article> 104 <header>Insert Post/Feed/List Link</header> 105 + <input id="recordBox" placeholder="https://" title="Must be a link to a ATProto based record" /> 106 <small>Posts must be quotable and all record types must exist upon the scheduled time. If it does not exist, it will not be attached to your post.</small> 107 </article> 108 </section>
+2 -2
src/layout/settings.tsx
··· 38 <label> 39 BSky App Password: 40 <BSkyAppPasswordField /> 41 - <small>If you need to change your bsky application password, you can 42 - <a href="https://bsky.app/settings/app-passwords" target="_blank">get a new one here</a>.</small> 43 </label> 44 <label> 45 BSky PDS:
··· 38 <label> 39 BSky App Password: 40 <BSkyAppPasswordField /> 41 + <small>If you need to change your bsky application password, you can&nbsp; 42 + <a href="https://bsky.app/settings/app-passwords" target="_blank">get a new one here</a>.</small> 43 </label> 44 <label> 45 BSky PDS:
+1 -1
src/pages/dashboard.tsx
··· 82 <button id="tabone" class="w-half" role="tab" aria-selected="true" aria-controls="postTab">New Post</button> 83 <button id="tabtwo" class="w-half" role="tab" aria-controls="repostTab">New Retweet</button> 84 <div id="postTab" role="tabpanel" aria-labelledby="tabone" > 85 - <PostCreation /> 86 </div> 87 <div id="repostTab" role="tabpanel" aria-labelledby="tabtwo" hidden> 88 <MakeRetweet />
··· 82 <button id="tabone" class="w-half" role="tab" aria-selected="true" aria-controls="postTab">New Post</button> 83 <button id="tabtwo" class="w-half" role="tab" aria-controls="repostTab">New Retweet</button> 84 <div id="postTab" role="tabpanel" aria-labelledby="tabone" > 85 + <PostCreation ctx={ctx} /> 86 </div> 87 <div id="repostTab" role="tabpanel" aria-labelledby="tabtwo" hidden> 88 <MakeRetweet />
+2 -2
src/types.ts
··· 7 enabled: boolean; 8 steps?: number[]; 9 bucket_url?: string; 10 }; 11 12 type SignupConfigSettings = { ··· 169 href: string; 170 }; 171 172 - export type AllContext = Context|ScheduledContext; 173 - 174 export type BskyEmbedWrapper = { 175 type: EmbedDataType; 176 data?: any; ··· 203 date: Date 204 } 205 206 export type BatchQuery = [BatchItem<'sqlite'>, ...BatchItem<'sqlite'>[]];
··· 7 enabled: boolean; 8 steps?: number[]; 9 bucket_url?: string; 10 + max_width?: number; 11 }; 12 13 type SignupConfigSettings = { ··· 170 href: string; 171 }; 172 173 export type BskyEmbedWrapper = { 174 type: EmbedDataType; 175 data?: any; ··· 202 date: Date 203 } 204 205 + export type AllContext = Context|ScheduledContext; 206 export type BatchQuery = [BatchItem<'sqlite'>, ...BatchItem<'sqlite'>[]];
+1 -1
src/utils/appScripts.ts
··· 1 // Change this value to break out of any caching that might be happening 2 // for the runtime scripts (ex: main.js & postHelper.js) 3 - export const CURRENT_SCRIPT_VERSION: string = "1.5.3"; 4 5 export const getAppScriptStr = (scriptName: string) => `/js/${scriptName}.min.js?v=${CURRENT_SCRIPT_VERSION}`; 6
··· 1 // Change this value to break out of any caching that might be happening 2 // for the runtime scripts (ex: main.js & postHelper.js) 3 + export const CURRENT_SCRIPT_VERSION: string = "1.5.4"; 4 5 export const getAppScriptStr = (scriptName: string) => `/js/${scriptName}.min.js?v=${CURRENT_SCRIPT_VERSION}`; 6
+19 -7
src/utils/r2Query.ts
··· 108 let failedToResize = true; 109 110 if (c.env.IMAGE_SETTINGS.enabled) { 111 const resizeFilename = uuidv4(); 112 const resizeBucketPush = await c.env.R2RESIZE.put(resizeFilename, await file.bytes(), { 113 customMetadata: {"user": userId }, ··· 116 117 if (!resizeBucketPush) { 118 console.error(`Failed to push ${file.name} to the resizing bucket`); 119 - return {"success": false, "error": "unable to handle image for resize, please make it smaller"} 120 } 121 122 - // TODO: use the image wrangler binding 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!), { 126 headers: { 127 "x-skyscheduler-helper": c.env.RESIZE_SECRET_HEADER ··· 129 cf: { 130 image: { 131 quality: qualityLevel, 132 - metadata: "copyright", 133 - format: "jpeg" 134 } 135 } 136 }); ··· 138 const resizedHeader = response.headers.get("Cf-Resized"); 139 const returnType = response.headers.get("Content-Type") || ""; 140 const transformFileSize: number = Number(response.headers.get("Content-Length")) || 0; 141 - const resizeHadError = resizedHeader === null || resizedHeader.indexOf("err=") !== -1; 142 143 if (!resizeHadError && BSKY_IMG_MIME_TYPES.includes(returnType)) { 144 console.log(`Attempting quality level ${qualityLevel}% for ${originalName}, size: ${transformFileSize}`); ··· 174 175 if (failedToResize) { 176 const fileSizeOverAmount: string = ((file.size - BSKY_IMG_SIZE_LIMIT)/MB_TO_BYTES).toFixed(2); 177 - return {"success": false, "originalName": originalName, "error": `Image is too large for bsky, over by ${fileSizeOverAmount}MB`}; 178 } 179 } 180
··· 108 let failedToResize = true; 109 110 if (c.env.IMAGE_SETTINGS.enabled) { 111 + // Randomly generated id to be used during the resize process 112 const resizeFilename = uuidv4(); 113 const resizeBucketPush = await c.env.R2RESIZE.put(resizeFilename, await file.bytes(), { 114 customMetadata: {"user": userId }, ··· 117 118 if (!resizeBucketPush) { 119 console.error(`Failed to push ${file.name} to the resizing bucket`); 120 + return {"success": false, "error": "resize process ran out of disk space, you'll need to resize the image or try again"}; 121 } 122 123 + // Default image rules for resizing an image 124 + const imageRules: RequestInitCfPropertiesImage = { 125 + fit: "scale-down", 126 + metadata: "copyright", 127 + format: "jpeg", 128 + }; 129 + 130 + // if the application is to also resize the width automatically, do so here. 131 + // this will preserve aspect ratio 132 + if (c.env.IMAGE_SETTINGS.max_width) { 133 + imageRules.width = c.env.IMAGE_SETTINGS.max_width; 134 + } 135 + 136 for (var i = 0; i < c.env.IMAGE_SETTINGS.steps.length; ++i) { 137 + const qualityLevel: number = c.env.IMAGE_SETTINGS.steps[i]; 138 const response = await fetch(new URL(resizeFilename, c.env.IMAGE_SETTINGS.bucket_url!), { 139 headers: { 140 "x-skyscheduler-helper": c.env.RESIZE_SECRET_HEADER ··· 142 cf: { 143 image: { 144 quality: qualityLevel, 145 + ...imageRules 146 } 147 } 148 }); ··· 150 const resizedHeader = response.headers.get("Cf-Resized"); 151 const returnType = response.headers.get("Content-Type") || ""; 152 const transformFileSize: number = Number(response.headers.get("Content-Length")) || 0; 153 + const resizeHadError: boolean = (resizedHeader === null || resizedHeader.indexOf("err=") !== -1); 154 155 if (!resizeHadError && BSKY_IMG_MIME_TYPES.includes(returnType)) { 156 console.log(`Attempting quality level ${qualityLevel}% for ${originalName}, size: ${transformFileSize}`); ··· 186 187 if (failedToResize) { 188 const fileSizeOverAmount: string = ((file.size - BSKY_IMG_SIZE_LIMIT)/MB_TO_BYTES).toFixed(2); 189 + return {"success": false, "originalName": originalName, "error": `Image is too large for BSky, size is over by ${fileSizeOverAmount}MB`}; 190 } 191 } 192
+2 -1
wrangler.toml
··· 79 queue = "skyscheduler-repost-queue" 80 max_retries = 3 81 82 [images] 83 binding = "IMAGES" 84 ··· 90 BETTER_AUTH_URL="https://skyscheduler.work" 91 92 # If we should use cf image transforms & how much of a quality percentage should we try and the public location of the resize bucket. 93 - IMAGE_SETTINGS={enabled=true, steps=[95, 85, 75], bucket_url="https://resize.skyscheduler.work/"} 94 95 # Signup options and if keys should be used 96 SIGNUP_SETTINGS = {use_captcha=true, invite_only=false, invite_thread="", invite_uses=10}
··· 79 queue = "skyscheduler-repost-queue" 80 max_retries = 3 81 82 + # used for resizing the thumbnails for link posts 83 [images] 84 binding = "IMAGES" 85 ··· 91 BETTER_AUTH_URL="https://skyscheduler.work" 92 93 # If we should use cf image transforms & how much of a quality percentage should we try and the public location of the resize bucket. 94 + IMAGE_SETTINGS={enabled=true, steps=[95, 85, 75], bucket_url="https://resize.skyscheduler.work/", max_width=3000} 95 96 # Signup options and if keys should be used 97 SIGNUP_SETTINGS = {use_captcha=true, invite_only=false, invite_thread="", invite_uses=10}