Import Instagram archive to a Bluesky account
9
fork

Configure Feed

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

Enhance Bluesky media embed handling with ImageEmbedImpl class

- Add new ImageEmbedImpl class to provide type-safe image embed creation
- Implement toJSON method for better logging and serialization
- Update media processing to use ImageEmbedImpl for consistent image embedding
- Improve error handling in media upload and processing
- Add mimeType to ImageEmbed interface for more comprehensive media information

+100 -55
+85 -48
src/bluesky.ts
··· 1 - import { AtpAgent, RichText, BlobRef } from '@atproto/api'; 2 - import { logger } from './logger'; 1 + import { AtpAgent, RichText, BlobRef } from "@atproto/api"; 2 + import { logger } from "./logger"; 3 3 4 4 export interface VideoEmbed { 5 - $type: 'app.bsky.embed.video'; 5 + $type: "app.bsky.embed.video"; 6 6 alt: string; 7 7 buffer: Buffer; 8 8 mimeType: string; ··· 15 15 } 16 16 17 17 export interface VideoEmbedPost { 18 - $type: 'app.bsky.embed.video'; 18 + $type: "app.bsky.embed.video"; 19 19 video: BlobRef; 20 20 mimeType: string; 21 21 size: number; 22 22 } 23 23 24 24 export interface ImageEmbed { 25 - $type: 'app.bsky.embed.images#image'; 25 + $type: "app.bsky.embed.images#image"; 26 26 alt: string; 27 27 image: Buffer | BlobRef; 28 + mimeType: string; 29 + } 30 + 31 + export class ImageEmbedImpl implements ImageEmbed { 32 + readonly $type = "app.bsky.embed.images#image"; 33 + 34 + constructor( 35 + public alt: string, 36 + public image: Buffer | BlobRef, 37 + public mimeType: string 38 + ) {} 39 + 40 + toJSON() { 41 + return { 42 + $type: this.$type, 43 + alt: this.alt, 44 + image: 45 + this.image instanceof Buffer 46 + ? "[Buffer length=" + this.image.length + "]" 47 + : this.image, 48 + }; 49 + } 28 50 } 29 51 30 52 export interface ImagesEmbed { 31 - $type: 'app.bsky.embed.images'; 53 + $type: "app.bsky.embed.images"; 32 54 images: ImageEmbed[]; 33 55 } 34 56 ··· 41 63 private readonly password: string; 42 64 43 65 constructor(username: string, password: string) { 44 - this.agent = new AtpAgent({ service: 'https://bsky.social' }); 66 + this.agent = new AtpAgent({ service: "https://bsky.social" }); 45 67 this.username = username; 46 68 this.password = password; 47 69 } 48 70 49 71 async login(): Promise<void> { 50 - logger.debug('Authenitcating with Bluesky atproto.'); 72 + logger.debug("Authenitcating with Bluesky atproto."); 51 73 try { 52 74 await this.agent.login({ 53 75 identifier: this.username, 54 76 password: this.password, 55 77 }); 56 - } catch(error) { 57 - logger.error('Authentication error'); 78 + } catch (error) { 79 + logger.error("Authentication error"); 58 80 throw error; 59 81 } 60 82 } ··· 62 84 /** 63 85 * Upload video file and get blob reference 64 86 */ 65 - async uploadVideo(buffer: Buffer, mimeType: string = 'video/mp4'): Promise<BlobRef> { 87 + async uploadVideo( 88 + buffer: Buffer, 89 + mimeType: string = "video/mp4" 90 + ): Promise<BlobRef> { 66 91 try { 67 - logger.debug('Starting video upload process...'); 68 - const response = await this.agent.uploadBlob(buffer, { encoding: mimeType }); 69 - 92 + logger.debug("Starting video upload process..."); 93 + const response = await this.agent.uploadBlob(buffer, { 94 + encoding: mimeType, 95 + }); 96 + 70 97 if (!response?.data?.blob) { 71 - throw new Error('Failed to get video upload reference'); 98 + throw new Error("Failed to get video upload reference"); 72 99 } 73 100 74 101 return response.data.blob; 75 102 } catch (error) { 76 - logger.error('Failed to upload video:', error); 103 + logger.error("Failed to upload video:", error); 77 104 throw error; 78 105 } 79 106 } ··· 81 108 /** 82 109 * Upload image file and get blob reference 83 110 */ 84 - async uploadImage(buffer: Buffer, mimeType: string = 'image/jpeg'): Promise<BlobRef> { 111 + async uploadImage( 112 + buffer: Buffer, 113 + mimeType: string = "image/jpeg" 114 + ): Promise<BlobRef> { 85 115 try { 86 - logger.debug('Uploading image...'); 87 - const response = await this.agent.uploadBlob(buffer, { encoding: mimeType }); 88 - 116 + logger.debug("Uploading image..."); 117 + const response = await this.agent.uploadBlob(buffer, { 118 + encoding: mimeType, 119 + }); 120 + 89 121 if (!response?.data?.blob) { 90 - throw new Error('Failed to get image upload reference'); 122 + throw new Error("Failed to get image upload reference"); 91 123 } 92 124 93 125 return response.data.blob; 94 126 } catch (error) { 95 - logger.error('Failed to upload image:', error); 127 + logger.error("Failed to upload image:", error); 96 128 throw error; 97 129 } 98 130 } ··· 101 133 if (!embeddedMedia) return undefined; 102 134 103 135 // Handle video embed 104 - if (!Array.isArray(embeddedMedia) && embeddedMedia.$type === 'app.bsky.embed.video') { 136 + if ( 137 + !Array.isArray(embeddedMedia) && 138 + embeddedMedia.$type === "app.bsky.embed.video" 139 + ) { 105 140 return { 106 - $type: 'app.bsky.embed.video', 141 + $type: "app.bsky.embed.video", 107 142 video: embeddedMedia.video!.ref, 108 143 mimeType: embeddedMedia.mimeType, 109 - size: embeddedMedia.video!.size 144 + size: embeddedMedia.video!.size, 110 145 }; 111 146 } 112 147 113 148 // Handle image embed(s) 114 149 if (Array.isArray(embeddedMedia) && embeddedMedia.length > 0) { 115 150 return { 116 - $type: 'app.bsky.embed.images', 117 - images: embeddedMedia.map(img => ({ 118 - $type: 'app.bsky.embed.images#image', 119 - alt: img.alt, 120 - image: img.image as BlobRef 121 - })) 151 + $type: "app.bsky.embed.images", 152 + images: embeddedMedia.map( 153 + (img) => 154 + new ImageEmbedImpl(img.alt, img.image as BlobRef, img.mimeType) 155 + ), 122 156 }; 123 157 } 124 158 125 159 return undefined; 126 160 } 127 161 128 - async createPost(postDate: Date, postText: string, embeddedMedia: any): Promise<string | null> { 162 + async createPost( 163 + postDate: Date, 164 + postText: string, 165 + embeddedMedia: any 166 + ): Promise<string | null> { 129 167 try { 130 168 // Handle image uploads if present 131 169 if (Array.isArray(embeddedMedia)) { 132 170 const uploadedImages = await Promise.all( 133 171 embeddedMedia.map(async (media) => { 134 172 const blob = await this.uploadImage(media.image, media.mimeType); 135 - return { 136 - $type: 'app.bsky.embed.images#image', 137 - alt: media.alt, 138 - image: blob 139 - }; 173 + return new ImageEmbedImpl(media.alt, blob, media.mimeType); 140 174 }) 141 175 ); 142 - 176 + 143 177 embeddedMedia = { 144 - $type: 'app.bsky.embed.images', 145 - images: uploadedImages 178 + $type: "app.bsky.embed.images", 179 + images: uploadedImages, 146 180 }; 147 - } else if (embeddedMedia?.$type === 'app.bsky.embed.video') { 181 + } else if (embeddedMedia?.$type === "app.bsky.embed.video") { 148 182 // Upload video first 149 - const blob = await this.uploadVideo(embeddedMedia.buffer, embeddedMedia.mimeType); 183 + const blob = await this.uploadVideo( 184 + embeddedMedia.buffer, 185 + embeddedMedia.mimeType 186 + ); 150 187 embeddedMedia.video = { 151 188 ref: blob, 152 189 mimeType: embeddedMedia.mimeType, 153 - size: embeddedMedia.buffer.length 190 + size: embeddedMedia.buffer.length, 154 191 }; 155 192 // Now transform the embed 156 193 embeddedMedia = this.determineEmbed(embeddedMedia); ··· 160 197 await rt.detectFacets(this.agent); 161 198 162 199 const postRecord = { 163 - $type: 'app.bsky.feed.post', 200 + $type: "app.bsky.feed.post", 164 201 text: rt.text, 165 202 facets: rt.facets, 166 203 createdAt: postDate.toISOString(), 167 - embed: embeddedMedia 204 + embed: embeddedMedia, 168 205 }; 169 206 170 207 const recordData = await this.agent.post(postRecord); 171 - const i = recordData.uri.lastIndexOf('/'); 208 + const i = recordData.uri.lastIndexOf("/"); 172 209 if (i > 0) { 173 210 const rkey = recordData.uri.substring(i + 1); 174 211 return `https://bsky.app/profile/${this.username}/post/${rkey}`; ··· 176 213 logger.warn(recordData); 177 214 return null; 178 215 } catch (error) { 179 - logger.error('Failed to create post:', error); 216 + logger.error("Failed to create post:", error); 180 217 return null; 181 218 } 182 219 } 183 - } 220 + }
+15 -7
src/media.ts
··· 1 - import { ImageEmbed, VideoEmbed } from './bluesky'; 1 + import { ImageEmbed, VideoEmbed, ImageEmbedImpl } from './bluesky'; 2 2 import { logger } from './logger'; 3 3 import { validateVideo } from './video'; 4 4 import FS from 'fs'; ··· 122 122 mimeType 123 123 } as VideoEmbed; 124 124 } else { 125 - (embeddedMedia as ImageEmbed[]).push({ 126 - $type: 'app.bsky.embed.images#image', 127 - alt: mediaText, 128 - image: mediaBuffer, 129 - mimeType 130 - } as ImageEmbed); 125 + try{ 126 + if(Array.isArray(embeddedMedia)) { 127 + embeddedMedia.push( 128 + new ImageEmbedImpl(mediaText, mediaBuffer, mimeType) 129 + ); 130 + } else { 131 + logger.error('Embedded media is not an array!!!'); 132 + logger.debug('Embedded media present instead of an array?', embeddedMedia); 133 + } 134 + } catch (error) { 135 + logger.error('Failed to push image into embedded media', error); 136 + logger.debug('Embedded media present instead of an array?', embeddedMedia); 137 + throw error; 138 + } 131 139 } 132 140 133 141 mediaCount++;