Skip to content

Instantly share code, notes, and snippets.

@skorfmann
Last active November 12, 2024 12:57
Show Gist options
  • Save skorfmann/8435d6bbb545d030540b76a590238621 to your computer and use it in GitHub Desktop.
Save skorfmann/8435d6bbb545d030540b76a590238621 to your computer and use it in GitHub Desktop.

Built this as one off script to post a bunch of records to bluesky based on a given csv to bootstrap https://bsky.app/profile/cdk.dev

sonnet 3.5 wrote all the code

import { XRPC, CredentialManager } from '@atcute/client'
import '@atcute/bluesky/lexicons'
import RichtextBuilder from '@atcute/bluesky-richtext-builder'
import { parse } from 'csv-parse/sync'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
async function fetchEmbedUrlCard(url: string, manager: CredentialManager) {
try {
const response = await fetch(url)
const html = await response.text()
const card: any = {
uri: url,
title: '',
description: ''
}
const titleMatch = html.match(/<meta property="og:title" content="([^"]+)"/)
if (titleMatch) card.title = titleMatch[1]
const descMatch = html.match(/<meta property="og:description" content="([^"]+)"/)
if (descMatch) card.description = descMatch[1]
const imgMatch = html.match(/<meta property="og:image" content="([^"]+)"/)
if (imgMatch) {
const imgUrl = imgMatch[1].startsWith('http') ? imgMatch[1] : `${url}${imgMatch[1]}`
const imgResponse = await fetch(imgUrl)
const contentType = imgResponse.headers.get('content-type')
if (!contentType?.startsWith('image/')) {
console.warn(`Skipping invalid image type: ${contentType} for URL: ${imgUrl}`)
return {
$type: 'app.bsky.embed.external',
external: card
}
}
const blob = await imgResponse.blob()
const blobResponse = await fetch('https://bsky.social/xrpc/com.atproto.repo.uploadBlob', {
method: 'POST',
headers: {
'Authorization': `Bearer ${await manager.session?.accessJwt}`
},
body: blob
})
const blobData = await blobResponse.json()
card.thumb = blobData.blob
}
return {
$type: 'app.bsky.embed.external',
external: card
}
} catch (error) {
console.error('Error fetching embed:', error)
return null
}
}
async function main() {
const manager = new CredentialManager({ service: 'https://bsky.social' })
const rpc = new XRPC({ handler: manager })
await manager.login({
identifier: `<user>`,
password: `<app password>`
})
const csvPath = resolve(__dirname, 'posts.csv')
const csvContent = readFileSync(csvPath, 'utf-8')
const records = parse(csvContent, {
columns: true,
skip_empty_lines: true
})
let index = 0;
for (const record of records) {
try {
// Calculate all lengths first
const tags = JSON.parse(record.categories)
const tagsLength = tags.reduce((acc: number, cat: any) =>
acc + cat.S.replace(/\s+/g, '').length + 2, 0) // +2 for # and space
const urlLength = record.url.length + 1 // +1 for space
const totalFixedLength = tagsLength + urlLength
const maxContentLength = 280 - totalFixedLength
// Determine content length and truncate if needed
let content = record.content
if (content.length > maxContentLength) {
console.warn(`Content too long (${content.length + totalFixedLength} total chars), truncating: ${record.title}`)
content = `${content.slice(0, maxContentLength - 3)}...` // -3 for ellipsis
}
// Now build the post with our pre-calculated content
const builder = new RichtextBuilder()
.addText(content)
.addText(' ')
// Add tags and URL
for (const cat of tags) {
builder.addTag(cat.S.replace(/\s+/g, ''))
builder.addText(' ')
}
builder.addLink(record.url, record.url)
const { text, facets } = builder.build()
const embed = await fetchEmbedUrlCard(record.url, manager)
// Use the createdAt from CSV and add minutes based on index
const createdAtDate = new Date(record.createdAt)
createdAtDate.setMinutes(createdAtDate.getMinutes() + index)
const result = await rpc.call('com.atproto.repo.createRecord', {
data: {
repo: await manager.session?.did!,
collection: 'app.bsky.feed.post',
record: {
text,
createdAt: createdAtDate.toISOString(),
facets,
embed,
$type: 'app.bsky.feed.post'
}
}
})
index++
console.log(`Posted: ${record.title}`)
console.log(result)
await new Promise(resolve => setTimeout(resolve, 5 * 1000))
} catch (error) {
console.error(`Error posting ${record.title}:`, error)
}
}
}
main().catch(console.error)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment