Import Instagram archive to a Bluesky account
9
fork

Configure Feed

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

Refactor test modes into an AppConfig class that separates the explicit test mode env variables.

+192 -55
+112
src/config.test.ts
··· 1 + import { AppConfig } from './config'; 2 + 3 + describe('AppConfig', () => { 4 + const originalEnv = process.env; 5 + 6 + beforeEach(() => { 7 + jest.resetModules(); 8 + process.env = { ...originalEnv }; 9 + // Explicitly reset all test mode environment variables 10 + delete process.env.TEST_VIDEO_MODE; 11 + delete process.env.TEST_IMAGE_MODE; 12 + delete process.env.TEST_IMAGES_MODE; 13 + }); 14 + 15 + afterEach(() => { 16 + process.env = originalEnv; 17 + }); 18 + 19 + describe('fromEnv', () => { 20 + test('should create config with all test modes disabled by default', () => { 21 + const config = AppConfig.fromEnv(); 22 + expect(config.isTestModeEnabled()).toBe(false); 23 + }); 24 + 25 + test('should enable test video mode when TEST_VIDEO_MODE=1', () => { 26 + process.env.TEST_VIDEO_MODE = '1'; 27 + const config = AppConfig.fromEnv(); 28 + expect(config.isTestModeEnabled()).toBe(true); 29 + }); 30 + 31 + test('should enable test image mode when TEST_IMAGE_MODE=1', () => { 32 + process.env.TEST_IMAGE_MODE = '1'; 33 + const config = AppConfig.fromEnv(); 34 + expect(config.isTestModeEnabled()).toBe(true); 35 + }); 36 + 37 + test('should enable test images mode when TEST_IMAGES_MODE=1', () => { 38 + process.env.TEST_IMAGES_MODE = '1'; 39 + const config = AppConfig.fromEnv(); 40 + expect(config.isTestModeEnabled()).toBe(true); 41 + }); 42 + }); 43 + 44 + describe('validate', () => { 45 + test('should not throw when no test modes are enabled', () => { 46 + const config = AppConfig.fromEnv(); 47 + expect(() => config.validate()).not.toThrow(); 48 + }); 49 + 50 + test('should not throw when only one test mode is enabled', () => { 51 + process.env.TEST_VIDEO_MODE = '1'; 52 + const config = AppConfig.fromEnv(); 53 + expect(() => config.validate()).not.toThrow(); 54 + }); 55 + 56 + test('should throw when multiple test modes are enabled', () => { 57 + process.env.TEST_VIDEO_MODE = '1'; 58 + process.env.TEST_IMAGE_MODE = '1'; 59 + const config = AppConfig.fromEnv(); 60 + expect(() => config.validate()).toThrow('Cannot enable multiple test modes simultaneously'); 61 + }); 62 + }); 63 + 64 + describe('getArchiveFolder', () => { 65 + test('should return test_video folder when video mode enabled', () => { 66 + process.env.TEST_VIDEO_MODE = '1'; 67 + const config = AppConfig.fromEnv(); 68 + expect(config.getArchiveFolder()).toMatch(/transfer\/test_video$/); 69 + }); 70 + 71 + test('should return test_image folder when image mode enabled', () => { 72 + process.env.TEST_IMAGE_MODE = '1'; 73 + const config = AppConfig.fromEnv(); 74 + expect(config.getArchiveFolder()).toMatch(/transfer\/test_image$/); 75 + }); 76 + 77 + test('should return test_images folder when images mode enabled', () => { 78 + process.env.TEST_IMAGES_MODE = '1'; 79 + const config = AppConfig.fromEnv(); 80 + expect(config.getArchiveFolder()).toMatch(/transfer\/test_images$/); 81 + }); 82 + 83 + test('should return ARCHIVE_FOLDER when no test mode enabled', () => { 84 + process.env.ARCHIVE_FOLDER = '/custom/archive/path'; 85 + const config = AppConfig.fromEnv(); 86 + expect(config.getArchiveFolder()).toBe('/custom/archive/path'); 87 + }); 88 + }); 89 + 90 + describe('isTestModeEnabled', () => { 91 + test('should return false when no test modes are enabled', () => { 92 + const config = AppConfig.fromEnv(); 93 + expect(config.isTestModeEnabled()).toBe(false); 94 + }); 95 + 96 + test('should return true when any test mode is enabled', () => { 97 + const testCases = ['TEST_VIDEO_MODE', 'TEST_IMAGE_MODE', 'TEST_IMAGES_MODE']; 98 + 99 + testCases.forEach(testMode => { 100 + // Reset env for each test case 101 + process.env = { ...originalEnv }; 102 + delete process.env.TEST_VIDEO_MODE; 103 + delete process.env.TEST_IMAGE_MODE; 104 + delete process.env.TEST_IMAGES_MODE; 105 + 106 + process.env[testMode] = '1'; 107 + const config = AppConfig.fromEnv(); 108 + expect(config.isTestModeEnabled()).toBe(true); 109 + }); 110 + }); 111 + }); 112 + });
+73
src/config.ts
··· 1 + import * as dotenv from 'dotenv'; 2 + import path from 'path'; 3 + 4 + dotenv.config(); 5 + 6 + /** 7 + * Configuration for the application 8 + */ 9 + export class AppConfig { 10 + private readonly testVideoMode: boolean; 11 + private readonly testImageMode: boolean; 12 + private readonly testImagesMode: boolean; 13 + 14 + constructor(config: { 15 + testVideoMode: boolean; 16 + testImageMode: boolean; 17 + testImagesMode: boolean; 18 + }) { 19 + this.testVideoMode = config.testVideoMode; 20 + this.testImageMode = config.testImageMode; 21 + this.testImagesMode = config.testImagesMode; 22 + } 23 + 24 + /** 25 + * Creates a configuration object from environment variables 26 + */ 27 + static fromEnv(): AppConfig { 28 + return new AppConfig({ 29 + testVideoMode: process.env.TEST_VIDEO_MODE === '1', 30 + testImageMode: process.env.TEST_IMAGE_MODE === '1', 31 + testImagesMode: process.env.TEST_IMAGES_MODE === '1' 32 + }); 33 + } 34 + 35 + /** 36 + * Checks if any test mode is enabled 37 + */ 38 + isTestModeEnabled(): boolean { 39 + return this.testVideoMode || this.testImageMode || this.testImagesMode; 40 + } 41 + 42 + /** 43 + * Gets the archive folder path based on test configuration 44 + */ 45 + getArchiveFolder(): string { 46 + const rootDir = path.resolve(__dirname, '..'); 47 + 48 + if (this.testVideoMode) return path.join(rootDir, 'transfer/test_video'); 49 + if (this.testImageMode) return path.join(rootDir, 'transfer/test_image'); 50 + if (this.testImagesMode) return path.join(rootDir, 'transfer/test_images'); 51 + return process.env.ARCHIVE_FOLDER!; 52 + } 53 + 54 + /** 55 + * Validates that only one test mode is enabled at a time 56 + * @throws Error if multiple test modes are enabled 57 + */ 58 + validate(): void { 59 + const enabledModes = Object.entries({ 60 + testVideoMode: this.testVideoMode, 61 + testImageMode: this.testImageMode, 62 + testImagesMode: this.testImagesMode 63 + }) 64 + .filter(([_, enabled]) => enabled) 65 + .map(([mode]) => mode); 66 + 67 + if (enabledModes.length > 1) { 68 + throw new Error( 69 + `Cannot enable multiple test modes simultaneously: ${enabledModes.join(', ')}` 70 + ); 71 + } 72 + } 73 + }
+7 -55
src/instagram-to-bluesky.ts
··· 9 9 import { 10 10 EmbeddedMedia, ImageEmbed, ImageEmbedImpl, ImagesEmbedImpl, VideoEmbedImpl 11 11 } from './bluesky/index'; 12 + import { AppConfig } from './config'; 12 13 import { logger } from './logger/logger'; 13 14 import { MediaProcessResult, VideoMediaProcessResultImpl } from './media'; 14 15 import { InstagramExportedPost } from './media/InstagramExportedPost'; ··· 18 19 19 20 const API_RATE_LIMIT_DELAY = 3000; // https://docs.bsky.app/docs/advanced-guides/rate-limits 20 21 21 - /** 22 - * Returns the absolute path to the archive folder 23 - * @param TEST_VIDEO_MODE 24 - * @param TEST_IMAGE_MODE 25 - * @param TEST_IMAGES_MODE 26 - * @returns 27 - */ 28 - export function getArchiveFolder( 29 - TEST_VIDEO_MODE: boolean, 30 - TEST_IMAGE_MODE: boolean, 31 - TEST_IMAGES_MODE: boolean 32 - ) { 33 - const rootDir = path.resolve(__dirname, ".."); 34 - 35 - if (TEST_VIDEO_MODE) return path.join(rootDir, "transfer/test_video"); 36 - if (TEST_IMAGE_MODE) return path.join(rootDir, "transfer/test_image"); 37 - if (TEST_IMAGES_MODE) return path.join(rootDir, "transfer/test_images"); 38 - return process.env.ARCHIVE_FOLDER!; 39 - } 40 - 41 - /** 42 - * Validates test mode configuration 43 - * @throws Error if both test modes are enabled 44 - */ 45 - function validateTestConfig( 46 - TEST_VIDEO_MODE: boolean, 47 - TEST_IMAGE_MODE: boolean, 48 - TEST_IMAGES_MODE: boolean 49 - ) { 50 - // TODO check for more than one enabled. 51 - if (TEST_VIDEO_MODE && TEST_IMAGE_MODE && TEST_IMAGES_MODE) { 52 - throw new Error( 53 - "Cannot enable both TEST_VIDEO_MODE and TEST_IMAGE_MODE simultaneously" 54 - ); 55 - } 56 - } 57 - 58 22 export function formatDuration(milliseconds: number): string { 59 23 const minutes = Math.floor(milliseconds / (1000 * 60)); 60 24 const hours = Math.floor(minutes / 60); ··· 149 113 * 150 114 */ 151 115 export async function main() { 152 - // Set environment variables within function scope, allows mocked unit testing. 153 116 const SIMULATE = process.env.SIMULATE === "1"; 154 - const TEST_VIDEO_MODE = process.env.TEST_VIDEO_MODE === "1"; 155 - const TEST_IMAGE_MODE = process.env.TEST_IMAGE_MODE === "1"; 156 - const TEST_IMAGES_MODE = process.env.TEST_IMAGES_MODE === "1"; 157 - 158 - // TODO make test configuration object 159 - validateTestConfig(TEST_VIDEO_MODE, TEST_IMAGE_MODE, TEST_IMAGES_MODE); 117 + const config = AppConfig.fromEnv(); 118 + config.validate(); 160 119 161 120 let MIN_DATE: Date | undefined = process.env.MIN_DATE 162 121 ? new Date(process.env.MIN_DATE) ··· 165 124 ? new Date(process.env.MAX_DATE) 166 125 : undefined; 167 126 168 - // TODO make test configuration object 169 - const archivalFolder = getArchiveFolder( 170 - TEST_VIDEO_MODE, 171 - TEST_IMAGE_MODE, 172 - TEST_IMAGES_MODE 173 - ); 127 + const archivalFolder = config.getArchiveFolder(); 174 128 175 129 // Log begining of import with a start date time to calculate the total time. 176 130 const importStart: Date = new Date(); ··· 199 153 200 154 // Decide where to fetch post data to process from. 201 155 let postsJsonPath: string; 202 - if (TEST_VIDEO_MODE || TEST_IMAGE_MODE || TEST_IMAGES_MODE) { 203 - // Use test post(s) to validate functionality with a test account. 204 - postsJsonPath = path.join(archivalFolder, "posts.json"); 156 + if (config.isTestModeEnabled()) { 157 + postsJsonPath = path.join(archivalFolder, 'posts.json'); 205 158 logger.info( 206 159 `--- TEST mode is enabled, using content from ${archivalFolder} ---` 207 160 ); 208 161 } else { 209 - // Use real instagram exported posts. 210 162 postsJsonPath = path.join( 211 163 archivalFolder, 212 - "your_instagram_activity/content/posts_1.json" 164 + 'your_instagram_activity/content/posts_1.json' 213 165 ); 214 166 } 215 167