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"
46import { parseStringPromise } from "xml2js"
57
68const ATPROTO_IDENTIFIER = process . env . ATPROTO_IDENTIFIER
79const ATPROTO_POST_KEY = process . env . ATPROTO_POST_KEY
810const RSS_FILE = process . env . RSS_FILE
11+ const DRY_RUN_DIR = "content/public/assets/bsky-tests"
912const 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 = / h t t p s ? : \/ \/ [ ^ \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 */
200203async 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
253302main ( ) . catch ( ( error ) => {
0 commit comments