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

Fix reposts of already posted posts

ok go

+56 -12
+41 -5
src/utils/db/maintain.ts
··· 1 - import { eq, getTableColumns, gt, inArray, isNull, sql } from "drizzle-orm"; 1 + import { count, eq, getTableColumns, gt, inArray, isNull, sql } from "drizzle-orm"; 2 2 import { BatchItem } from "drizzle-orm/batch"; 3 3 import { DrizzleD1Database } from "drizzle-orm/d1"; 4 4 import flatten from "just-flatten-it"; 5 + import isEmpty from "just-is-empty"; 6 + import remove from "just-remove"; 7 + import { RepostInfo } from "../../classes/repost"; 5 8 import { mediaFiles, posts, repostCounts, reposts } from "../../db/app.schema"; 6 9 import { users } from "../../db/auth.schema"; 7 10 import { MAX_POSTED_LENGTH } from "../../limits"; ··· 66 69 .where(inArray(mediaFiles.fileName, flatten(userMedia)))); 67 70 } 68 71 69 - const allPosts = await db.select({id: posts.uuid}).from(posts); 72 + // clean up post data 73 + const allPosts = await db.select({id: posts.uuid, info: posts.repostInfo}).from(posts); 70 74 for (const post of allPosts) { 71 - const count = db.$count(reposts, eq(reposts.uuid, post.id)); 72 - batchedQueries.push(db.insert(repostCounts).values({uuid: post.id, 73 - count: count}).onConflictDoNothing()); 75 + // Clean up repost counts 76 + const countHelper = db.$count(reposts, eq(reposts.uuid, post.id)); 77 + batchedQueries.push(db.insert(repostCounts) 78 + .values({uuid: post.id, count: countHelper}) 79 + .onConflictDoUpdate({target: repostCounts.uuid, set: {count: countHelper}})); 80 + 81 + // Clean up repost info of posts 82 + // TODO: this doesn't work properly, and I accidentally obliterated my 83 + // test data, and the bug is already fixed so there's no real reason to keep going with this 84 + // it'll just be a silly quirk. 85 + if (!isEmpty(post.info) && false) { 86 + // this post has repost info 87 + let repostInfoMap: Map<string, RepostInfo> = new Map(); 88 + post.info!.forEach((itm) => repostInfoMap.set(itm.guid, itm)); 89 + // Keep this too to make sure we worked on all objects, if anything is left over in this array, delete it. 90 + // could probably do a where not exists but meh 91 + let repostInfoGuids: string[] = post.info!.map((itm) => itm.guid); 92 + 93 + const repostInfoData = await db.select({id: reposts.scheduleGuid, count: count()}).from(reposts) 94 + .where(inArray(reposts.scheduleGuid, repostInfoGuids)).all(); 95 + 96 + for (const res of repostInfoData) { 97 + // if the object is empty, remove from the map 98 + if (res.count == 0) { 99 + repostInfoMap.delete(res.id!); 100 + } 101 + // remove from this array 102 + repostInfoGuids = remove(repostInfoGuids, [res.id]); 103 + } 104 + // delete any records that don't exist 105 + repostInfoGuids.forEach((itm) => repostInfoMap.delete(itm)); 106 + const newRepostInfo = Array.from(repostInfoMap.values()); 107 + batchedQueries.push(db.update(posts).set({repostInfo: newRepostInfo}).where(eq(posts.uuid, post.id))); 108 + } 74 109 } 110 + 75 111 if (batchedQueries.length > 0) 76 112 await db.batch(batchedQueries as BatchQuery); 77 113 };
+15 -7
src/utils/dbQuery.ts
··· 390 390 return {ok: false, msg: `Num of reposts rules for this post has exceeded the limit of ${MAX_REPOST_RULES_PER_POST} rules`}; 391 391 } 392 392 393 + const repostInfoTimeStr = repostInfo.time.toISOString(); 393 394 // Check to see if we have an exact repost match. 394 395 // If we do, do not update the repostInfo, as repost table will drop the duplicates for us anyways. 395 - const isNewInfoNotDuped = (el: RepostInfo) => { 396 - return el.time != repostInfo.time && el.count != repostInfo.count && 397 - el.hours != repostInfo.hours; 396 + const isNewInfoNotDuped = (el: any) => { 397 + if (el.time == repostInfoTimeStr) { 398 + if (el.count == repostInfo.count) { 399 + return el.hours != repostInfo.hours; 400 + } 401 + } 402 + return true; 398 403 }; 399 404 if (newRepostInfo.every(isNewInfoNotDuped)) { 400 405 newRepostInfo.push(repostInfo); ··· 403 408 dbOperations.push(db.update(posts).set({repostInfo: newRepostInfo}).where(and( 404 409 eq(posts.userId, userId), eq(posts.cid, cid)))); 405 410 } 406 - 407 411 } else { 408 412 // Limit of post reposts on the user's account. 409 413 const accountCurrentReposts = await db.$count(posts, and(eq(posts.userId, userId), eq(posts.isRepost, true))); ··· 452 456 if (existingPost.isRepost && !isEmpty(content)) { 453 457 dbOperations.push(db.update(posts).set({content: content!}).where(eq(posts.uuid, postUUID))); 454 458 } 459 + 460 + // Because there could be conflicts that drop, run a count on the entire list and use the value from that 455 461 const newCount = db.$count(reposts, eq(reposts.uuid, postUUID)); 456 - dbOperations.push(db.update(repostCounts) 457 - .set({count: newCount}) 458 - .where(eq(repostCounts.uuid, postUUID))); 462 + // we also don't know if the repost count table has repost values for this item, so we should 463 + // attempt to always insert and update if it already exists 464 + dbOperations.push(db.insert(repostCounts) 465 + .values({uuid: postUUID, count: newCount}) 466 + .onConflictDoUpdate({target: repostCounts.uuid, set: {count: newCount}})); 459 467 } 460 468 else { 461 469 // this is a first time repost post, so we know there were no conflicts