Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 196 lines 5.7 kB view raw
1#!/usr/bin/env node 2 3/** 4 * Bulk Share Paintings to Bluesky 5 * 6 * Post multiple paintings from the aesthetic.computer TV feed to Bluesky. 7 * 8 * Usage: 9 * node bulk-share-paintings.mjs 10 10 * node bulk-share-paintings.mjs 10 --delay 3000 11 */ 12 13import { AtpAgent, RichText } from '@atproto/api' 14import { config } from 'dotenv' 15 16config() 17 18const BSKY_SERVICE = process.env.BSKY_SERVICE || 'https://bsky.social' 19const BSKY_IDENTIFIER = process.env.BSKY_IDENTIFIER 20const BSKY_APP_PASSWORD = process.env.BSKY_APP_PASSWORD 21const AC_API = 'https://aesthetic.computer' 22 23const args = process.argv.slice(2) 24const count = parseInt(args[0]) || 10 25const delayIndex = args.indexOf('--delay') 26const delay = delayIndex !== -1 ? parseInt(args[delayIndex + 1]) : 2000 27 28let agent = null 29 30async function login() { 31 if (!BSKY_IDENTIFIER || !BSKY_APP_PASSWORD) { 32 console.error('❌ Error: Missing Bluesky credentials') 33 process.exit(1) 34 } 35 36 console.log(`🔐 Logging in as @${BSKY_IDENTIFIER}...`) 37 agent = new AtpAgent({ service: BSKY_SERVICE }) 38 await agent.login({ 39 identifier: BSKY_IDENTIFIER, 40 password: BSKY_APP_PASSWORD 41 }) 42 console.log('✅ Logged in successfully\n') 43} 44 45async function fetchPaintings(limit) { 46 console.log(`📡 Fetching ${limit} paintings from TV feed...\n`) 47 48 const response = await fetch(`${AC_API}/api/tv?limit=${limit}`) 49 if (!response.ok) { 50 throw new Error(`HTTP ${response.status}: ${response.statusText}`) 51 } 52 53 const data = await response.json() 54 55 if (!data.media || !data.media.paintings) { 56 throw new Error('No paintings found') 57 } 58 59 // Filter out paintings we might have already posted (basic check) 60 const paintings = data.media.paintings.filter(p => p.owner.handle) 61 62 console.log(`✅ Found ${paintings.length} paintings with handles\n`) 63 return paintings 64} 65 66async function downloadPainting(url) { 67 const response = await fetch(url) 68 if (!response.ok) { 69 throw new Error(`HTTP ${response.status}`) 70 } 71 const arrayBuffer = await response.arrayBuffer() 72 return Buffer.from(arrayBuffer) 73} 74 75async function postPainting(painting, index, total) { 76 const handle = painting.owner.handle.replace('@', '') 77 const slug = painting.slug 78 const acUrl = `https://aesthetic.computer/painting~@${handle}/${slug}` 79 80 console.log(`\n[${index + 1}/${total}] 🎨 Posting painting by @${handle}`) 81 console.log(` Slug: ${slug}`) 82 console.log(` URL: ${acUrl}`) 83 84 try { 85 // Download painting 86 const imageBuffer = await downloadPainting(painting.media.url) 87 console.log(` ✅ Downloaded ${imageBuffer.length} bytes`) 88 89 // Upload to Bluesky 90 const uploadResponse = await agent.uploadBlob(imageBuffer, { 91 encoding: 'image/png' 92 }) 93 console.log(` ✅ Uploaded to Bluesky`) 94 95 // Create post with RichText for proper link formatting 96 const postText = `New painting by @${handle} 🎨✨\n\n${acUrl}` 97 98 const rt = new RichText({ text: postText }) 99 await rt.detectFacets(agent) 100 101 const postRecord = { 102 text: rt.text, 103 facets: rt.facets, 104 createdAt: new Date().toISOString(), 105 embed: { 106 $type: 'app.bsky.embed.images', 107 images: [{ 108 image: uploadResponse.data.blob, 109 alt: `Painting by @${handle} on aesthetic.computer` 110 }] 111 } 112 } 113 114 const response = await agent.post(postRecord) 115 const postId = response.uri.split('/').pop() 116 const bskyUrl = `https://bsky.app/profile/${BSKY_IDENTIFIER}/post/${postId}` 117 118 console.log(` ✅ Posted! ${bskyUrl}`) 119 120 return { success: true, handle, slug, bskyUrl } 121 } catch (error) { 122 console.error(` ❌ Failed: ${error.message}`) 123 return { success: false, handle, slug, error: error.message } 124 } 125} 126 127async function sleep(ms) { 128 return new Promise(resolve => setTimeout(resolve, ms)) 129} 130 131async function main() { 132 console.log(`\n🚀 Bulk Share Paintings to Bluesky\n`) 133 console.log(` Count: ${count}`) 134 console.log(` Delay: ${delay}ms between posts\n`) 135 console.log('═'.repeat(50) + '\n') 136 137 try { 138 // Login once 139 await login() 140 141 // Fetch paintings 142 const paintings = await fetchPaintings(count * 2) // Fetch extra in case some fail 143 144 if (paintings.length === 0) { 145 console.error('❌ No paintings to post') 146 process.exit(1) 147 } 148 149 // Post paintings 150 const results = [] 151 const toPost = paintings.slice(0, count) 152 153 for (let i = 0; i < toPost.length; i++) { 154 const result = await postPainting(toPost[i], i, toPost.length) 155 results.push(result) 156 157 // Delay between posts (except for the last one) 158 if (i < toPost.length - 1) { 159 console.log(` ⏳ Waiting ${delay}ms...`) 160 await sleep(delay) 161 } 162 } 163 164 // Summary 165 console.log('\n' + '═'.repeat(50)) 166 console.log('✨ Summary\n') 167 168 const successful = results.filter(r => r.success).length 169 const failed = results.filter(r => !r.success).length 170 171 console.log(`✅ Successful: ${successful}`) 172 console.log(`❌ Failed: ${failed}`) 173 console.log(`📊 Total: ${results.length}\n`) 174 175 if (successful > 0) { 176 console.log('🔗 Posted paintings:') 177 results.filter(r => r.success).forEach((r, i) => { 178 console.log(` ${i + 1}. @${r.handle} - ${r.bskyUrl}`) 179 }) 180 } 181 182 if (failed > 0) { 183 console.log('\n⚠️ Failed paintings:') 184 results.filter(r => !r.success).forEach((r, i) => { 185 console.log(` ${i + 1}. @${r.handle}/${r.slug} - ${r.error}`) 186 }) 187 } 188 189 console.log() 190 } catch (error) { 191 console.error('\n💥 Error:', error.message) 192 process.exit(1) 193 } 194} 195 196main()