Schedule posts to Bluesky with Cloudflare workers. skyscheduler.work
cf tool bsky-tool cloudflare bluesky schedule bsky service social-media cloudflare-workers
at main 158 lines 5.3 kB view raw
1const repostForm = document.getElementById("repostForm"); 2const repostTitle = document.getElementById("repostTitle"); 3const repostTitleSection = document.getElementById("repostTitleSection"); 4const repostRecordURL = document.getElementById("repostRecordURL"); 5 6async function getAccountHandle(account) { 7 if (account.match(/did\:plc\:/i)) { 8 return account; 9 } 10 const lookupRequest = await fetch(`https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=${account}`); 11 if (lookupRequest.ok) { 12 const response = await lookupRequest.json(); 13 if (response.hasOwnProperty("did")) { 14 return response.did; 15 } 16 } 17 return null; 18} 19 20async function getPostCID(account, postid) { 21 const cidResponse = await fetch(`https://public.api.bsky.app/xrpc/com.atproto.repo.getRecord?collection=app.bsky.feed.post&repo=${account}&rkey=${postid}`); 22 if (cidResponse.ok) { 23 const response = await cidResponse.json(); 24 if (response.hasOwnProperty("cid")) 25 return response.cid; 26 } 27 return null; 28} 29 30document.addEventListener("resetRepost", () => { 31 repostForm.reset(); 32 repostForm.removeAttribute("disabled"); 33 showRepostProgress(false); 34 setElementVisible(repostTitleSection, true); 35 setElementDisabled(repostTitle, false); 36 repostTitle.value = ""; 37 repostRecordURL.value = ""; 38 setTimeout(scrollTop, 400); 39}); 40 41repostForm.addEventListener('submit', async (e) => { 42 e.preventDefault(); 43 showRepostProgress(true); 44 const scheduledDateVal = document.getElementById("repostTime").value; 45 const postRecordVal = repostRecordURL.value; 46 let dateTime; 47 try { 48 dateTime = new Date(scheduledDateVal).toISOString(); 49 } catch(dateErr) { 50 pushToast("Invalid date", false); 51 showRepostProgress(false); 52 return; 53 } 54 55 const postObject = { 56 url: postRecordVal, 57 scheduledDate: dateTime 58 }; 59 60 // Add repost data if we should be making reposts 61 const repostCycleOptions = document.getElementById("makeRepostOptions"); 62 if (repostCycleOptions.checked) { 63 const repostValues = repostCycleOptions.parentElement.querySelectorAll("select"); 64 postObject.repostData = { 65 hours: repostValues[0].value, 66 times: repostValues[1].value 67 }; 68 } 69 70 // Push any names we have for the post here too 71 if (repostTitle.value !== "" && isElementVisible(repostTitleSection)) { 72 postObject.content = repostTitle.value; 73 } 74 75 const {account, postid} = ATPROTO_RECORD_REGEX.exec(postRecordVal)?.groups; 76 if (account === undefined || postid === undefined) { 77 pushToast("URL provided is invalid!", false); 78 showRepostProgress(false); 79 return; 80 } 81 const didResponse = await getAccountHandle(account); 82 if (didResponse === null) { 83 pushToast("Unable to infer account poster at this time", false); 84 showRepostProgress(false); 85 return; 86 } 87 postObject.uri = `at://${didResponse}/app.bsky.feed.post/${postid}`; 88 const cidFetch = await getPostCID(didResponse, postid); 89 if (cidFetch === null) { 90 pushToast("Unable to infer post records at this time, double check URL", false); 91 showRepostProgress(false); 92 return; 93 } 94 postObject.cid = cidFetch; 95 96 const payload = JSON.stringify(postObject); 97 const response = await fetch('/post/create/repost', { 98 method: 'POST', 99 headers: {'Content-Type': 'application/json' }, 100 body: payload 101 }); 102 const data = await response.json(); 103 104 if (response.ok) { 105 pushToast(data.msg, true); 106 document.dispatchEvent(new Event("resetRepost")); 107 refreshPosts(); 108 } else { 109 // For postnow, we try again, immediate failures still add to the DB 110 if (response.status === 406 && postNow) { 111 document.dispatchEvent(new Event("resetRepost")); 112 htmx.trigger("body", "accountViolations"); 113 refreshPosts(); 114 } 115 pushToast(translateErrorObject(data, data.error?.message || data.error || "An Error Occurred"), false); 116 showRepostProgress(false); 117 } 118}); 119 120document.addEventListener("addNewRepost", (ev) => { 121 const postHeader = ev.detail.target; 122 const postURIHolder = postHeader.parentElement.querySelector("footer small a:not(hidden)"); 123 const postBodyHolder = postHeader.parentElement.querySelector(".postText"); 124 if (postURIHolder && postBodyHolder) { 125 setElementVisible(repostTitleSection, true); 126 setElementDisabled(repostTitle, false); 127 const postURI = postURIHolder.getAttribute("href"); 128 const isExistingRepost = postHeader.hasAttribute("data-repost"); 129 if (isExistingRepost) { 130 repostTitle.value = postBodyHolder.innerText.trim(); 131 } else { 132 setElementVisible(repostTitleSection, false); 133 setElementDisabled(repostTitle, true); 134 repostTitle.value = ""; 135 } 136 repostRecordURL.value = postURI; 137 if (contentTabs !== null) { 138 contentTabs.switchTab("dashtabs", 1); 139 scrollToObject(repostRecordURL); 140 document.getElementById("repostTime").value = getScheduleTimeForNextHour(); 141 return; 142 } 143 } 144 pushToast("cannot add reposts to this post", false); 145}); 146 147 148function showRepostProgress(shouldShow) { 149 const el = document.getElementById("makingRepostRequest"); 150 el.setAttribute("aria-busy", shouldShow); 151 setElementDisabled(el, shouldShow); 152 setElementDisabled(postForm, shouldShow); 153 if (shouldShow) { 154 el.textContent = "Scheduling Retweets..."; 155 } else { 156 el.textContent = "Schedule Retweet"; 157 } 158}