Skip to content

Commit 51f3a25

Browse files
committed
chore: update from obsidian
1 parent e082ffd commit 51f3a25

4 files changed

Lines changed: 99 additions & 33 deletions

File tree

.github/workflows/scripts/post-to-bluesky.mjs

Lines changed: 80 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
#!/usr/bin/env node
22

3-
import { readFileSync } from "fs"
3+
import { readFile, writeFile, mkdir } from "fs/promises"
4+
import { existsSync, readFileSync } from "fs"
5+
import { join } from "path"
46
import { parseStringPromise } from "xml2js"
57

68
const ATPROTO_IDENTIFIER = process.env.ATPROTO_IDENTIFIER
79
const ATPROTO_POST_KEY = process.env.ATPROTO_POST_KEY
810
const RSS_FILE = process.env.RSS_FILE
11+
const DRY_RUN_DIR = "content/public/assets/bsky-tests"
912
const BSKY_HANDLE = "chaoticgood.computer"
1013

1114
/**
@@ -182,12 +185,12 @@ async function getPostedUrls() {
182185
continue
183186
}
184187

185-
// Extract URLs from post text
186-
const text = post.record.text
187-
const urlRegex = /https?:\/\/[^\s]+/g
188-
const matches = text.match(urlRegex)
189-
if (matches) {
190-
matches.forEach((url) => postedUrls.add(url.trim()))
188+
// Extract URL from external embed (our posts use embeds, not text URLs)
189+
if (post.record.embed?.$type === "app.bsky.embed.external") {
190+
const url = post.record.embed.external?.uri
191+
if (url) {
192+
postedUrls.add(url)
193+
}
191194
}
192195
}
193196

@@ -198,20 +201,32 @@ async function getPostedUrls() {
198201
* Main execution
199202
*/
200203
async function main() {
204+
const args = process.argv.slice(2)
205+
const dryRunMode = args.includes("--dry-run")
206+
201207
if (!ATPROTO_IDENTIFIER || !ATPROTO_POST_KEY || !RSS_FILE) {
202208
throw new Error("Missing required environment variables")
203209
}
204210

205211
console.log("Parsing RSS feed from:", RSS_FILE)
212+
console.log(` Mode: ${dryRunMode ? "DRY RUN" : "LIVE"}`)
206213
const items = await parseRSS(RSS_FILE)
207214
console.log(`Found ${items.length} items in RSS feed`)
208215

209216
console.log("Fetching already posted URLs from Bluesky...")
210217
const postedUrls = await getPostedUrls()
211218
console.log(`Found ${postedUrls.size} URLs already posted`)
212219

213-
const { accessJwt, did } = await authenticate()
214-
console.log("Authenticated successfully")
220+
let accessJwt, did
221+
222+
if (dryRunMode) {
223+
console.log("Dry-run mode: skipping authentication")
224+
} else {
225+
const auth = await authenticate()
226+
accessJwt = auth.accessJwt
227+
did = auth.did
228+
console.log("Authenticated successfully")
229+
}
215230

216231
// Filter to only unposted items and reverse to chronological order (oldest first)
217232
const unpostedItems = items
@@ -220,34 +235,68 @@ async function main() {
220235

221236
console.log(`Found ${unpostedItems.length} new items to post`)
222237

223-
let newPosts = 0
224-
for (const item of unpostedItems) {
225-
const link = item.link[0]
226-
const title = item.title[0]
227-
228-
console.log(`Posting item ${newPosts + 1}/${unpostedItems.length}: ${title}`)
229-
console.log(` URL: ${link}`)
238+
if (dryRunMode) {
239+
// Dry-run mode: save posts to JSON files instead of posting
240+
if (!existsSync(DRY_RUN_DIR)) {
241+
await mkdir(DRY_RUN_DIR, { recursive: true })
242+
}
230243

231-
const postText = `New blog post: "${title}"`
244+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").split("T")[0]
245+
const outputFile = join(DRY_RUN_DIR, `bluesky-${timestamp}.json`)
246+
247+
const dryRunData = {
248+
timestamp: new Date().toISOString(),
249+
total_rss_items: items.length,
250+
already_posted: postedUrls.size,
251+
new_posts: unpostedItems.length,
252+
posts: unpostedItems.map((item) => ({
253+
title: item.title[0],
254+
link: item.link[0],
255+
pubDate: item.pubDate?.[0],
256+
description: item.description?.[0],
257+
text: `New blog post: "${item.title[0]}"`,
258+
})),
259+
}
232260

233-
try {
234-
await createPost(accessJwt, did, postText, link, title)
235-
newPosts++
236-
console.log(` ✓ Posted successfully`)
261+
await writeFile(outputFile, JSON.stringify(dryRunData, null, 2), "utf-8")
237262

238-
// Rate limiting: createRecord costs 3 points, limit is 5000 points/hour = 1666 creates/hour
239-
// Wait 3 seconds between posts to stay well under the limit (~1200 posts/hour max)
240-
if (newPosts < unpostedItems.length) {
241-
console.log(` ⏱️ Waiting 3 seconds before next post...`)
242-
await new Promise((resolve) => setTimeout(resolve, 3000))
263+
console.log(`\n✅ Dry-run output saved to: ${outputFile}`)
264+
console.log(`\nWould post ${unpostedItems.length} items:`)
265+
unpostedItems.forEach((item, idx) => {
266+
console.log(` ${idx + 1}. ${item.title[0]}`)
267+
console.log(` ${item.link[0]}`)
268+
})
269+
} else {
270+
// Live mode: actually post to Bluesky
271+
let newPosts = 0
272+
for (const item of unpostedItems) {
273+
const link = item.link[0]
274+
const title = item.title[0]
275+
276+
console.log(`Posting item ${newPosts + 1}/${unpostedItems.length}: ${title}`)
277+
console.log(` URL: ${link}`)
278+
279+
const postText = `New blog post: "${title}"`
280+
281+
try {
282+
await createPost(accessJwt, did, postText, link, title)
283+
newPosts++
284+
console.log(` ✓ Posted successfully`)
285+
286+
// Rate limiting: createRecord costs 3 points, limit is 5000 points/hour = 1666 creates/hour
287+
// Wait 3 seconds between posts to stay well under the limit (~1200 posts/hour max)
288+
if (newPosts < unpostedItems.length) {
289+
console.log(` ⏱️ Waiting 3 seconds before next post...`)
290+
await new Promise((resolve) => setTimeout(resolve, 3000))
291+
}
292+
} catch (error) {
293+
console.error(` ✗ Failed to post: ${error.message}`)
294+
// Continue with other posts even if one fails
243295
}
244-
} catch (error) {
245-
console.error(` ✗ Failed to post: ${error.message}`)
246-
// Continue with other posts even if one fails
247296
}
248-
}
249297

250-
console.log(`\nPosted ${newPosts} new items to Bluesky`)
298+
console.log(`\nPosted ${newPosts} new items to Bluesky`)
299+
}
251300
}
252301

253302
main().catch((error) => {

content/private

Submodule private updated from ebf88ad to 010897f
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"timestamp": "2026-02-06T00:50:20.726Z",
3+
"total_rss_items": 40,
4+
"already_posted": 40,
5+
"new_posts": 1,
6+
"posts": [
7+
{
8+
"title": "Systems: Week 7",
9+
"link": "https://blog.chaoticgood.computer/content/notes/scratch/2026-W06",
10+
"pubDate": "Sun, 01 Feb 2026 00:00:00 GMT",
11+
"description": " (1 min read) ",
12+
"text": "New blog post: \"Systems: Week 7\""
13+
}
14+
]
15+
}

content/public/content/notes/periodic/weekly/2026/2026-W06.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ tags:
77

88
### Systems: Week 7 — As old as they are, emails still feel like magic
99

10-
Alright — if this system works, I might actually cry.
10+
Alright — if this system works, I'm going to flip.
11+
12+
After showing the newest version of the vault to people, a lot of the response I get is "This is cool, but I'm not going to check this URL compulsively. Is there a subscription somewhere?". This is a starter system for the email subscription (and, as an extension, the Bluesky incremental post). It's rough, but it's something to start with.
1113

1214
#### Notes from this week
1315

0 commit comments

Comments
 (0)