diff --git a/packages/client-instagram/eslint.config.mjs b/packages/client-instagram/eslint.config.mjs new file mode 100644 index 00000000000..92fe5bbebef --- /dev/null +++ b/packages/client-instagram/eslint.config.mjs @@ -0,0 +1,3 @@ +import eslintGlobalConfig from "../../eslint.config.mjs"; + +export default [...eslintGlobalConfig]; diff --git a/packages/client-instagram/src/index.ts b/packages/client-instagram/src/index.ts index 9c70cb42a1f..f322d78c7ea 100644 --- a/packages/client-instagram/src/index.ts +++ b/packages/client-instagram/src/index.ts @@ -6,50 +6,52 @@ import { InstagramInteractionService } from "./services/interaction"; import { InstagramPostService } from "./services/post"; export const InstagramClientInterface: Client = { - async start(runtime: IAgentRuntime) { - try { - // Validate configuration - const config = await validateInstagramConfig(runtime); - elizaLogger.log("Instagram client configuration validated"); - - // Initialize client and get initial state - const state = await initializeClient(runtime, config); - elizaLogger.log("Instagram client initialized"); - - // Create services - const postService = new InstagramPostService(runtime, state); - const interactionService = new InstagramInteractionService(runtime, state); - - // Start services - if (!config.INSTAGRAM_DRY_RUN) { - await postService.start(); - elizaLogger.log("Instagram post service started"); - - if (config.INSTAGRAM_ENABLE_ACTION_PROCESSING) { - await interactionService.start(); - elizaLogger.log("Instagram interaction service started"); + async start(runtime: IAgentRuntime) { + try { + // Validate configuration + const config = await validateInstagramConfig(runtime); + elizaLogger.log("Instagram client configuration validated"); + + // Initialize client and get initial state + const state = await initializeClient(runtime, config); + elizaLogger.log("Instagram client initialized"); + + // Create services + const postService = new InstagramPostService(runtime, state); + const interactionService = new InstagramInteractionService( + runtime, + state + ); + + // Start services + if (!config.INSTAGRAM_DRY_RUN) { + await postService.start(); + elizaLogger.log("Instagram post service started"); + + if (config.INSTAGRAM_ENABLE_ACTION_PROCESSING) { + await interactionService.start(); + elizaLogger.log("Instagram interaction service started"); + } + } else { + elizaLogger.log("Instagram client running in dry-run mode"); + } + + // Return manager instance + return { + post: postService, + interaction: interactionService, + state, + }; + } catch (error) { + elizaLogger.error("Failed to start Instagram client:", error); + throw error; } - } else { - elizaLogger.log("Instagram client running in dry-run mode"); - } - - // Return manager instance - return { - post: postService, - interaction: interactionService, - state - }; - - } catch (error) { - elizaLogger.error("Failed to start Instagram client:", error); - throw error; - } - }, - - async stop(runtime: IAgentRuntime) { - elizaLogger.log("Stopping Instagram client services..."); - // Cleanup will be handled by the services themselves - } + }, + + async stop(_runtime: IAgentRuntime) { + elizaLogger.log("Stopping Instagram client services..."); + // Cleanup will be handled by the services themselves + }, }; -export default InstagramClientInterface; \ No newline at end of file +export default InstagramClientInterface; diff --git a/packages/client-instagram/src/lib/auth.ts b/packages/client-instagram/src/lib/auth.ts index a2b956ce98f..307547996cf 100644 --- a/packages/client-instagram/src/lib/auth.ts +++ b/packages/client-instagram/src/lib/auth.ts @@ -1,6 +1,6 @@ // src/lib/auth.ts import { IAgentRuntime, elizaLogger } from "@elizaos/core"; -import { IgLoginTwoFactorRequiredError } from 'instagram-private-api'; +import { IgLoginTwoFactorRequiredError } from "instagram-private-api"; import { InstagramConfig } from "../environment"; import { InstagramState } from "../types"; import { fetchProfile } from "./profile"; @@ -10,96 +10,94 @@ import { createInitialState, getIgClient } from "./state"; * Authenticates with Instagram */ async function authenticate( - runtime: IAgentRuntime, - config: InstagramConfig + runtime: IAgentRuntime, + config: InstagramConfig ): Promise { - const ig = getIgClient(); - let state = createInitialState(); + const ig = getIgClient(); + const state = createInitialState(); - try { - // Generate device ID - ig.state.generateDevice(config.INSTAGRAM_USERNAME); - - // Attempt to load cached session - const cachedSession = await runtime.cacheManager.get('instagram/session'); - if (cachedSession) { - try { - await ig.state.deserialize(cachedSession); - const profile = await fetchProfile(runtime, config); - return { - ...state, - isInitialized: true, - profile - }; - } catch (error) { - elizaLogger.warn('Cached session invalid, proceeding with fresh login'); - } - } - - // Proceed with fresh login try { - await ig.account.login( - config.INSTAGRAM_USERNAME, - config.INSTAGRAM_PASSWORD - ); + // Generate device ID + ig.state.generateDevice(config.INSTAGRAM_USERNAME); + + // Attempt to load cached session + const cachedSession = + await runtime.cacheManager.get("instagram/session"); + if (cachedSession) { + try { + await ig.state.deserialize(cachedSession); + const profile = await fetchProfile(runtime, config); + return { + ...state, + isInitialized: true, + profile, + }; + } catch { + elizaLogger.warn( + `Cached session invalid, proceeding with fresh login` + ); + } + } - // Cache the session - const serialized = await ig.state.serialize(); - await runtime.cacheManager.set('instagram/session', serialized); + // Proceed with fresh login + try { + await ig.account.login( + config.INSTAGRAM_USERNAME, + config.INSTAGRAM_PASSWORD + ); - const profile = await fetchProfile(runtime, config); + // Cache the session + const serialized = await ig.state.serialize(); + await runtime.cacheManager.set("instagram/session", serialized); - return { - ...state, - isInitialized: true, - profile - }; + const profile = await fetchProfile(runtime, config); + return { + ...state, + isInitialized: true, + profile, + }; + } catch (error) { + if (error instanceof IgLoginTwoFactorRequiredError) { + // Handle 2FA if needed - would need to implement 2FA code generation + throw new Error("2FA authentication not yet implemented"); + } + throw error; + } } catch (error) { - if (error instanceof IgLoginTwoFactorRequiredError) { - // Handle 2FA if needed - would need to implement 2FA code generation - throw new Error('2FA authentication not yet implemented'); - } - throw error; + elizaLogger.error("Authentication failed:", error); + throw error; } - - } catch (error) { - elizaLogger.error('Authentication failed:', error); - throw error; - } } /** * Sets up webhooks for real-time updates if needed */ async function setupWebhooks() { - // Implement webhook setup - // This is a placeholder for future implementation + // Implement webhook setup + // This is a placeholder for future implementation } /** * Initializes the Instagram client */ export async function initializeClient( - runtime: IAgentRuntime, - config: InstagramConfig + runtime: IAgentRuntime, + config: InstagramConfig ): Promise { - try { - // Authenticate and get initial state - const state = await authenticate(runtime, config); + try { + // Authenticate and get initial state + const state = await authenticate(runtime, config); - // Set up webhook handlers if needed - await setupWebhooks(); + // Set up webhook handlers if needed + await setupWebhooks(); - return state; - } catch (error) { - elizaLogger.error('Failed to initialize Instagram client:', error); - throw error; - } + return state; + } catch (error) { + elizaLogger.error("Failed to initialize Instagram client:", error); + throw error; + } } // Export other authentication related functions if needed -export { - authenticate, - setupWebhooks -}; +export { authenticate, setupWebhooks }; diff --git a/packages/client-instagram/src/services/post.ts b/packages/client-instagram/src/services/post.ts index 2d1a94d9b5e..67b8cb78abe 100644 --- a/packages/client-instagram/src/services/post.ts +++ b/packages/client-instagram/src/services/post.ts @@ -1,8 +1,17 @@ // src/services/post.ts -import { IAgentRuntime, ModelClass, composeContext, elizaLogger, generateImage, generateText, getEmbeddingZeroVector, stringToUuid } from "@elizaos/core"; -import { promises as fs } from 'fs'; +import { + IAgentRuntime, + ModelClass, + composeContext, + elizaLogger, + generateImage, + generateText, + getEmbeddingZeroVector, + stringToUuid, +} from "@elizaos/core"; +import { promises as fs } from "fs"; import path from "path"; -import sharp from 'sharp'; +import sharp from "sharp"; import { getIgClient } from "../lib/state"; import { InstagramState } from "../types"; @@ -29,300 +38,331 @@ Your response should not contain any questions. Brief, concise statements only. Add up to 3 relevant hashtags at the end.`; interface PostOptions { - media: Array<{ - type: 'IMAGE' | 'VIDEO' | 'CAROUSEL'; - url: string; - }>; - caption?: string; + media: Array<{ + type: "IMAGE" | "VIDEO" | "CAROUSEL"; + url: string; + }>; + caption?: string; } export class InstagramPostService { - private runtime: IAgentRuntime; - private state: InstagramState; - private isProcessing: boolean = false; - private lastPostTime: number = 0; - private stopProcessing: boolean = false; - - constructor(runtime: IAgentRuntime, state: InstagramState) { - this.runtime = runtime; - this.state = state; - } - - async start() { - const generatePostLoop = async () => { - const lastPost = await this.runtime.cacheManager.get<{ timestamp: number }>( - 'instagram/lastPost' - ); - - const lastPostTimestamp = lastPost?.timestamp ?? 0; - const minMinutes = parseInt(this.runtime.getSetting('POST_INTERVAL_MIN') || '90', 10); - const maxMinutes = parseInt(this.runtime.getSetting('POST_INTERVAL_MAX') || '180', 10); - const randomMinutes = Math.floor(Math.random() * (maxMinutes - minMinutes + 1)) + minMinutes; - const delay = randomMinutes * 60 * 1000; - - if (Date.now() > lastPostTimestamp + delay) { - await this.generateNewPost(); - } - - if (!this.stopProcessing) { - setTimeout(generatePostLoop, delay); - } - - elizaLogger.log(`Next Instagram post scheduled in ${randomMinutes} minutes`); - }; - - // Start the loop - generatePostLoop(); - } - - async stop() { - this.stopProcessing = true; - } - - private async generateNewPost() { - try { - elizaLogger.log("Generating new Instagram post"); - - const roomId = stringToUuid( - "instagram_generate_room-" + this.state.profile?.username - ); - - await this.runtime.ensureUserExists( - this.runtime.agentId, - this.state.profile?.username || '', - this.runtime.character.name, - "instagram" - ); - - const topics = this.runtime.character.topics.join(", "); - - const state = await this.runtime.composeState( - { - userId: this.runtime.agentId, - roomId: roomId, - agentId: this.runtime.agentId, - content: { - text: topics || "", - action: "POST", - }, - }, - { - instagramUsername: this.state.profile?.username - } - ); - - const context = composeContext({ - state, - // TODO: Add back in when we have a template for Instagram on character - //template: this.runtime.character.templates?.instagramPostTemplate || instagramPostTemplate, - template: instagramPostTemplate, - - }); - - elizaLogger.debug("generate post prompt:\n" + context); - - const content = await generateText({ - runtime: this.runtime, - context, - modelClass: ModelClass.SMALL, - }); - - // Clean the generated content - let cleanedContent = ""; - - // Try parsing as JSON first - try { - const parsedResponse = JSON.parse(content); - if (parsedResponse.text) { - cleanedContent = parsedResponse.text; - } else if (typeof parsedResponse === "string") { - cleanedContent = parsedResponse; + private runtime: IAgentRuntime; + private state: InstagramState; + private isProcessing: boolean = false; + private lastPostTime: number = 0; + private stopProcessing: boolean = false; + + constructor(runtime: IAgentRuntime, state: InstagramState) { + this.runtime = runtime; + this.state = state; + } + + async start() { + const generatePostLoop = async () => { + const lastPost = await this.runtime.cacheManager.get<{ + timestamp: number; + }>("instagram/lastPost"); + + const lastPostTimestamp = lastPost?.timestamp ?? 0; + const minMinutes = parseInt( + this.runtime.getSetting("POST_INTERVAL_MIN") || "90", + 10 + ); + const maxMinutes = parseInt( + this.runtime.getSetting("POST_INTERVAL_MAX") || "180", + 10 + ); + const randomMinutes = + Math.floor(Math.random() * (maxMinutes - minMinutes + 1)) + + minMinutes; + const delay = randomMinutes * 60 * 1000; + + if (Date.now() > lastPostTimestamp + delay) { + await this.generateNewPost(); + } + + if (!this.stopProcessing) { + setTimeout(generatePostLoop, delay); + } + + elizaLogger.log( + `Next Instagram post scheduled in ${randomMinutes} minutes` + ); + }; + + // Start the loop + generatePostLoop(); + } + + async stop() { + this.stopProcessing = true; + } + + private async generateNewPost() { + try { + elizaLogger.log("Generating new Instagram post"); + + const roomId = stringToUuid( + "instagram_generate_room-" + this.state.profile?.username + ); + + await this.runtime.ensureUserExists( + this.runtime.agentId, + this.state.profile?.username || "", + this.runtime.character.name, + "instagram" + ); + + const topics = this.runtime.character.topics.join(", "); + + const state = await this.runtime.composeState( + { + userId: this.runtime.agentId, + roomId: roomId, + agentId: this.runtime.agentId, + content: { + text: topics || "", + action: "POST", + }, + }, + { + instagramUsername: this.state.profile?.username, + } + ); + + const context = composeContext({ + state, + // TODO: Add back in when we have a template for Instagram on character + //template: this.runtime.character.templates?.instagramPostTemplate || instagramPostTemplate, + template: instagramPostTemplate, + }); + + elizaLogger.debug("generate post prompt:\n" + context); + + const content = await generateText({ + runtime: this.runtime, + context, + modelClass: ModelClass.SMALL, + }); + + // Clean the generated content + let cleanedContent = ""; + + // Try parsing as JSON first + try { + const parsedResponse = JSON.parse(content); + if (parsedResponse.text) { + cleanedContent = parsedResponse.text; + } else if (typeof parsedResponse === "string") { + cleanedContent = parsedResponse; + } + } catch { + // If not JSON, clean the raw content + cleanedContent = content + .replace(/^\s*{?\s*"text":\s*"|"\s*}?\s*$/g, "") // Remove JSON-like wrapper + .replace(/^['"](.*)['"]$/g, "$1") // Remove quotes + .replace(/\\"/g, '"') // Unescape quotes + .replace(/\\n/g, "\n\n") // Unescape newlines + .trim(); + } + + if (!cleanedContent) { + elizaLogger.error( + "Failed to extract valid content from response:", + { + rawResponse: content, + attempted: "JSON parsing", + } + ); + return; + } + + // For Instagram, we need to generate or get an image + const mediaUrl = await this.getOrGenerateImage(cleanedContent); + + await this.createPost({ + media: [ + { + type: "IMAGE", + url: mediaUrl, + }, + ], + caption: cleanedContent, + }); + + // Create memory of the post + await this.runtime.messageManager.createMemory({ + id: stringToUuid(`instagram-post-${Date.now()}`), + userId: this.runtime.agentId, + agentId: this.runtime.agentId, + content: { + text: cleanedContent, + source: "instagram", + }, + roomId, + embedding: getEmbeddingZeroVector(), + createdAt: Date.now(), + }); + } catch (error) { + elizaLogger.error("Error generating Instagram post:", { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + phase: "generateNewPost", + }); } - } catch (error) { - // If not JSON, clean the raw content - cleanedContent = content - .replace(/^\s*{?\s*"text":\s*"|"\s*}?\s*$/g, "") // Remove JSON-like wrapper - .replace(/^['"](.*)['"]$/g, "$1") // Remove quotes - .replace(/\\"/g, '"') // Unescape quotes - .replace(/\\n/g, "\n\n") // Unescape newlines - .trim(); - } - - if (!cleanedContent) { - elizaLogger.error("Failed to extract valid content from response:", { - rawResponse: content, - attempted: "JSON parsing", - }); - return; - } - - // For Instagram, we need to generate or get an image - const mediaUrl = await this.getOrGenerateImage(cleanedContent); - - await this.createPost({ - media: [{ - type: 'IMAGE', - url: mediaUrl - }], - caption: cleanedContent - }); - - // Create memory of the post - await this.runtime.messageManager.createMemory({ - id: stringToUuid(`instagram-post-${Date.now()}`), - userId: this.runtime.agentId, - agentId: this.runtime.agentId, - content: { - text: cleanedContent, - source: "instagram", - }, - roomId, - embedding: getEmbeddingZeroVector(), - createdAt: Date.now(), - }); - - } catch (error) { - elizaLogger.error("Error generating Instagram post:", { - error: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : undefined, - phase: 'generateNewPost' - }); } - } - - // Placeholder - implement actual image generation/selection - private async getOrGenerateImage(content: string): Promise { - try { - elizaLogger.log("Generating image for Instagram post"); - - const result = await generateImage({ - prompt: content, - width: 1024, - height: 1024, - count: 1, - numIterations: 50, - guidanceScale: 7.5 - }, this.runtime); - - if (!result.success || !result.data || result.data.length === 0) { - throw new Error("Failed to generate image: " + (result.error || "No image data returned")); - } - - // Save the base64 image to a temporary file - const imageData = result.data[0].replace(/^data:image\/\w+;base64,/, ''); - const tempDir = path.resolve(process.cwd(), 'temp'); - await fs.mkdir(tempDir, { recursive: true }); - const tempFile = path.join(tempDir, `instagram-post-${Date.now()}.png`); - await fs.writeFile(tempFile, Buffer.from(imageData, 'base64')); - - return tempFile; - } catch (error) { - elizaLogger.error("Error generating image:", { - error: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : undefined, - phase: 'getOrGenerateImage' - }); - throw error; + + // Placeholder - implement actual image generation/selection + private async getOrGenerateImage(content: string): Promise { + try { + elizaLogger.log("Generating image for Instagram post"); + + const result = await generateImage( + { + prompt: content, + width: 1024, + height: 1024, + count: 1, + numIterations: 50, + guidanceScale: 7.5, + }, + this.runtime + ); + + if (!result.success || !result.data || result.data.length === 0) { + throw new Error( + "Failed to generate image: " + + (result.error || "No image data returned") + ); + } + + // Save the base64 image to a temporary file + const imageData = result.data[0].replace( + /^data:image\/\w+;base64,/, + "" + ); + const tempDir = path.resolve(process.cwd(), "temp"); + await fs.mkdir(tempDir, { recursive: true }); + const tempFile = path.join( + tempDir, + `instagram-post-${Date.now()}.png` + ); + await fs.writeFile(tempFile, Buffer.from(imageData, "base64")); + + return tempFile; + } catch (error) { + elizaLogger.error("Error generating image:", { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + phase: "getOrGenerateImage", + }); + throw error; + } } - } - - async createPost(options: PostOptions) { - const ig = getIgClient(); - - try { - elizaLogger.log("Creating Instagram post", { - mediaCount: options.media.length, - hasCaption: !!options.caption - }); - - // Process media - const processedMedia = await Promise.all( - options.media.map(async (media) => { - const buffer = await this.processMedia(media); - return { - ...media, - buffer - }; - }) - ); - - // Handle different post types - if (processedMedia.length > 1) { - // Create carousel post - await ig.publish.album({ - items: processedMedia.map(media => ({ - file: media.buffer, - caption: options.caption - })) - }); - } else { - // Single image/video post - const media = processedMedia[0]; - if (media.type === 'VIDEO') { - await ig.publish.video({ - video: media.buffer, - caption: options.caption, - coverImage: media.buffer - }); - } else { - await ig.publish.photo({ - file: media.buffer, - caption: options.caption - }); + + async createPost(options: PostOptions) { + const ig = getIgClient(); + + try { + elizaLogger.log("Creating Instagram post", { + mediaCount: options.media.length, + hasCaption: !!options.caption, + }); + + // Process media + const processedMedia = await Promise.all( + options.media.map(async (media) => { + const buffer = await this.processMedia(media); + return { + ...media, + buffer, + }; + }) + ); + + // Handle different post types + if (processedMedia.length > 1) { + // Create carousel post + await ig.publish.album({ + items: processedMedia.map((media) => ({ + file: media.buffer, + caption: options.caption, + })), + }); + } else { + // Single image/video post + const media = processedMedia[0]; + if (media.type === "VIDEO") { + await ig.publish.video({ + video: media.buffer, + caption: options.caption, + coverImage: media.buffer, + }); + } else { + await ig.publish.photo({ + file: media.buffer, + caption: options.caption, + }); + } + } + + // Update last post time + this.lastPostTime = Date.now(); + await this.runtime.cacheManager.set("instagram/lastPost", { + timestamp: this.lastPostTime, + }); + + elizaLogger.log("Instagram post created successfully"); + } catch (error) { + elizaLogger.error("Error creating Instagram post:", { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + phase: "createPost", + mediaCount: options.media.length, + hasCaption: !!options.caption, + }); + throw error; } - } - - // Update last post time - this.lastPostTime = Date.now(); - await this.runtime.cacheManager.set('instagram/lastPost', { - timestamp: this.lastPostTime - }); - - elizaLogger.log("Instagram post created successfully"); - } catch (error) { - elizaLogger.error("Error creating Instagram post:", { - error: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : undefined, - phase: 'createPost', - mediaCount: options.media.length, - hasCaption: !!options.caption - }); - throw error; } - } - - private async processMedia(media: { type: string; url: string }): Promise { - try { - elizaLogger.log("Processing media", { type: media.type, url: media.url }); - - // Read file directly from filesystem instead of using fetch - const buffer = await fs.readFile(media.url); - - if (media.type === 'IMAGE') { - // Process image with sharp - return await sharp(buffer) - .resize(1080, 1080, { - fit: 'inside', - withoutEnlargement: true - }) - .jpeg({ - quality: 85, - progressive: true - }) - .toBuffer(); - } - - // For other types, return original buffer - return buffer; - } catch (error) { - elizaLogger.error("Error processing media:", { - error: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : undefined, - phase: 'processMedia', - mediaType: media.type, - url: media.url - }); - throw error; + + private async processMedia(media: { + type: string; + url: string; + }): Promise { + try { + elizaLogger.log("Processing media", { + type: media.type, + url: media.url, + }); + + // Read file directly from filesystem instead of using fetch + const buffer = await fs.readFile(media.url); + + if (media.type === "IMAGE") { + // Process image with sharp + return await sharp(buffer) + .resize(1080, 1080, { + fit: "inside", + withoutEnlargement: true, + }) + .jpeg({ + quality: 85, + progressive: true, + }) + .toBuffer(); + } + + // For other types, return original buffer + return buffer; + } catch (error) { + elizaLogger.error("Error processing media:", { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + phase: "processMedia", + mediaType: media.type, + url: media.url, + }); + throw error; + } } - } -} \ No newline at end of file +}