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

slight small optimization with any reposts

This will group reposts if the repost target already exists.

+833 -15
+1
migrations/0016_glamorous_mephistopheles.sql
··· 1 + CREATE INDEX `repostAddOn_idx` ON `posts` (`user`,`cid`);
+798
migrations/meta/0016_snapshot.json
··· 1 + { 2 + "version": "6", 3 + "dialect": "sqlite", 4 + "id": "3d37dd35-7738-4d16-9fb1-989bff5667a3", 5 + "prevId": "590bd9d0-e49d-411e-9a68-cf00b27246c9", 6 + "tables": { 7 + "accounts": { 8 + "name": "accounts", 9 + "columns": { 10 + "id": { 11 + "name": "id", 12 + "type": "text", 13 + "primaryKey": true, 14 + "notNull": true, 15 + "autoincrement": false 16 + }, 17 + "account_id": { 18 + "name": "account_id", 19 + "type": "text", 20 + "primaryKey": false, 21 + "notNull": true, 22 + "autoincrement": false 23 + }, 24 + "provider_id": { 25 + "name": "provider_id", 26 + "type": "text", 27 + "primaryKey": false, 28 + "notNull": true, 29 + "autoincrement": false 30 + }, 31 + "user_id": { 32 + "name": "user_id", 33 + "type": "text", 34 + "primaryKey": false, 35 + "notNull": true, 36 + "autoincrement": false 37 + }, 38 + "access_token": { 39 + "name": "access_token", 40 + "type": "text", 41 + "primaryKey": false, 42 + "notNull": false, 43 + "autoincrement": false 44 + }, 45 + "refresh_token": { 46 + "name": "refresh_token", 47 + "type": "text", 48 + "primaryKey": false, 49 + "notNull": false, 50 + "autoincrement": false 51 + }, 52 + "id_token": { 53 + "name": "id_token", 54 + "type": "text", 55 + "primaryKey": false, 56 + "notNull": false, 57 + "autoincrement": false 58 + }, 59 + "access_token_expires_at": { 60 + "name": "access_token_expires_at", 61 + "type": "integer", 62 + "primaryKey": false, 63 + "notNull": false, 64 + "autoincrement": false 65 + }, 66 + "refresh_token_expires_at": { 67 + "name": "refresh_token_expires_at", 68 + "type": "integer", 69 + "primaryKey": false, 70 + "notNull": false, 71 + "autoincrement": false 72 + }, 73 + "scope": { 74 + "name": "scope", 75 + "type": "text", 76 + "primaryKey": false, 77 + "notNull": false, 78 + "autoincrement": false 79 + }, 80 + "password": { 81 + "name": "password", 82 + "type": "text", 83 + "primaryKey": false, 84 + "notNull": false, 85 + "autoincrement": false 86 + }, 87 + "created_at": { 88 + "name": "created_at", 89 + "type": "integer", 90 + "primaryKey": false, 91 + "notNull": true, 92 + "autoincrement": false, 93 + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" 94 + }, 95 + "updated_at": { 96 + "name": "updated_at", 97 + "type": "integer", 98 + "primaryKey": false, 99 + "notNull": true, 100 + "autoincrement": false 101 + } 102 + }, 103 + "indexes": { 104 + "accounts_userId_idx": { 105 + "name": "accounts_userId_idx", 106 + "columns": [ 107 + "user_id" 108 + ], 109 + "isUnique": false 110 + } 111 + }, 112 + "foreignKeys": { 113 + "accounts_user_id_users_id_fk": { 114 + "name": "accounts_user_id_users_id_fk", 115 + "tableFrom": "accounts", 116 + "tableTo": "users", 117 + "columnsFrom": [ 118 + "user_id" 119 + ], 120 + "columnsTo": [ 121 + "id" 122 + ], 123 + "onDelete": "cascade", 124 + "onUpdate": "no action" 125 + } 126 + }, 127 + "compositePrimaryKeys": {}, 128 + "uniqueConstraints": {}, 129 + "checkConstraints": {} 130 + }, 131 + "sessions": { 132 + "name": "sessions", 133 + "columns": { 134 + "id": { 135 + "name": "id", 136 + "type": "text", 137 + "primaryKey": true, 138 + "notNull": true, 139 + "autoincrement": false 140 + }, 141 + "expires_at": { 142 + "name": "expires_at", 143 + "type": "integer", 144 + "primaryKey": false, 145 + "notNull": true, 146 + "autoincrement": false 147 + }, 148 + "token": { 149 + "name": "token", 150 + "type": "text", 151 + "primaryKey": false, 152 + "notNull": true, 153 + "autoincrement": false 154 + }, 155 + "created_at": { 156 + "name": "created_at", 157 + "type": "integer", 158 + "primaryKey": false, 159 + "notNull": true, 160 + "autoincrement": false, 161 + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" 162 + }, 163 + "updated_at": { 164 + "name": "updated_at", 165 + "type": "integer", 166 + "primaryKey": false, 167 + "notNull": true, 168 + "autoincrement": false 169 + }, 170 + "ip_address": { 171 + "name": "ip_address", 172 + "type": "text", 173 + "primaryKey": false, 174 + "notNull": false, 175 + "autoincrement": false 176 + }, 177 + "user_agent": { 178 + "name": "user_agent", 179 + "type": "text", 180 + "primaryKey": false, 181 + "notNull": false, 182 + "autoincrement": false 183 + }, 184 + "user_id": { 185 + "name": "user_id", 186 + "type": "text", 187 + "primaryKey": false, 188 + "notNull": true, 189 + "autoincrement": false 190 + } 191 + }, 192 + "indexes": { 193 + "sessions_token_unique": { 194 + "name": "sessions_token_unique", 195 + "columns": [ 196 + "token" 197 + ], 198 + "isUnique": true 199 + }, 200 + "sessions_userId_idx": { 201 + "name": "sessions_userId_idx", 202 + "columns": [ 203 + "user_id" 204 + ], 205 + "isUnique": false 206 + } 207 + }, 208 + "foreignKeys": { 209 + "sessions_user_id_users_id_fk": { 210 + "name": "sessions_user_id_users_id_fk", 211 + "tableFrom": "sessions", 212 + "tableTo": "users", 213 + "columnsFrom": [ 214 + "user_id" 215 + ], 216 + "columnsTo": [ 217 + "id" 218 + ], 219 + "onDelete": "cascade", 220 + "onUpdate": "no action" 221 + } 222 + }, 223 + "compositePrimaryKeys": {}, 224 + "uniqueConstraints": {}, 225 + "checkConstraints": {} 226 + }, 227 + "users": { 228 + "name": "users", 229 + "columns": { 230 + "id": { 231 + "name": "id", 232 + "type": "text", 233 + "primaryKey": true, 234 + "notNull": true, 235 + "autoincrement": false 236 + }, 237 + "name": { 238 + "name": "name", 239 + "type": "text", 240 + "primaryKey": false, 241 + "notNull": true, 242 + "autoincrement": false 243 + }, 244 + "email": { 245 + "name": "email", 246 + "type": "text", 247 + "primaryKey": false, 248 + "notNull": true, 249 + "autoincrement": false 250 + }, 251 + "email_verified": { 252 + "name": "email_verified", 253 + "type": "integer", 254 + "primaryKey": false, 255 + "notNull": true, 256 + "autoincrement": false 257 + }, 258 + "image": { 259 + "name": "image", 260 + "type": "text", 261 + "primaryKey": false, 262 + "notNull": false, 263 + "autoincrement": false 264 + }, 265 + "created_at": { 266 + "name": "created_at", 267 + "type": "integer", 268 + "primaryKey": false, 269 + "notNull": true, 270 + "autoincrement": false 271 + }, 272 + "updated_at": { 273 + "name": "updated_at", 274 + "type": "integer", 275 + "primaryKey": false, 276 + "notNull": true, 277 + "autoincrement": false 278 + }, 279 + "username": { 280 + "name": "username", 281 + "type": "text", 282 + "primaryKey": false, 283 + "notNull": false, 284 + "autoincrement": false 285 + }, 286 + "display_username": { 287 + "name": "display_username", 288 + "type": "text", 289 + "primaryKey": false, 290 + "notNull": false, 291 + "autoincrement": false 292 + }, 293 + "bsky_app_pass": { 294 + "name": "bsky_app_pass", 295 + "type": "text", 296 + "primaryKey": false, 297 + "notNull": true, 298 + "autoincrement": false 299 + }, 300 + "pds": { 301 + "name": "pds", 302 + "type": "text", 303 + "primaryKey": false, 304 + "notNull": true, 305 + "autoincrement": false, 306 + "default": "'https://bsky.social'" 307 + } 308 + }, 309 + "indexes": { 310 + "users_email_unique": { 311 + "name": "users_email_unique", 312 + "columns": [ 313 + "email" 314 + ], 315 + "isUnique": true 316 + }, 317 + "users_username_unique": { 318 + "name": "users_username_unique", 319 + "columns": [ 320 + "username" 321 + ], 322 + "isUnique": true 323 + } 324 + }, 325 + "foreignKeys": {}, 326 + "compositePrimaryKeys": {}, 327 + "uniqueConstraints": {}, 328 + "checkConstraints": {} 329 + }, 330 + "verifications": { 331 + "name": "verifications", 332 + "columns": { 333 + "id": { 334 + "name": "id", 335 + "type": "text", 336 + "primaryKey": true, 337 + "notNull": true, 338 + "autoincrement": false 339 + }, 340 + "identifier": { 341 + "name": "identifier", 342 + "type": "text", 343 + "primaryKey": false, 344 + "notNull": true, 345 + "autoincrement": false 346 + }, 347 + "value": { 348 + "name": "value", 349 + "type": "text", 350 + "primaryKey": false, 351 + "notNull": true, 352 + "autoincrement": false 353 + }, 354 + "expires_at": { 355 + "name": "expires_at", 356 + "type": "integer", 357 + "primaryKey": false, 358 + "notNull": true, 359 + "autoincrement": false 360 + }, 361 + "created_at": { 362 + "name": "created_at", 363 + "type": "integer", 364 + "primaryKey": false, 365 + "notNull": true, 366 + "autoincrement": false, 367 + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" 368 + }, 369 + "updated_at": { 370 + "name": "updated_at", 371 + "type": "integer", 372 + "primaryKey": false, 373 + "notNull": true, 374 + "autoincrement": false, 375 + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" 376 + } 377 + }, 378 + "indexes": { 379 + "verifications_identifier_idx": { 380 + "name": "verifications_identifier_idx", 381 + "columns": [ 382 + "identifier" 383 + ], 384 + "isUnique": false 385 + } 386 + }, 387 + "foreignKeys": {}, 388 + "compositePrimaryKeys": {}, 389 + "uniqueConstraints": {}, 390 + "checkConstraints": {} 391 + }, 392 + "media": { 393 + "name": "media", 394 + "columns": { 395 + "file": { 396 + "name": "file", 397 + "type": "text", 398 + "primaryKey": true, 399 + "notNull": true, 400 + "autoincrement": false 401 + }, 402 + "hasPost": { 403 + "name": "hasPost", 404 + "type": "integer", 405 + "primaryKey": false, 406 + "notNull": false, 407 + "autoincrement": false, 408 + "default": false 409 + }, 410 + "user": { 411 + "name": "user", 412 + "type": "text", 413 + "primaryKey": false, 414 + "notNull": false, 415 + "autoincrement": false 416 + }, 417 + "created_at": { 418 + "name": "created_at", 419 + "type": "integer", 420 + "primaryKey": false, 421 + "notNull": true, 422 + "autoincrement": false 423 + } 424 + }, 425 + "indexes": { 426 + "media_oldWithNoPost_idx": { 427 + "name": "media_oldWithNoPost_idx", 428 + "columns": [ 429 + "hasPost", 430 + "created_at" 431 + ], 432 + "isUnique": false, 433 + "where": "hasPost = 0" 434 + }, 435 + "media_userid_idx": { 436 + "name": "media_userid_idx", 437 + "columns": [ 438 + "user" 439 + ], 440 + "isUnique": false 441 + } 442 + }, 443 + "foreignKeys": { 444 + "media_user_users_id_fk": { 445 + "name": "media_user_users_id_fk", 446 + "tableFrom": "media", 447 + "tableTo": "users", 448 + "columnsFrom": [ 449 + "user" 450 + ], 451 + "columnsTo": [ 452 + "id" 453 + ], 454 + "onDelete": "cascade", 455 + "onUpdate": "no action" 456 + } 457 + }, 458 + "compositePrimaryKeys": {}, 459 + "uniqueConstraints": {}, 460 + "checkConstraints": {} 461 + }, 462 + "posts": { 463 + "name": "posts", 464 + "columns": { 465 + "uuid": { 466 + "name": "uuid", 467 + "type": "text", 468 + "primaryKey": true, 469 + "notNull": true, 470 + "autoincrement": false 471 + }, 472 + "content": { 473 + "name": "content", 474 + "type": "text", 475 + "primaryKey": false, 476 + "notNull": true, 477 + "autoincrement": false 478 + }, 479 + "scheduled_date": { 480 + "name": "scheduled_date", 481 + "type": "integer", 482 + "primaryKey": false, 483 + "notNull": true, 484 + "autoincrement": false 485 + }, 486 + "posted": { 487 + "name": "posted", 488 + "type": "integer", 489 + "primaryKey": false, 490 + "notNull": false, 491 + "autoincrement": false, 492 + "default": false 493 + }, 494 + "postNow": { 495 + "name": "postNow", 496 + "type": "integer", 497 + "primaryKey": false, 498 + "notNull": false, 499 + "autoincrement": false, 500 + "default": false 501 + }, 502 + "embedContent": { 503 + "name": "embedContent", 504 + "type": "text", 505 + "primaryKey": false, 506 + "notNull": true, 507 + "autoincrement": false, 508 + "default": "(json_array())" 509 + }, 510 + "uri": { 511 + "name": "uri", 512 + "type": "text", 513 + "primaryKey": false, 514 + "notNull": false, 515 + "autoincrement": false 516 + }, 517 + "cid": { 518 + "name": "cid", 519 + "type": "text", 520 + "primaryKey": false, 521 + "notNull": false, 522 + "autoincrement": false 523 + }, 524 + "isRepost": { 525 + "name": "isRepost", 526 + "type": "integer", 527 + "primaryKey": false, 528 + "notNull": false, 529 + "autoincrement": false, 530 + "default": false 531 + }, 532 + "contentLabel": { 533 + "name": "contentLabel", 534 + "type": "text", 535 + "primaryKey": false, 536 + "notNull": true, 537 + "autoincrement": false, 538 + "default": "'None'" 539 + }, 540 + "created_at": { 541 + "name": "created_at", 542 + "type": "integer", 543 + "primaryKey": false, 544 + "notNull": true, 545 + "autoincrement": false, 546 + "default": "CURRENT_TIMESTAMP" 547 + }, 548 + "updated_at": { 549 + "name": "updated_at", 550 + "type": "integer", 551 + "primaryKey": false, 552 + "notNull": false, 553 + "autoincrement": false 554 + }, 555 + "user": { 556 + "name": "user", 557 + "type": "text", 558 + "primaryKey": false, 559 + "notNull": true, 560 + "autoincrement": false 561 + } 562 + }, 563 + "indexes": { 564 + "user_idx": { 565 + "name": "user_idx", 566 + "columns": [ 567 + "user" 568 + ], 569 + "isUnique": false 570 + }, 571 + "postedUpdate_idx": { 572 + "name": "postedUpdate_idx", 573 + "columns": [ 574 + "updated_at", 575 + "posted" 576 + ], 577 + "isUnique": false, 578 + "where": "posted = 1" 579 + }, 580 + "postedUUID_idx": { 581 + "name": "postedUUID_idx", 582 + "columns": [ 583 + "uuid", 584 + "posted" 585 + ], 586 + "isUnique": false 587 + }, 588 + "postNowScheduledDatePosted_idx": { 589 + "name": "postNowScheduledDatePosted_idx", 590 + "columns": [ 591 + "posted", 592 + "scheduled_date", 593 + "postNow" 594 + ], 595 + "isUnique": false, 596 + "where": "posted = 0 and postNow <> 1" 597 + }, 598 + "repostAddOn_idx": { 599 + "name": "repostAddOn_idx", 600 + "columns": [ 601 + "user", 602 + "cid" 603 + ], 604 + "isUnique": false 605 + } 606 + }, 607 + "foreignKeys": { 608 + "posts_user_users_id_fk": { 609 + "name": "posts_user_users_id_fk", 610 + "tableFrom": "posts", 611 + "tableTo": "users", 612 + "columnsFrom": [ 613 + "user" 614 + ], 615 + "columnsTo": [ 616 + "id" 617 + ], 618 + "onDelete": "cascade", 619 + "onUpdate": "no action" 620 + } 621 + }, 622 + "compositePrimaryKeys": {}, 623 + "uniqueConstraints": {}, 624 + "checkConstraints": {} 625 + }, 626 + "reposts": { 627 + "name": "reposts", 628 + "columns": { 629 + "id": { 630 + "name": "id", 631 + "type": "integer", 632 + "primaryKey": true, 633 + "notNull": true, 634 + "autoincrement": true 635 + }, 636 + "post_uuid": { 637 + "name": "post_uuid", 638 + "type": "text", 639 + "primaryKey": false, 640 + "notNull": true, 641 + "autoincrement": false 642 + }, 643 + "scheduled_date": { 644 + "name": "scheduled_date", 645 + "type": "integer", 646 + "primaryKey": false, 647 + "notNull": true, 648 + "autoincrement": false 649 + } 650 + }, 651 + "indexes": { 652 + "repost_scheduledDate_idx": { 653 + "name": "repost_scheduledDate_idx", 654 + "columns": [ 655 + "scheduled_date" 656 + ], 657 + "isUnique": false 658 + }, 659 + "repost_postid_idx": { 660 + "name": "repost_postid_idx", 661 + "columns": [ 662 + "post_uuid" 663 + ], 664 + "isUnique": false 665 + } 666 + }, 667 + "foreignKeys": { 668 + "reposts_post_uuid_posts_uuid_fk": { 669 + "name": "reposts_post_uuid_posts_uuid_fk", 670 + "tableFrom": "reposts", 671 + "tableTo": "posts", 672 + "columnsFrom": [ 673 + "post_uuid" 674 + ], 675 + "columnsTo": [ 676 + "uuid" 677 + ], 678 + "onDelete": "cascade", 679 + "onUpdate": "no action" 680 + } 681 + }, 682 + "compositePrimaryKeys": {}, 683 + "uniqueConstraints": {}, 684 + "checkConstraints": {} 685 + }, 686 + "violations": { 687 + "name": "violations", 688 + "columns": { 689 + "id": { 690 + "name": "id", 691 + "type": "integer", 692 + "primaryKey": true, 693 + "notNull": true, 694 + "autoincrement": true 695 + }, 696 + "user": { 697 + "name": "user", 698 + "type": "text", 699 + "primaryKey": false, 700 + "notNull": true, 701 + "autoincrement": false 702 + }, 703 + "tosViolation": { 704 + "name": "tosViolation", 705 + "type": "integer", 706 + "primaryKey": false, 707 + "notNull": false, 708 + "autoincrement": false, 709 + "default": false 710 + }, 711 + "userPassInvalid": { 712 + "name": "userPassInvalid", 713 + "type": "integer", 714 + "primaryKey": false, 715 + "notNull": false, 716 + "autoincrement": false, 717 + "default": false 718 + }, 719 + "accountSuspended": { 720 + "name": "accountSuspended", 721 + "type": "integer", 722 + "primaryKey": false, 723 + "notNull": false, 724 + "autoincrement": false, 725 + "default": false 726 + }, 727 + "accountGone": { 728 + "name": "accountGone", 729 + "type": "integer", 730 + "primaryKey": false, 731 + "notNull": false, 732 + "autoincrement": false, 733 + "default": false 734 + }, 735 + "mediaTooBig": { 736 + "name": "mediaTooBig", 737 + "type": "integer", 738 + "primaryKey": false, 739 + "notNull": false, 740 + "autoincrement": false, 741 + "default": false 742 + }, 743 + "created_at": { 744 + "name": "created_at", 745 + "type": "integer", 746 + "primaryKey": false, 747 + "notNull": true, 748 + "autoincrement": false, 749 + "default": "CURRENT_TIMESTAMP" 750 + } 751 + }, 752 + "indexes": { 753 + "violations_user_unique": { 754 + "name": "violations_user_unique", 755 + "columns": [ 756 + "user" 757 + ], 758 + "isUnique": true 759 + }, 760 + "violations_user_idx": { 761 + "name": "violations_user_idx", 762 + "columns": [ 763 + "user" 764 + ], 765 + "isUnique": false 766 + } 767 + }, 768 + "foreignKeys": { 769 + "violations_user_users_id_fk": { 770 + "name": "violations_user_users_id_fk", 771 + "tableFrom": "violations", 772 + "tableTo": "users", 773 + "columnsFrom": [ 774 + "user" 775 + ], 776 + "columnsTo": [ 777 + "id" 778 + ], 779 + "onDelete": "cascade", 780 + "onUpdate": "no action" 781 + } 782 + }, 783 + "compositePrimaryKeys": {}, 784 + "uniqueConstraints": {}, 785 + "checkConstraints": {} 786 + } 787 + }, 788 + "views": {}, 789 + "enums": {}, 790 + "_meta": { 791 + "schemas": {}, 792 + "tables": {}, 793 + "columns": {} 794 + }, 795 + "internal": { 796 + "indexes": {} 797 + } 798 + }
+7
migrations/meta/_journal.json
··· 113 113 "when": 1769762068364, 114 114 "tag": "0015_deep_unicorn", 115 115 "breakpoints": true 116 + }, 117 + { 118 + "idx": 16, 119 + "version": "6", 120 + "when": 1769927122775, 121 + "tag": "0016_glamorous_mephistopheles", 122 + "breakpoints": true 116 123 } 117 124 ] 118 125 }
+2
src/db/app.schema.ts
··· 36 36 index("postNowScheduledDatePosted_idx") 37 37 .on(table.posted, table.scheduledDate, table.postNow) 38 38 .where(sql`posted = 0 and postNow <> 1`), 39 + // used to lower down the amount of posts that fill up the post table 40 + index("repostAddOn_idx").on(table.userId, table.cid) 39 41 ]); 40 42 41 43 export const reposts = sqliteTable('reposts', {
+25 -15
src/utils/dbQuery.ts
··· 248 248 } 249 249 } 250 250 251 - // Create the posts 252 - const postUUID = uuidv4(); 253 - let dbOperations: BatchItem<"sqlite">[] = [ 254 - db.insert(posts).values({ 255 - content: `Repost of ${url}`, 256 - uuid: postUUID, 257 - cid: cid, 258 - uri: uri, 259 - posted: true, 260 - isRepost: true, 261 - scheduledDate: scheduleDate, 262 - userId: userId 263 - }) 264 - ]; 265 - 251 + let postUUID; 252 + let dbOperations: BatchItem<"sqlite">[] = []; 253 + 254 + // Check to see if the post already exists 255 + // (check also against the userId here as well to avoid cross account data collisions) 256 + const existingPost = await db.select({id: posts.uuid}).from(posts).where(and( 257 + eq(posts.userId, userId), eq(posts.cid, cid))).limit(1).all(); 258 + 259 + if (existingPost.length > 1) { 260 + postUUID = existingPost[0].id; 261 + } else { 262 + // Create the post base for this repost 263 + postUUID = uuidv4(); 264 + dbOperations.push(db.insert(posts).values({ 265 + content: `Repost of ${url}`, 266 + uuid: postUUID, 267 + cid: cid, 268 + uri: uri, 269 + posted: true, 270 + isRepost: true, 271 + scheduledDate: scheduleDate, 272 + userId: userId 273 + })); 274 + } 275 + 266 276 // Push initial repost 267 277 dbOperations.push(db.insert(reposts).values({ 268 278 uuid: postUUID,