Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

at main 218 lines 7.1 kB view raw
1#!/usr/bin/env node 2 3/** 4 * Share Latest Painting to Bluesky 5 * 6 * Fetch the latest painting from a user's aesthetic.computer gallery 7 * and post it to Bluesky with the image embedded. 8 * 9 * Usage: 10 * node share-latest-painting.mjs @handle 11 * node share-latest-painting.mjs @handle --preview 12 * node share-latest-painting.mjs @handle --message "Custom message" 13 */ 14 15import { AtpAgent, RichText } from '@atproto/api' 16import { config } from 'dotenv' 17import { writeFileSync } from 'fs' 18 19config() 20 21const BSKY_SERVICE = process.env.BSKY_SERVICE || 'https://bsky.social' 22const BSKY_IDENTIFIER = process.env.BSKY_IDENTIFIER 23const BSKY_APP_PASSWORD = process.env.BSKY_APP_PASSWORD 24const AC_API = 'https://aesthetic.computer' 25 26// Parse command line arguments 27const args = process.argv.slice(2) 28const handle = args[0] 29const isPreview = args.includes('--preview') 30const messageIndex = args.indexOf('--message') 31const customMessage = messageIndex !== -1 ? args[messageIndex + 1] : null 32 33async function fetchLatestPainting(userHandle) { 34 console.log(`\n🔍 Fetching paintings for: ${userHandle}`) 35 36 // Remove @ if present 37 const cleanHandle = userHandle.startsWith('@') ? userHandle.slice(1) : userHandle 38 39 // Use TV API to get paintings, then filter by handle 40 const tvUrl = `${AC_API}/api/tv?limit=500` 41 console.log(`📡 Querying TV feed...\n`) 42 43 try { 44 const response = await fetch(tvUrl) 45 if (!response.ok) { 46 throw new Error(`HTTP ${response.status}: ${response.statusText}`) 47 } 48 49 const data = await response.json() 50 51 if (!data.media || !data.media.paintings) { 52 throw new Error('No paintings found in TV feed') 53 } 54 55 // Filter paintings by handle 56 const userPaintings = data.media.paintings.filter(painting => { 57 const paintingHandle = painting.owner.handle?.replace('@', '') || null 58 return paintingHandle === cleanHandle 59 }) 60 61 if (userPaintings.length === 0) { 62 throw new Error(`No paintings found for @${cleanHandle}`) 63 } 64 65 const latest = userPaintings[0] 66 67 console.log(`✅ Found ${userPaintings.length} paintings for @${cleanHandle}`) 68 console.log(`🎨 Latest: ${latest.slug}`) 69 console.log(`📅 When: ${new Date(latest.when).toLocaleString()}`) 70 console.log(`🔗 URL: ${latest.media.url}\n`) 71 72 return { 73 url: latest.media.url, 74 handle: cleanHandle, 75 slug: latest.slug, 76 when: latest.when, 77 totalPaintings: userPaintings.length 78 } 79 } catch (error) { 80 console.error('❌ Failed to fetch paintings:', error.message) 81 throw error 82 } 83} 84 85async function downloadPainting(url) { 86 console.log(`📥 Downloading painting from: ${url}`) 87 88 try { 89 const response = await fetch(url) 90 if (!response.ok) { 91 throw new Error(`HTTP ${response.status}: ${response.statusText}`) 92 } 93 94 const arrayBuffer = await response.arrayBuffer() 95 const buffer = Buffer.from(arrayBuffer) 96 97 console.log(`✅ Downloaded ${buffer.length} bytes\n`) 98 99 return buffer 100 } catch (error) { 101 console.error('❌ Failed to download painting:', error.message) 102 throw error 103 } 104} 105 106async function postToBluesky(paintingData, imageBuffer, message) { 107 if (!BSKY_IDENTIFIER || !BSKY_APP_PASSWORD) { 108 console.error('❌ Error: Missing Bluesky credentials') 109 console.error('\nPlease set in .env:') 110 console.error(' BSKY_IDENTIFIER=aesthetic.computer') 111 console.error(' BSKY_APP_PASSWORD=your-app-password') 112 process.exit(1) 113 } 114 115 console.log(`📤 Posting to Bluesky as @${BSKY_IDENTIFIER}`) 116 console.log(`📡 Using service: ${BSKY_SERVICE}\n`) 117 118 const agent = new AtpAgent({ service: BSKY_SERVICE }) 119 120 try { 121 // Login 122 console.log('🔐 Logging in...') 123 await agent.login({ 124 identifier: BSKY_IDENTIFIER, 125 password: BSKY_APP_PASSWORD 126 }) 127 console.log('✅ Logged in successfully\n') 128 129 // Upload image as blob 130 console.log('📤 Uploading painting to Bluesky...') 131 const uploadResponse = await agent.uploadBlob(imageBuffer, { 132 encoding: 'image/png' 133 }) 134 console.log('✅ Image uploaded as blob\n') 135 136 // Create post with embedded image and AC URL 137 const acUrl = `https://aesthetic.computer/painting~@${paintingData.handle}/${paintingData.slug}` 138 const postText = message || `New painting by @${paintingData.handle} 🎨✨\n\n${acUrl}` 139 140 console.log('📝 Creating post...') 141 console.log('───────────────────────────') 142 console.log(postText) 143 console.log('───────────────────────────\n') 144 145 // Use RichText to automatically detect and format URLs as links 146 const rt = new RichText({ text: postText }) 147 await rt.detectFacets(agent) 148 149 const postRecord = { 150 text: rt.text, 151 facets: rt.facets, 152 createdAt: new Date().toISOString(), 153 embed: { 154 $type: 'app.bsky.embed.images', 155 images: [{ 156 image: uploadResponse.data.blob, 157 alt: `Painting by @${paintingData.handle} on aesthetic.computer` 158 }] 159 } 160 } 161 162 const response = await agent.post(postRecord) 163 164 console.log('✅ Post created successfully!') 165 console.log(`🔗 URI: ${response.uri}`) 166 console.log(`🆔 CID: ${response.cid}`) 167 168 // Extract post ID from URI 169 const postId = response.uri.split('/').pop() 170 console.log(`🌐 View: https://bsky.app/profile/${BSKY_IDENTIFIER}/post/${postId}\n`) 171 172 return response 173 } catch (error) { 174 console.error('❌ Failed to post to Bluesky:', error.message) 175 throw error 176 } 177} 178 179async function main() { 180 if (!handle || handle.startsWith('--')) { 181 console.error('\n❌ Usage: node share-latest-painting.mjs @handle [--preview] [--message "text"]') 182 console.error('\nExamples:') 183 console.error(' node share-latest-painting.mjs @jeffrey') 184 console.error(' node share-latest-painting.mjs @jeffrey --preview') 185 console.error(' node share-latest-painting.mjs @jeffrey --message "Check out this artwork! 🎨"') 186 process.exit(1) 187 } 188 189 try { 190 // Fetch latest painting metadata 191 const paintingData = await fetchLatestPainting(handle) 192 193 // Download the painting image 194 const imageBuffer = await downloadPainting(paintingData.url) 195 196 if (isPreview) { 197 // Save preview to disk 198 const previewPath = './preview-painting.png' 199 writeFileSync(previewPath, imageBuffer) 200 console.log(`👀 Preview saved to: ${previewPath}`) 201 console.log(`\n📊 Painting Info:`) 202 console.log(` Handle: @${paintingData.handle}`) 203 console.log(` URL: ${paintingData.url}`) 204 console.log(` Total paintings: ${paintingData.totalPaintings}`) 205 console.log(` Size: ${imageBuffer.length} bytes`) 206 console.log(`\n💡 Run without --preview to post to Bluesky`) 207 } else { 208 // Post to Bluesky 209 await postToBluesky(paintingData, imageBuffer, customMessage) 210 } 211 212 } catch (error) { 213 console.error('\n💥 Error:', error.message) 214 process.exit(1) 215 } 216} 217 218main()