/** * Patch: add delete() support to the Bluesky syndicator. * * When a Micropub delete fires, Indiekit should be able to call * syndicator.delete(url, syndication) to remove the corresponding post from * Bluesky. This syndicator currently has no delete() method at all. * * Fix A (bluesky.js): * Add Bluesky#deletePost(bskyUrl) — resolves the AT-URI from the bsky.app * profile URL (using the existing getPostParts helper) and calls * com.atproto.repo.deleteRecord to permanently delete the record. * * Fix B (index.js): * Add BlueskySyndicator#delete(url, syndication) — finds the bsky.app URL * in the syndication array passed by the Micropub delete action and delegates * to Bluesky#deletePost(). */ import { access, readFile, writeFile } from "node:fs/promises"; const MARKER = "// [patch] bluesky-syndicator-delete"; // ── Fix A: bluesky.js — add deletePost() method ─────────────────────────────── const blueskyClassCandidates = [ "node_modules/@rmdes/indiekit-syndicator-bluesky/lib/bluesky.js", ]; const OLD_POST_REPOST = ` async postRepost(postUrl) { const client = await this.#client(); const post = await this.getPost(postUrl); const repost = await client.repost(post.uri, post.cid); return uriToPostUrl(this.profileUrl, repost.uri); } /** * Post a quote post`; const NEW_POST_REPOST = ` async postRepost(postUrl) { const client = await this.#client(); const post = await this.getPost(postUrl); const repost = await client.repost(post.uri, post.cid); return uriToPostUrl(this.profileUrl, repost.uri); } /** * Delete a Bluesky post by its bsky.app profile URL. ${MARKER} * @param {string} bskyUrl - bsky.app post URL (e.g. https://bsky.app/profile/handle/post/rkey) * @returns {Promise} */ async deletePost(bskyUrl) { ${MARKER} const client = await this.#client(); const postParts = getPostParts(bskyUrl); await client.com.atproto.repo.deleteRecord({ repo: postParts.did, collection: "app.bsky.feed.post", rkey: postParts.rkey, }); } /** * Post a quote post`; // ── Fix B: index.js — add delete() method to BlueskySyndicator ─────────────── const indexCandidates = [ "node_modules/@rmdes/indiekit-syndicator-bluesky/index.js", ]; const OLD_SYNDICATE_END = ` } catch (error) { throw new IndiekitError(error.message, { cause: error, plugin: this.name, status: error.statusCode, }); } } init(Indiekit) {`; const NEW_SYNDICATE_END = ` } catch (error) { throw new IndiekitError(error.message, { cause: error, plugin: this.name, status: error.statusCode, }); } } /** * Delete a previously syndicated post from Bluesky. ${MARKER} * @param {string} url - Original blog post URL (unused, for API parity) * @param {string[]} [syndication=[]] - Array of syndication URLs from the deleted post * @returns {Promise} */ async delete(url, syndication = []) { ${MARKER} try { const bskyUrl = syndication.find((s) => s.includes("bsky.app")); if (!bskyUrl) { console.warn(\`[Bluesky] No Bluesky URL found in syndication for \${url} — skipping delete\`); return; } const bluesky = new Bluesky({ identifier: this.options?.handle, password: this.options?.password, profileUrl: this.#profileUrl, serviceUrl: this.#serviceUrl, }); await bluesky.deletePost(bskyUrl); console.log(\`[Bluesky] Deleted syndicated post: \${bskyUrl}\`); } catch (error) { throw new IndiekitError(error.message, { cause: error, plugin: this.name, status: error.statusCode, }); } } init(Indiekit) {`; async function exists(p) { try { await access(p); return true; } catch { return false; } } async function applyPatch(candidates, oldSnippet, newSnippet, label) { let checked = 0; let patched = 0; for (const filePath of candidates) { if (!(await exists(filePath))) continue; checked++; const source = await readFile(filePath, "utf8"); if (source.includes(MARKER)) { console.log(`[postinstall] patch-bluesky-syndicator-delete: ${label} already applied to ${filePath}`); continue; } if (!source.includes(oldSnippet)) { console.warn(`[postinstall] patch-bluesky-syndicator-delete: ${label} snippet not found in ${filePath}`); continue; } await writeFile(filePath, source.replace(oldSnippet, newSnippet), "utf8"); patched++; console.log(`[postinstall] Applied patch-bluesky-syndicator-delete (${label}) to ${filePath}`); } return { checked, patched }; } const a = await applyPatch(blueskyClassCandidates, OLD_POST_REPOST, NEW_POST_REPOST, "Bluesky#deletePost"); const b = await applyPatch(indexCandidates, OLD_SYNDICATE_END, NEW_SYNDICATE_END, "BlueskySyndicator#delete"); const totalChecked = a.checked + b.checked; const totalPatched = a.patched + b.patched; if (totalChecked === 0) { console.log("[postinstall] patch-bluesky-syndicator-delete: no target files found"); } else if (totalPatched === 0) { console.log("[postinstall] patch-bluesky-syndicator-delete: already up to date"); } else { console.log(`[postinstall] patch-bluesky-syndicator-delete: patched ${totalPatched} file(s)`); }