A simple tool which lets you scrape twitter accounts and crosspost them to bluesky accounts! Comes with a CLI and a webapp for managing profiles! Works with images/videos/link embeds/threads.
at main 285 lines 10 kB view raw
1#!/usr/bin/env node 2 3let testsPassed = 0; 4let testsFailed = 0; 5 6function assert(condition, message) { 7 if (condition) { 8 console.log(`${message}`); 9 testsPassed++; 10 } else { 11 console.log(`${message}`); 12 testsFailed++; 13 } 14} 15 16console.log('Running logic tests...\n'); 17 18// Test 1: Twitter URL Manipulation 19console.log('Test 1: Twitter URL Manipulation (High Quality Download)'); 20{ 21 const url1 = 'https://pbs.twimg.com/media/ABC123.jpg'; 22 const highQuality1 = url1.includes('?') ? url1.replace('?', ':orig?') : url1 + ':orig'; 23 assert(highQuality1 === 'https://pbs.twimg.com/media/ABC123.jpg:orig', 'Should append :orig to plain URLs'); 24 25 const url2 = 'https://pbs.twimg.com/media/ABC123.jpg?format=jpg&name=small'; 26 const highQuality2 = url2.includes('?') ? url2.replace('?', ':orig?') : url2 + ':orig'; 27 assert( 28 highQuality2 === 'https://pbs.twimg.com/media/ABC123.jpg:orig?format=jpg&name=small', 29 'Should replace ? with :orig? for query URLs', 30 ); 31 32 const url3 = 'https://pbs.twimg.com/media/DEF456.png?name=large'; 33 const highQuality3 = url3.includes('?') ? url3.replace('?', ':orig?') : url3 + ':orig'; 34 assert(highQuality3 === 'https://pbs.twimg.com/media/DEF456.png:orig?name=large', 'Should work with PNGs too'); 35 console.log(); 36} 37 38// Test 2: Text Splitting Logic 39console.log('Test 2: Text Splitting Logic'); 40{ 41 function splitText(text, limit = 300) { 42 if (text.length <= limit) return [text]; 43 const chunks = []; 44 let remaining = text; 45 while (remaining.length > 0) { 46 if (remaining.length <= limit) { 47 chunks.push(remaining); 48 break; 49 } 50 let splitIndex = remaining.lastIndexOf('\n\n', limit); 51 if (splitIndex === -1) { 52 splitIndex = remaining.lastIndexOf('. ', limit); 53 if (splitIndex === -1) { 54 splitIndex = remaining.lastIndexOf(' ', limit); 55 if (splitIndex === -1) { 56 splitIndex = limit; 57 } 58 } else { 59 splitIndex += 1; 60 } 61 } 62 chunks.push(remaining.substring(0, splitIndex).trim()); 63 remaining = remaining.substring(splitIndex).trim(); 64 } 65 return chunks; 66 } 67 68 const text1 = 'Hello world'; 69 const result1 = splitText(text1, 300); 70 assert(result1.length === 1, 'Short text should not be split'); 71 assert(result1[0] === 'Hello world', 'Content should be preserved'); 72 73 const text2 = 'First paragraph.\n\nSecond paragraph.\n\nThird paragraph.'; 74 const result2 = splitText(text2, 50); 75 assert(result2.length >= 2, `Should split at paragraph breaks (got ${result2.length} chunks)`); 76 const allHaveContent = result2.every((c) => c.length > 0); 77 assert(allHaveContent, 'All chunks have content'); 78 console.log(); 79} 80 81// Test 3: MIME Type Detection 82console.log('Test 3: MIME Type Detection'); 83{ 84 const isPng = (mimeType) => mimeType === 'image/png'; 85 const isJpeg = (mimeType) => mimeType === 'image/jpeg' || mimeType === 'image/jpg'; 86 const isWebp = (mimeType) => mimeType === 'image/webp'; 87 const isGif = (mimeType) => mimeType === 'image/gif'; 88 const isAnimation = (mimeType) => isGif(mimeType) || isWebp(mimeType); 89 90 assert(isPng('image/png') === true, 'PNG detection works'); 91 assert(isPng('image/jpeg') === false, 'JPEG is not PNG'); 92 assert(isJpeg('image/jpeg') === true, 'JPEG detection works'); 93 assert(isJpeg('image/jpg') === true, 'JPEG with JPG extension detection works'); 94 assert(isWebp('image/webp') === true, 'WebP detection works'); 95 assert(isGif('image/gif') === true, 'GIF detection works'); 96 assert(isAnimation('image/webp') === true, 'WebP is animation'); 97 assert(isAnimation('image/gif') === true, 'GIF is animation'); 98 assert(isAnimation('image/jpeg') === false, 'JPEG is not animation'); 99 console.log(); 100} 101 102// Test 4: Aspect Ratio Calculation 103console.log('Test 4: Aspect Ratio Calculation'); 104{ 105 const sizes = { 106 large: { w: 1200, h: 800 }, 107 medium: { w: 600, h: 400 }, 108 small: { w: 300, h: 200 }, 109 }; 110 111 const getAspectRatio = (mediaSizes, originalInfo) => { 112 if (mediaSizes?.large) { 113 return { width: mediaSizes.large.w, height: mediaSizes.large.h }; 114 } else if (originalInfo) { 115 return { width: originalInfo.width, height: originalInfo.height }; 116 } 117 return undefined; 118 }; 119 120 const ratio1 = getAspectRatio(sizes, undefined); 121 assert(ratio1.width === 1200 && ratio1.height === 800, 'Uses large size when available'); 122 123 const ratio2 = getAspectRatio(undefined, { width: 1920, height: 1080 }); 124 assert(ratio2.width === 1920 && ratio2.height === 1080, 'Falls back to original_info'); 125 126 const ratio3 = getAspectRatio(undefined, undefined); 127 assert(ratio3 === undefined, 'Returns undefined when no data'); 128 console.log(); 129} 130 131// Test 5: Video Variant Sorting 132console.log('Test 5: Video Variant Sorting (Highest Quality First)'); 133{ 134 const variants = [ 135 { content_type: 'video/mp4', url: 'low.mp4', bitrate: 500000 }, 136 { content_type: 'video/mp4', url: 'high.mp4', bitrate: 2000000 }, 137 { content_type: 'video/mp4', url: 'medium.mp4', bitrate: 1000000 }, 138 { content_type: 'audio/mp4', url: 'audio.mp4', bitrate: 128000 }, 139 ]; 140 141 const mp4s = variants 142 .filter((v) => v.content_type === 'video/mp4') 143 .sort((a, b) => (b.bitrate || 0) - (a.bitrate || 0)); 144 145 assert(mp4s.length === 3, 'Should filter to only MP4 videos'); 146 assert(mp4s[0].url === 'high.mp4', 'Highest bitrate first'); 147 assert(mp4s[1].url === 'medium.mp4', 'Medium bitrate second'); 148 assert(mp4s[2].url === 'low.mp4', 'Low bitrate last'); 149 console.log(); 150} 151 152// Test 6: Size Formatting 153console.log('Test 6: Size Formatting'); 154{ 155 const formatSize = (bytes) => (bytes / 1024).toFixed(2) + ' KB'; 156 const formatSizeMB = (bytes) => (bytes / 1024 / 1024).toFixed(2) + ' MB'; 157 158 assert(formatSize(1024) === '1.00 KB', '1KB formats correctly'); 159 assert(formatSize(1536) === '1.50 KB', '1.5KB formats correctly'); 160 assert(formatSizeMB(1048576) === '1.00 MB', '1MB formats correctly'); 161 console.log(); 162} 163 164// Test 7: Fixed Delay (10 seconds) 165console.log('Test 7: Fixed Delay (10 seconds)'); 166{ 167 const wait = 10000; 168 assert(wait === 10000, 'Delay is fixed at 10 seconds'); 169 assert(wait >= 5000 && wait <= 15000, 'Delay is reasonable for pacing'); 170 console.log(); 171} 172 173// Test 8: Retry Logic Simulation 174console.log('Test 8: Retry Logic Simulation (High Quality -> Standard)'); 175{ 176 const runRetryTests = async () => { 177 // Test 8a: High quality succeeds 178 { 179 let highQualityFailed = false; 180 const downloadWithRetry = async (url) => { 181 const isHighQuality = url.includes(':orig'); 182 if (isHighQuality && highQualityFailed) { 183 throw new Error('High quality download failed'); 184 } 185 if (isHighQuality && !highQualityFailed) { 186 return { buffer: Buffer.from('high quality'), mimeType: 'image/jpeg' }; 187 } 188 return { buffer: Buffer.from('standard quality'), mimeType: 'image/jpeg' }; 189 }; 190 191 highQualityFailed = false; 192 const result1 = await downloadWithRetry('https://example.com/image.jpg:orig'); 193 assert(result1.buffer.toString() === 'high quality', 'High quality download succeeds when available'); 194 } 195 196 // Test 8b: High quality fails, falls back to standard 197 { 198 let callCount = 0; 199 const downloadWithRetry = async (url) => { 200 callCount++; 201 const isHighQuality = url.includes(':orig'); 202 203 if (isHighQuality && callCount === 1) { 204 throw new Error('High quality download failed'); 205 } 206 207 if (isHighQuality && callCount === 2) { 208 const fallbackUrl = url.replace(':orig?', '?'); 209 return { buffer: Buffer.from('standard quality'), mimeType: 'image/jpeg' }; 210 } 211 212 return { buffer: Buffer.from('standard quality'), mimeType: 'image/jpeg' }; 213 }; 214 215 const result2 = await downloadWithRetry('https://example.com/image.jpg:orig'); 216 assert(result2.buffer.toString() === 'standard quality', 'Falls back to standard quality on failure'); 217 } 218 219 // Test 8c: Standard URL doesn't use retry logic 220 { 221 const downloadWithRetry = async (url) => { 222 const isHighQuality = url.includes(':orig'); 223 if (isHighQuality) { 224 throw new Error('Should not be high quality'); 225 } 226 return { buffer: Buffer.from('standard quality'), mimeType: 'image/jpeg' }; 227 }; 228 229 const result3 = await downloadWithRetry('https://example.com/image.jpg'); 230 assert(result3.buffer.toString() === 'standard quality', 'Standard URL downloads directly'); 231 } 232 }; 233 234 runRetryTests().catch((err) => { 235 console.log(` ✗ Retry test error: ${err.message}`); 236 testsFailed++; 237 }); 238 console.log(); 239} 240 241// Test 9: Image Compression Quality Settings 242console.log('Test 9: Image Compression Quality Settings'); 243{ 244 const settings = { 245 jpeg: { quality: 92, mozjpeg: true }, 246 jpegFallback: { quality: 85, mozjpeg: true }, 247 png: { compressionLevel: 9, adaptiveFiltering: true }, 248 webp: { quality: 90, effort: 6 }, 249 }; 250 251 assert(settings.jpeg.quality === 92, 'JPEG quality is 92%'); 252 assert(settings.jpeg.mozjpeg === true, 'JPEG uses mozjpeg'); 253 assert(settings.jpegFallback.quality === 85, 'Fallback JPEG quality is 85%'); 254 assert(settings.png.compressionLevel === 9, 'PNG compression level is 9 (max)'); 255 assert(settings.webp.quality === 90, 'WebP quality is 90%'); 256 assert(settings.webp.effort === 6, 'WebP encoding effort is 6 (high)'); 257 console.log(); 258} 259 260// Test 10: Bluesky Size Limits 261console.log('Test 10: Bluesky Size Limits Compliance'); 262{ 263 const MAX_SIZE = 950 * 1024; 264 const LARGE_IMAGE_THRESHOLD = 2000; 265 const FALLBACK_THRESHOLD = 1600; 266 267 assert(MAX_SIZE === 972800, 'Max size is 950KB (972800 bytes)'); 268 assert(LARGE_IMAGE_THRESHOLD === 2000, 'Large image threshold is 2000px'); 269 assert(FALLBACK_THRESHOLD === 1600, 'Fallback threshold is 1600px'); 270 console.log(); 271} 272 273// Summary 274console.log('─'.repeat(40)); 275console.log(`Tests passed: ${testsPassed}`); 276console.log(`Tests failed: ${testsFailed}`); 277console.log('─'.repeat(40)); 278 279if (testsFailed > 0) { 280 console.log('\nSome tests failed!'); 281 process.exit(1); 282} else { 283 console.log('\nAll tests passed!'); 284 process.exit(0); 285}