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

project cleanup and formatting

no new features or code, just simplification.

+76 -122
+5
assets/css/stylesheet.css
··· 6 6 display: none; 7 7 } 8 8 9 + .sidebar-block { 10 + margin-top: -1.3rem; 11 + padding-bottom: 0.5rem; 12 + } 13 + 9 14 #repostScheduleSimple { 10 15 select { 11 16 width: 10%;
+12
assets/js/main.js
··· 219 219 pushToast(successMessage, true); 220 220 redirectAfterDelay(successLocation); 221 221 }); 222 + } 223 + 224 + function addEasyModalOpen(buttonID, modalEl, closeButtonID) { 225 + document.getElementById(buttonID).addEventListener("click", (ev) => { 226 + ev.preventDefault(); 227 + clearSettingsData(); 228 + openModal(modalEl); 229 + }); 230 + document.getElementById(closeButtonID).addEventListener("click", (ev) => { 231 + ev.preventDefault(); 232 + closeModal(modalEl); 233 + }); 222 234 }
+1 -1
assets/js/main.min.js
··· 1 - function pushToast(e,t){Toastify({text:e,duration:t?Toastify.defaults.duration:1e4,style:{background:t?"green":"red"}}).showToast()}function formatDate(e){return new Date(e).toLocaleString(void 0,{year:"numeric",month:"short",day:"numeric",hour:"numeric"})}function updateAllTimes(){document.querySelectorAll(".timestamp").forEach(e=>{e.hasAttribute("corrected")||(e.innerHTML=formatDate(e.innerHTML),e.setAttribute("corrected",!0))})}function refreshPosts(){document.getElementById("refresh-posts-force").click()}document.addEventListener("postDeleted",function(){pushToast("Post deleted",!0)}),document.addEventListener("postFailedDelete",function(){pushToast("Post failed to delete, try again",!1),refreshPosts()}),document.addEventListener("timeSidebar",function(){updateAllTimes()}),document.addEventListener("postUpdatedNotice",function(){pushToast("Post updated successfully!",!0)}),document.addEventListener("accountUpdated",function(e){closeModal(document.getElementById("changeInfo")),document.getElementById("settingsData").reset();const t=document.getElementById("violationBar");t&&t.setAttribute("hidden","true"),pushToast("Settings Updated!",!0)}),document.addEventListener("accountDeleted",function(e){pushToast("Account deleted!",!0)}),document.addEventListener("refreshPosts",function(e){refreshPosts()});const domainRegex=/^([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/,linkRegex=/(?:^.*\/profile\/)([0-9a-zA-Z\-\.]+)(?:\/post\/\w+)?(?:\/)?$/g;function updateUsername(e){let t=e.replace(/[^\x00-\x7F]/g,"").replace("@","");if(t.includes("did:plc:"))return pushToast("Invalid link posted, does not have handle in it",!1),"";var n=linkRegex.exec(t);if(null!=n&&n.length>=2)return n[1];if(!t.match(domainRegex)){const e=t+".bsky.social";if(e.match(domainRegex))return e}return t}function addUsernameFieldWatchers(){const e=document.getElementById("username");null!==e&&(e.value="",e.addEventListener("change",t=>{t.preventDefault(),e.value=updateUsername(e.value)}),e.addEventListener("paste",t=>{t.preventDefault(),e.value=updateUsername(t.clipboardData.getData("text"))}))}function addCounter(e,t,n){const o=document.getElementById(e),r=document.getElementById(t);if(r){if(r.hasAttribute("counting"))return;const e=e=>{r.innerHTML=`${e.all}/${n}`,e.all>n?r.classList.add("tooLong"):r.classList.remove("tooLong")};Countable.on(o,e),r.setAttribute("counting",!0),r.addEventListener("reset",()=>{r.innerHTML=`0/${n}`,r.classList.remove("tooLong")}),r.addEventListener("recount",()=>{Countable.count(o,e)})}}function recountCounter(e){document.getElementById(e).dispatchEvent(new Event("recount"))}function resetCounter(e){document.getElementById(e).dispatchEvent(new Event("reset"))}function redirectAfterDelay(e){setTimeout(function(){window.location.href=e},1200)}function translateErrorObject(e,t){let n=t;var o=!1;try{n=JSON.parse(e.message),o=!0}catch{null!==e.message&&(n=e.message)}if(o){var r="";for(error of n)r+=`${error.message}\n`;n=r}return`Error Occurred!\n----\n${n}`}function rawSubmitHandler(e,t){const n=document.getElementById("loading");document.getElementById("loginForm").addEventListener("submit",async o=>{o.preventDefault(),n.removeAttribute("hidden");let r={};document.querySelectorAll("input").forEach(e=>{"checkbox"===e.getAttribute("type")?r[e.name]=e.checked:r[e.name]=e.value});try{const o=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)});if(setTimeout(function(){n.setAttribute("hidden",!0)},1e3),o.ok)t();else{const e=await o.json();pushToast(translateErrorObject(e,e.msg),!1)}}catch(e){pushToast("An error occurred",!1),console.error(e)}})}function easySetup(e,t,n){addUsernameFieldWatchers(),rawSubmitHandler(e,function(){pushToast(t,!0),redirectAfterDelay(n)})} 1 + function pushToast(e,t){Toastify({text:e,duration:t?Toastify.defaults.duration:1e4,style:{background:t?"green":"red"}}).showToast()}function formatDate(e){return new Date(e).toLocaleString(void 0,{year:"numeric",month:"short",day:"numeric",hour:"numeric"})}function updateAllTimes(){document.querySelectorAll(".timestamp").forEach(e=>{e.hasAttribute("corrected")||(e.innerHTML=formatDate(e.innerHTML),e.setAttribute("corrected",!0))})}function refreshPosts(){document.getElementById("refresh-posts-force").click()}document.addEventListener("postDeleted",function(){pushToast("Post deleted",!0)}),document.addEventListener("postFailedDelete",function(){pushToast("Post failed to delete, try again",!1),refreshPosts()}),document.addEventListener("timeSidebar",function(){updateAllTimes()}),document.addEventListener("postUpdatedNotice",function(){pushToast("Post updated successfully!",!0)}),document.addEventListener("accountUpdated",function(e){closeModal(document.getElementById("changeInfo")),document.getElementById("settingsData").reset();const t=document.getElementById("violationBar");t&&t.setAttribute("hidden","true"),pushToast("Settings Updated!",!0)}),document.addEventListener("accountDeleted",function(e){pushToast("Account deleted!",!0)}),document.addEventListener("refreshPosts",function(e){refreshPosts()});const domainRegex=/^([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/,linkRegex=/(?:^.*\/profile\/)([0-9a-zA-Z\-\.]+)(?:\/post\/\w+)?(?:\/)?$/g;function updateUsername(e){let t=e.replace(/[^\x00-\x7F]/g,"").replace("@","");if(t.includes("did:plc:"))return pushToast("Invalid link posted, does not have handle in it",!1),"";var n=linkRegex.exec(t);if(null!=n&&n.length>=2)return n[1];if(!t.match(domainRegex)){const e=t+".bsky.social";if(e.match(domainRegex))return e}return t}function addUsernameFieldWatchers(){const e=document.getElementById("username");null!==e&&(e.value="",e.addEventListener("change",t=>{t.preventDefault(),e.value=updateUsername(e.value)}),e.addEventListener("paste",t=>{t.preventDefault(),e.value=updateUsername(t.clipboardData.getData("text"))}))}function addCounter(e,t,n){const o=document.getElementById(e),a=document.getElementById(t);if(a){if(a.hasAttribute("counting"))return;const e=e=>{a.innerHTML=`${e.all}/${n}`,e.all>n?a.classList.add("tooLong"):a.classList.remove("tooLong")};Countable.on(o,e),a.setAttribute("counting",!0),a.addEventListener("reset",()=>{a.innerHTML=`0/${n}`,a.classList.remove("tooLong")}),a.addEventListener("recount",()=>{Countable.count(o,e)})}}function recountCounter(e){document.getElementById(e).dispatchEvent(new Event("recount"))}function resetCounter(e){document.getElementById(e).dispatchEvent(new Event("reset"))}function redirectAfterDelay(e){setTimeout(function(){window.location.href=e},1200)}function translateErrorObject(e,t){let n=t;var o=!1;try{n=JSON.parse(e.message),o=!0}catch{null!==e.message&&(n=e.message)}if(o){var a="";for(error of n)a+=`${error.message}\n`;n=a}return`Error Occurred!\n----\n${n}`}function rawSubmitHandler(e,t){const n=document.getElementById("loading");document.getElementById("loginForm").addEventListener("submit",async o=>{o.preventDefault(),n.removeAttribute("hidden");let a={};document.querySelectorAll("input").forEach(e=>{"checkbox"===e.getAttribute("type")?a[e.name]=e.checked:a[e.name]=e.value});try{const o=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)});if(setTimeout(function(){n.setAttribute("hidden",!0)},1e3),o.ok)t();else{const e=await o.json();pushToast(translateErrorObject(e,e.msg),!1)}}catch(e){pushToast("An error occurred",!1),console.error(e)}})}function easySetup(e,t,n){addUsernameFieldWatchers(),rawSubmitHandler(e,function(){pushToast(t,!0),redirectAfterDelay(n)})}function addEasyModalOpen(e,t,n){document.getElementById(e).addEventListener("click",e=>{e.preventDefault(),clearSettingsData(),openModal(t)}),document.getElementById(n).addEventListener("click",e=>{e.preventDefault(),closeModal(t)})}
+2 -63
package-lock.json
··· 154 154 "version": "1.4.10", 155 155 "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.4.10.tgz", 156 156 "integrity": "sha512-AThrfb6CpG80wqkanfrbN2/fGOYzhGladHFf3JhaWt/3/Vtf4h084T6PJLrDE7M/vCCGYvDI1DkvP3P1OB2HAg==", 157 - "peer": true, 158 157 "dependencies": { 159 158 "@standard-schema/spec": "^1.0.0", 160 159 "zod": "^4.1.12" ··· 184 183 "version": "0.3.0", 185 184 "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.3.0.tgz", 186 185 "integrity": "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==", 187 - "license": "MIT", 188 - "peer": true 186 + "license": "MIT" 189 187 }, 190 188 "node_modules/@better-fetch/fetch": { 191 189 "version": "1.1.21", 192 190 "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.21.tgz", 193 - "integrity": "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==", 194 - "peer": true 191 + "integrity": "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==" 195 192 }, 196 193 "node_modules/@cloudflare/kv-asset-handler": { 197 194 "version": "0.4.1", ··· 381 378 "cpu": [ 382 379 "arm" 383 380 ], 384 - "dev": true, 385 381 "license": "MIT", 386 382 "optional": true, 387 383 "os": [ ··· 398 394 "cpu": [ 399 395 "arm64" 400 396 ], 401 - "dev": true, 402 397 "license": "MIT", 403 398 "optional": true, 404 399 "os": [ ··· 415 410 "cpu": [ 416 411 "x64" 417 412 ], 418 - "dev": true, 419 413 "license": "MIT", 420 414 "optional": true, 421 415 "os": [ ··· 432 426 "cpu": [ 433 427 "arm64" 434 428 ], 435 - "dev": true, 436 429 "license": "MIT", 437 430 "optional": true, 438 431 "os": [ ··· 449 442 "cpu": [ 450 443 "x64" 451 444 ], 452 - "dev": true, 453 445 "license": "MIT", 454 446 "optional": true, 455 447 "os": [ ··· 466 458 "cpu": [ 467 459 "arm64" 468 460 ], 469 - "dev": true, 470 461 "license": "MIT", 471 462 "optional": true, 472 463 "os": [ ··· 483 474 "cpu": [ 484 475 "x64" 485 476 ], 486 - "dev": true, 487 477 "license": "MIT", 488 478 "optional": true, 489 479 "os": [ ··· 500 490 "cpu": [ 501 491 "arm" 502 492 ], 503 - "dev": true, 504 493 "license": "MIT", 505 494 "optional": true, 506 495 "os": [ ··· 517 506 "cpu": [ 518 507 "arm64" 519 508 ], 520 - "dev": true, 521 509 "license": "MIT", 522 510 "optional": true, 523 511 "os": [ ··· 534 522 "cpu": [ 535 523 "ia32" 536 524 ], 537 - "dev": true, 538 525 "license": "MIT", 539 526 "optional": true, 540 527 "os": [ ··· 551 538 "cpu": [ 552 539 "loong64" 553 540 ], 554 - "dev": true, 555 541 "license": "MIT", 556 542 "optional": true, 557 543 "os": [ ··· 568 554 "cpu": [ 569 555 "mips64el" 570 556 ], 571 - "dev": true, 572 557 "license": "MIT", 573 558 "optional": true, 574 559 "os": [ ··· 585 570 "cpu": [ 586 571 "ppc64" 587 572 ], 588 - "dev": true, 589 573 "license": "MIT", 590 574 "optional": true, 591 575 "os": [ ··· 602 586 "cpu": [ 603 587 "riscv64" 604 588 ], 605 - "dev": true, 606 589 "license": "MIT", 607 590 "optional": true, 608 591 "os": [ ··· 619 602 "cpu": [ 620 603 "s390x" 621 604 ], 622 - "dev": true, 623 605 "license": "MIT", 624 606 "optional": true, 625 607 "os": [ ··· 636 618 "cpu": [ 637 619 "x64" 638 620 ], 639 - "dev": true, 640 621 "license": "MIT", 641 622 "optional": true, 642 623 "os": [ ··· 653 634 "cpu": [ 654 635 "x64" 655 636 ], 656 - "dev": true, 657 637 "license": "MIT", 658 638 "optional": true, 659 639 "os": [ ··· 670 650 "cpu": [ 671 651 "x64" 672 652 ], 673 - "dev": true, 674 653 "license": "MIT", 675 654 "optional": true, 676 655 "os": [ ··· 687 666 "cpu": [ 688 667 "x64" 689 668 ], 690 - "dev": true, 691 669 "license": "MIT", 692 670 "optional": true, 693 671 "os": [ ··· 704 682 "cpu": [ 705 683 "arm64" 706 684 ], 707 - "dev": true, 708 685 "license": "MIT", 709 686 "optional": true, 710 687 "os": [ ··· 721 698 "cpu": [ 722 699 "ia32" 723 700 ], 724 - "dev": true, 725 701 "license": "MIT", 726 702 "optional": true, 727 703 "os": [ ··· 738 714 "cpu": [ 739 715 "x64" 740 716 ], 741 - "dev": true, 742 717 "license": "MIT", 743 718 "optional": true, 744 719 "os": [ ··· 805 780 "cpu": [ 806 781 "ppc64" 807 782 ], 808 - "dev": true, 809 783 "license": "MIT", 810 784 "optional": true, 811 785 "os": [ ··· 822 796 "cpu": [ 823 797 "arm" 824 798 ], 825 - "dev": true, 826 799 "license": "MIT", 827 800 "optional": true, 828 801 "os": [ ··· 839 812 "cpu": [ 840 813 "arm64" 841 814 ], 842 - "dev": true, 843 815 "license": "MIT", 844 816 "optional": true, 845 817 "os": [ ··· 856 828 "cpu": [ 857 829 "x64" 858 830 ], 859 - "dev": true, 860 831 "license": "MIT", 861 832 "optional": true, 862 833 "os": [ ··· 873 844 "cpu": [ 874 845 "arm64" 875 846 ], 876 - "dev": true, 877 847 "license": "MIT", 878 848 "optional": true, 879 849 "os": [ ··· 890 860 "cpu": [ 891 861 "x64" 892 862 ], 893 - "dev": true, 894 863 "license": "MIT", 895 864 "optional": true, 896 865 "os": [ ··· 907 876 "cpu": [ 908 877 "arm64" 909 878 ], 910 - "dev": true, 911 879 "license": "MIT", 912 880 "optional": true, 913 881 "os": [ ··· 924 892 "cpu": [ 925 893 "x64" 926 894 ], 927 - "dev": true, 928 895 "license": "MIT", 929 896 "optional": true, 930 897 "os": [ ··· 941 908 "cpu": [ 942 909 "arm" 943 910 ], 944 - "dev": true, 945 911 "license": "MIT", 946 912 "optional": true, 947 913 "os": [ ··· 958 924 "cpu": [ 959 925 "arm64" 960 926 ], 961 - "dev": true, 962 927 "license": "MIT", 963 928 "optional": true, 964 929 "os": [ ··· 975 940 "cpu": [ 976 941 "ia32" 977 942 ], 978 - "dev": true, 979 943 "license": "MIT", 980 944 "optional": true, 981 945 "os": [ ··· 992 956 "cpu": [ 993 957 "loong64" 994 958 ], 995 - "dev": true, 996 959 "license": "MIT", 997 960 "optional": true, 998 961 "os": [ ··· 1009 972 "cpu": [ 1010 973 "mips64el" 1011 974 ], 1012 - "dev": true, 1013 975 "license": "MIT", 1014 976 "optional": true, 1015 977 "os": [ ··· 1026 988 "cpu": [ 1027 989 "ppc64" 1028 990 ], 1029 - "dev": true, 1030 991 "license": "MIT", 1031 992 "optional": true, 1032 993 "os": [ ··· 1043 1004 "cpu": [ 1044 1005 "riscv64" 1045 1006 ], 1046 - "dev": true, 1047 1007 "license": "MIT", 1048 1008 "optional": true, 1049 1009 "os": [ ··· 1060 1020 "cpu": [ 1061 1021 "s390x" 1062 1022 ], 1063 - "dev": true, 1064 1023 "license": "MIT", 1065 1024 "optional": true, 1066 1025 "os": [ ··· 1077 1036 "cpu": [ 1078 1037 "x64" 1079 1038 ], 1080 - "dev": true, 1081 1039 "license": "MIT", 1082 1040 "optional": true, 1083 1041 "os": [ ··· 1094 1052 "cpu": [ 1095 1053 "arm64" 1096 1054 ], 1097 - "dev": true, 1098 1055 "license": "MIT", 1099 1056 "optional": true, 1100 1057 "os": [ ··· 1111 1068 "cpu": [ 1112 1069 "x64" 1113 1070 ], 1114 - "dev": true, 1115 1071 "license": "MIT", 1116 1072 "optional": true, 1117 1073 "os": [ ··· 1128 1084 "cpu": [ 1129 1085 "arm64" 1130 1086 ], 1131 - "dev": true, 1132 1087 "license": "MIT", 1133 1088 "optional": true, 1134 1089 "os": [ ··· 1145 1100 "cpu": [ 1146 1101 "x64" 1147 1102 ], 1148 - "dev": true, 1149 1103 "license": "MIT", 1150 1104 "optional": true, 1151 1105 "os": [ ··· 1162 1116 "cpu": [ 1163 1117 "arm64" 1164 1118 ], 1165 - "dev": true, 1166 1119 "license": "MIT", 1167 1120 "optional": true, 1168 1121 "os": [ ··· 1179 1132 "cpu": [ 1180 1133 "x64" 1181 1134 ], 1182 - "dev": true, 1183 1135 "license": "MIT", 1184 1136 "optional": true, 1185 1137 "os": [ ··· 1196 1148 "cpu": [ 1197 1149 "arm64" 1198 1150 ], 1199 - "dev": true, 1200 1151 "license": "MIT", 1201 1152 "optional": true, 1202 1153 "os": [ ··· 1213 1164 "cpu": [ 1214 1165 "ia32" 1215 1166 ], 1216 - "dev": true, 1217 1167 "license": "MIT", 1218 1168 "optional": true, 1219 1169 "os": [ ··· 1230 1180 "cpu": [ 1231 1181 "x64" 1232 1182 ], 1233 - "dev": true, 1234 1183 "license": "MIT", 1235 1184 "optional": true, 1236 1185 "os": [ ··· 2127 2076 "resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.4.10.tgz", 2128 2077 "integrity": "sha512-0kqwEBJLe8eyFzbUspRG/htOriCf9uMLlnpe34dlIJGdmDfPuQISd4shShvUrvIVhPxsY1dSTXdXPLpqISYOYg==", 2129 2078 "license": "MIT", 2130 - "peer": true, 2131 2079 "dependencies": { 2132 2080 "@better-auth/core": "1.4.10", 2133 2081 "@better-auth/telemetry": "1.4.10", ··· 2363 2311 "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.1.7.tgz", 2364 2312 "integrity": "sha512-6gaJe1bBIEgVebQu/7q9saahVzvBsGaByEnE8aDVncZEDiJO7sdNB28ot9I6iXSbR25egGmmZ6aIURXyQHRraQ==", 2365 2313 "license": "MIT", 2366 - "peer": true, 2367 2314 "dependencies": { 2368 2315 "@better-auth/utils": "^0.3.0", 2369 2316 "@better-fetch/fetch": "^1.1.4", ··· 2811 2758 "integrity": "sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg==", 2812 2759 "devOptional": true, 2813 2760 "license": "MIT", 2814 - "peer": true, 2815 2761 "dependencies": { 2816 2762 "@drizzle-team/brocli": "^0.10.2", 2817 2763 "@esbuild-kit/esm-loader": "^2.5.5", ··· 2827 2773 "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.1.tgz", 2828 2774 "integrity": "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA==", 2829 2775 "license": "Apache-2.0", 2830 - "peer": true, 2831 2776 "peerDependencies": { 2832 2777 "@aws-sdk/client-rds-data": ">=3", 2833 2778 "@cloudflare/workers-types": ">=4", ··· 3139 3084 "devOptional": true, 3140 3085 "hasInstallScript": true, 3141 3086 "license": "MIT", 3142 - "peer": true, 3143 3087 "bin": { 3144 3088 "esbuild": "bin/esbuild" 3145 3089 }, ··· 4001 3945 "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", 4002 3946 "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", 4003 3947 "license": "MIT", 4004 - "peer": true, 4005 3948 "funding": { 4006 3949 "url": "https://github.com/sponsors/panva" 4007 3950 } ··· 4064 4007 "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.9.tgz", 4065 4008 "integrity": "sha512-3BeXMoiOhpOwu62CiVpO6lxfq4eS6KMYfQdMsN/2kUCRNuF2YiEr7u0HLHaQU+O4Xu8YXE3bHVkwaQ85i72EuA==", 4066 4009 "license": "MIT", 4067 - "peer": true, 4068 4010 "engines": { 4069 4011 "node": ">=20.0.0" 4070 4012 } ··· 4530 4472 } 4531 4473 ], 4532 4474 "license": "MIT", 4533 - "peer": true, 4534 4475 "engines": { 4535 4476 "node": "^20.0.0 || >=22.0.0" 4536 4477 } ··· 5681 5622 "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", 5682 5623 "dev": true, 5683 5624 "license": "MIT", 5684 - "peer": true, 5685 5625 "dependencies": { 5686 5626 "pathe": "^2.0.3" 5687 5627 } ··· 6456 6396 "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", 6457 6397 "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", 6458 6398 "license": "MIT", 6459 - "peer": true, 6460 6399 "funding": { 6461 6400 "url": "https://github.com/sponsors/colinhacks" 6462 6401 }
+2 -2
src/endpoints/account.tsx
··· 11 11 import { AccountUpdateSchema } from "../validation/accountUpdateSchema"; 12 12 import { AccountDeleteSchema, AccountForgotSchema } from "../validation/accountForgotDeleteSchema"; 13 13 import { doesUserExist, getAllMediaOfUser, getUserEmailForHandle, getUsernameForUser, updateUserData } from "../utils/dbQuery"; 14 - import { doesInviteKeyHaveValues, useInviteKey } from "../utils/inviteKeys"; 14 + import { doesInviteKeyHaveValues, consumeInviteKey } from "../utils/inviteKeys"; 15 15 import { doesHandleExist } from "../utils/bskyApi"; 16 16 import { deleteFromR2 } from "../utils/r2Query"; 17 17 import isEmpty from "just-is-empty"; ··· 170 170 // check success of user creation 171 171 if (createUser.token !== null) { 172 172 // Burn the invite key 173 - c.executionCtx.waitUntil(useInviteKey(c, signupToken)); 173 + c.executionCtx.waitUntil(consumeInviteKey(c, signupToken)); 174 174 175 175 console.log(`user ${username} created! with code ${signupToken||'none'}`); 176 176 return c.json({ok: true, message: "signup success"});
+2 -2
src/index.tsx
··· 10 10 import TermsOfService from "./pages/tos"; 11 11 import PrivacyPolicy from "./pages/privacy"; 12 12 import { cleanUpPostsTask, schedulePostTask } from "./utils/scheduler"; 13 - import { runMaintenenceUpdates } from "./utils/dbQuery"; 13 + import { runMaintenanceUpdates } from "./utils/dbQuery"; 14 14 import { setupAccounts } from "./utils/setup"; 15 15 import { makeInviteKey } from "./utils/inviteKeys"; 16 16 import { makeConstScript } from "./utils/constScriptGen"; ··· 108 108 }); 109 109 110 110 app.get("/db-update", authAdminOnlyMiddleware, (c) => { 111 - c.executionCtx.waitUntil(runMaintenenceUpdates(c.env)); 111 + c.executionCtx.waitUntil(runMaintenanceUpdates(c.env)); 112 112 return c.text("ran"); 113 113 }); 114 114
+5 -21
src/layout/settings.tsx
··· 45 45 <footer id="settingsButtons"> 46 46 <button id="deleteAccountButton" class="btn-error">Delete Account</button> 47 47 <button form="settingsData">Save</button> 48 - <button class="secondary" onclick='closeSettingsModal();'>Cancel</button> 48 + <button class="secondary" id="closeSettingsButton">Cancel</button> 49 49 </footer> 50 50 </article> 51 51 </dialog> ··· 53 53 <article> 54 54 <header>Delete Account</header> 55 55 <p>To delete your SkyScheduler account, please type your password below.<br /> 56 - All pending, scheduled tweets + all unposted media will be deleted from this service. 56 + All pending, scheduled posts + all unposted media will be deleted from this service. 57 57 58 58 <center><strong>NOTE</strong>: THIS ACTION IS <u>PERMANENT</u>.</center> 59 59 </p> ··· 69 69 </div> 70 70 <footer id="accountDeleteButtons"> 71 71 <button class="btn-error" form="delAccountForm">Delete</button> 72 - <button class="secondary" onclick='closeDeleteModal();'>Cancel</button> 72 + <button class="secondary" id="closeDeleteButton">Cancel</button> 73 73 </footer> 74 74 </article> 75 75 </dialog> ··· 84 84 document.querySelectorAll("#deleteAccount input").forEach((el) => el.value = ""); 85 85 document.getElementById("accountResponse").innerHTML = ""; 86 86 document.getElementById("accountDeleteResponse").innerHTML = ""; 87 - } 88 - 89 - function closeSettingsModal() { 90 - closeModal(changeInfoModal); 91 - } 92 - function closeDeleteModal() { 93 - closeModal(deleteAccountModal); 94 87 } 95 88 96 89 addUsernameFieldWatchers(); 97 - document.getElementById("deleteAccountButton").addEventListener("click", (ev) => { 98 - ev.preventDefault(); 99 - clearSettingsData(); 100 - openModal(deleteAccountModal); 101 - }); 102 - 103 - document.getElementById("settingsButton").addEventListener("click", (ev) => { 104 - ev.preventDefault(); 105 - clearSettingsData(); 106 - openModal(changeInfoModal); 107 - }); 90 + addEasyModalOpen("deleteAccountButton", deleteAccountModal, "closeDeleteButton"); 91 + addEasyModalOpen("settingsButton", changeInfoModal, "closeSettingsButton"); 108 92 `}</script> 109 93 </> 110 94 );
+2 -2
src/pages/dashboard.tsx
··· 26 26 <article> 27 27 <header> 28 28 <h4>SkyScheduler Dashboard</h4> 29 - <div> 30 - <small>Schedule Bluesky posts effortlessly.</small><br /> 29 + <div class="sidebar-block"> 30 + <small><i>Schedule Bluesky posts effortlessly</i>.</small><br /> 31 31 <small>Account: <b class="truncate" id="currentUser" hx-get="/account/username" hx-trigger="accountUpdated from:body, load once" hx-target="this"></b></small> 32 32 </div> 33 33 <center class="controls">
+3 -2
src/pages/forgot.tsx
··· 16 16 <AccountHandler title="Forgot Password Reset" 17 17 submitText="Request Password Reset" 18 18 loadingText="Requesting Password Reset..." endpoint="/account/forgot" 19 - successText="Success! Check your bsky dms for info. Redirecting to home.." 19 + successText="Success! Check your DMs for info. Redirecting to home.." 20 20 redirect="/" 21 21 footerHTML={<FooterCopyright />}> 22 22 23 23 <center hx-history="false"> 24 24 <p>You will receive a Direct Message from <code>@{ctx.env.RESET_BOT_USERNAME}</code> on Bluesky with a link to reset your password.<br /><br /> 25 25 If you encounter errors, your Bluesky Communication settings might be set to forbid contact via Direct Messages from accounts you don't follow.<br /> 26 - It is <u>heavily recommended</u> that <a href={botAccountURL} target="_blank">you follow the service account</a>.</p> 26 + It is <u>heavily recommended</u> that <a href={botAccountURL} target="_blank">you follow the service account</a>.<br /> 27 + <b>NOTE</b>: DMs are sent one way. Your account reset URL can only be seen by you.</p> 27 28 </center> 28 29 29 30 <UsernameField />
+1 -1
src/pages/login.tsx
··· 21 21 <label hx-history="false"> 22 22 Dashboard Password 23 23 <DashboardPasswordField autocomplete={PWAutoCompleteSettings.CurrentPass} required={true} /> 24 - <small><b>NOTE</b>: This password is not related to your bluesky account!</small> 24 + <small><b>NOTE</b>: This password is not related to your Bluesky account!</small> 25 25 </label> 26 26 </AccountHandler> 27 27 </BaseLayout>
+2 -2
src/pages/privacy.tsx
··· 44 44 <div style="margin-left: 15px"> 45 45 <strong>Note that</strong>: 46 46 <ul> 47 - <li>Data is not accessible to the maintainers of the website</li> 47 + <li>Data is not accessible to the maintainers of this service</li> 48 48 <li>We do not sell your data to any third party</li> 49 49 <li>No data is used for genAI purposes nor for training generative AI models</li> 50 - <li>You can verify all of this by just looking at <a href="https://github.com/socksthewolf/skyscheduler" class="secondary" ref="noopener nofollow">the source code</a></li> 50 + <li>You can verify this by just looking at <a href="https://github.com/socksthewolf/skyscheduler" class="secondary" ref="noopener nofollow">the source code</a></li> 51 51 </ul> 52 52 </div> 53 53 </p>
+4 -3
src/pages/tos.tsx
··· 19 19 <h4>Usage</h4> 20 20 <p>By using SkyScheduler you agree to: 21 21 <ol> 22 - <li>Not use the service to spam or to otherwise violate the terms of the <a class="secondary" href="https://bsky.social/about/support/tos" rel="nofollow noindex noopener" target="_blank">Bluesky Terms of Service</a></li> 22 + <li>Not use the service to scam, spam or to otherwise violate the terms of the <a class="secondary" href="https://bsky.social/about/support/tos" rel="nofollow noindex noopener" target="_blank">Bluesky Terms of Service</a></li> 23 23 <li>Not upload material that is illegal, illicit or stolen</li> 24 24 <li>Not attempt to reverse engineer the software to cause damage or otherwise harm others</li> 25 25 <li>Not hold SkyScheduler at fault for any damages, neither perceived nor tangible</li> 26 - <li>Grant SkyScheduler a temporary, non-exclusive, royalty-free license to the content that you schedule for the sole purpose of transmitting it on your behalf to the ATProtocol of the PDS of your choosing (default: Bluesky).</li> 26 + <li>Grant SkyScheduler a temporary, non-exclusive, royalty-free license to the content that you schedule for the sole purpose of transmitting it on your behalf via the ATProtocol to the PDS of your choosing (default: Bluesky).</li> 27 27 <ul> 28 28 <li>Upon successful transmission, content will be deleted from our temporary holding storage.</li> 29 29 </ul> 30 30 </ol> 31 31 <hr /> 32 - Violations of these agreements will allow SkyScheduler to terminate your access to the website. Upon account deletion/termination, all temporarily stored content will be deleted. 32 + Violations of these agreements will allow SkyScheduler to terminate your access to the website. Upon account deletion/termination, all temporarily stored content will be deleted.<br /> 33 + Deletions may take up to 30 days to fully cycle out of backups. 33 34 </p> 34 35 <h4>Disclaimer/Limitations</h4> 35 36 <p>SkyScheduler IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+1 -1
src/utils/bskyMsg.ts
··· 33 33 try { 34 34 await agent.chat.bsky.convo.deleteMessageForSelf({convoId: convoId, messageId: messageId}, chatHeaders); 35 35 } catch(err) { 36 - console.error(`failed to delete message for self, got error ${err}`); 36 + console.error(`failed to delete reset message for self, got error ${err}`); 37 37 } 38 38 // Message has been sent. 39 39 return true;
+4
src/utils/bskyPrune.ts
··· 4 4 import split from 'just-split'; 5 5 import isEmpty from 'just-is-empty'; 6 6 7 + // This looks for a bunch of posts that are posted and determines if the posts 8 + // are still on the network or not. If they are not, then this prunes the posts from 9 + // the database. This call is quite expensive and should only be ran on a weekly 10 + // cron job. 7 11 export const pruneBskyPosts = async (env: Bindings, userId?:string) => { 8 12 const allPostedPosts = (userId !== undefined) ? await getAllPostedPostsOfUser(env, userId) : await getAllPostedPosts(env); 9 13 let removePostIds: string[] = [];
+1 -1
src/utils/constScriptGen.ts
··· 3 3 MAX_LENGTH, MAX_ALT_TEXT, R2_FILE_SIZE_LIMIT, MAX_THUMBNAIL_SIZE } from "../limits.d"; 4 4 import { PreloadRules } from "../types.d"; 5 5 6 - export const CONST_SCRIPT_VERSION: number = 5; 6 + const CONST_SCRIPT_VERSION: number = 5; 7 7 8 8 const makeFileTypeStr = (typeMap: string[]) => { 9 9 return typeMap.map((type) => `"${type}"`).join()
+1 -1
src/utils/dbQuery.ts
··· 391 391 }; 392 392 393 393 /** Maintenance operations **/ 394 - export const runMaintenenceUpdates = async (env: Bindings) => { 394 + export const runMaintenanceUpdates = async (env: Bindings) => { 395 395 const db: DrizzleD1Database = drizzle(env.DB); 396 396 // Create a posted query that also checks for valid json and content length 397 397 const postedQuery = db.select({
+1 -1
src/utils/inviteKeys.ts
··· 32 32 return true; 33 33 }; 34 34 35 - export const useInviteKey = async(c: Context, inviteKey: string|undefined) => { 35 + export const consumeInviteKey = async(c: Context, inviteKey: string|undefined) => { 36 36 if (isUsingInviteKeys(c)) { 37 37 if (inviteKey === undefined) 38 38 return;
+1 -1
src/utils/r2Query.ts
··· 92 92 const resizeFilename = uuidv4(); 93 93 const resizeBucketPush = await env.R2RESIZE.put(resizeFilename, await file.bytes(), { 94 94 customMetadata: {"user": userId }, 95 - httpMetadata: {contentType: file.type } 95 + httpMetadata: { contentType: file.type } 96 96 }); 97 97 98 98 if (!resizeBucketPush) {
+5 -5
src/validation/accountResetSchema.ts
··· 1 - import * as z from "zod/v4"; 2 1 import { MAX_DASHBOARD_PASS, MIN_DASHBOARD_PASS } from "../limits.d"; 2 + import * as z from "zod/v4"; 3 3 4 4 export const AccountResetSchema = z.object({ 5 - resetToken: z.string().nonempty(), 5 + resetToken: z.string().nonempty("reset token is missing!"), 6 6 password: z.string().trim() 7 7 .min(MIN_DASHBOARD_PASS, "password too short") 8 8 .max(MAX_DASHBOARD_PASS, "password too long") 9 9 .nonempty("password cannot be empty") 10 10 .nonoptional(), 11 11 confirmPassword: z.string().trim() 12 - .min(MIN_DASHBOARD_PASS, "password too short") 13 - .max(MAX_DASHBOARD_PASS, "password too long") 14 - .nonempty("password cannot be empty") 12 + .min(MIN_DASHBOARD_PASS, "confirm password too short") 13 + .max(MAX_DASHBOARD_PASS, "confirm password too long") 14 + .nonempty("confirm password cannot be empty") 15 15 .nonoptional(), 16 16 }).refine((schema) => schema.confirmPassword === schema.password, "Passwords do not match");
+4 -9
src/validation/embedSchema.ts
··· 1 - import { MAX_ALT_TEXT, BSKY_VIDEO_LENGTH_LIMIT } from "../limits.d"; 1 + import { BSKY_VIDEO_LENGTH_LIMIT } from "../limits.d"; 2 2 import { EmbedDataType } from "../types.d"; 3 + import { AltTextSchema } from "./sharedValidations"; 3 4 import { FileContentSchema } from "./mediaSchema"; 4 5 import { postRecordURI } from "./regexCases"; 5 6 import * as z from "zod/v4"; 6 7 import isEmpty from "just-is-empty"; 7 - 8 - export const AltTextSchema = z.object({ 9 - alt: z.string().trim() 10 - .max(MAX_ALT_TEXT, "alt text is too long") 11 - .prefault("") 12 - }); 13 8 14 9 export const ImageEmbedSchema = z.object({ 15 10 ...FileContentSchema.shape, ··· 46 41 return false; 47 42 } 48 43 }, { 49 - message: "The link embed contained invalid data, please check your URL and try again", 44 + message: "the link to embed failed to parse, is it accessible?", 50 45 path: ["content"] 51 46 }), 52 47 type: z.literal(EmbedDataType.WebLink), ··· 57 52 normalize: true, 58 53 protocol: /^https?$/, 59 54 hostname: z.regexes.domain, 60 - error: "provided weblink is not in the correct form of an url" 55 + error: "provided link is not an URL, please check URL and try again" 61 56 }).trim() 62 57 .nonoptional("link embeds require a url"), 63 58 description: z.string().trim().default("")
+1 -1
src/validation/loginSchema.ts
··· 1 - import * as z from "zod/v4"; 2 1 import { PasswordSchema, UsernameSchema } from "./sharedValidations"; 2 + import * as z from "zod/v4"; 3 3 4 4 // Schema for login validation 5 5 export const LoginSchema = z.object({
+3 -1
src/validation/mediaSchema.ts
··· 2 2 import { fileKeyRegex } from "./regexCases"; 3 3 4 4 export const FileContentSchema = z.object({ 5 - content: z.string().toLowerCase().regex(fileKeyRegex, "file key is invalid").nonempty("file key was empty") 5 + content: z.string().toLowerCase() 6 + .regex(fileKeyRegex, "file key is invalid") 7 + .nonempty("file key was empty") 6 8 }); 7 9 8 10 export const FileDeleteSchema = FileContentSchema;
+3 -1
src/validation/postSchema.ts
··· 1 1 import { MIN_LENGTH, MAX_REPOST_INTERVAL_LIMIT, MAX_REPOST_IN_HOURS, MAX_LENGTH } from "../limits.d"; 2 - import { AltTextSchema, ImageEmbedSchema, LinkEmbedSchema, PostRecordSchema, VideoEmbedSchema } from "./embedSchema"; 2 + import { ImageEmbedSchema, LinkEmbedSchema, PostRecordSchema, VideoEmbedSchema } from "./embedSchema"; 3 3 import { FileContentSchema } from "./mediaSchema"; 4 4 import { EmbedDataType, PostLabel } from "../types.d"; 5 5 import * as z from "zod/v4"; 6 + import { AltTextSchema } from "./sharedValidations"; 6 7 7 8 const TextContent = z.object({ 8 9 content: z.string().trim() ··· 35 36 } 36 37 }, "Invalid date format. Please use ISO 8601 format (e.g. 2024-12-14T07:17:05+01:00)"), 37 38 }).superRefine(({embeds, label}, ctx) => { 39 + // Check that labels are properly set if we have embed data 38 40 if (embeds !== undefined && embeds.length > 0 && label === undefined) { 39 41 // If it's only a quote post and nothing else, then no content label is required. 40 42 if (embeds.length == 1 && embeds[0].type == EmbedDataType.Record)
+3
src/validation/regexCases.ts
··· 1 + // passwords are 4 groups of 4 char separated by dashes 1 2 export const appPasswordRegex = /(?:[0-9a-z]{4}-){3}[0-9a-z]{4}/i; 3 + // GUID + file extensions 2 4 export const fileKeyRegex = /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})\.(png|jp[e]?g|bmp|webp|heic|svg|mp[4|4v|2]|qt|mpg4|m4v|[a]?gif|webm|mp[e]?g|m[1-2]v|mov)$/i; 5 + // Given a link to a post/profile record 3 6 export const postRecordURI = /(?:^.*\/profile\/)(?<account>[0-9a-zA-Z\-\.\:]+)\/(?<type>post|feed|lists|follows)\/(?<postid>[a-z0-9]+)(?:\/)?$/i;
+7 -1
src/validation/sharedValidations.ts
··· 1 1 import * as z from "zod/v4"; 2 2 import { appPasswordRegex } from "./regexCases"; 3 - import { BSKY_MAX_APP_PASSWORD_LENGTH, BSKY_MIN_USERNAME_LENGTH, BSKY_MAX_USERNAME_LENGTH, MAX_DASHBOARD_PASS, MIN_DASHBOARD_PASS } from "../limits.d"; 3 + import { BSKY_MAX_APP_PASSWORD_LENGTH, BSKY_MIN_USERNAME_LENGTH, BSKY_MAX_USERNAME_LENGTH, MAX_DASHBOARD_PASS, MIN_DASHBOARD_PASS, MAX_ALT_TEXT } from "../limits.d"; 4 4 5 5 export const UsernameSchema = z.object({ 6 6 username: z.string().trim().toLowerCase() ··· 24 24 .nonempty("missing bsky app password") 25 25 .max(BSKY_MAX_APP_PASSWORD_LENGTH, "app password too long") 26 26 .regex(appPasswordRegex, "please go back and recreate your app password from your bsky settings") 27 + }); 28 + 29 + export const AltTextSchema = z.object({ 30 + alt: z.string().trim() 31 + .max(MAX_ALT_TEXT, "alt text is too long") 32 + .prefault("") 27 33 });