/** * Meta AI Client for Image Generation * * Uses the Meta AI web interface via GraphQL * Requires cookies from a logged-in meta.ai session * * Image Model: Imagine (Emu) * * Based on: https://github.com/Strvm/meta-ai-api */ const META_AI_BASE = "https://www.meta.ai"; const GRAPHQL_ENDPOINT = `${META_AI_BASE}/api/graphql/`; interface MetaAIOptions { cookies: string; } interface MetaImageResult { url: string; data?: string; // base64 prompt: string; model: string; } interface MetaSession { lsd?: string; fb_dtsg?: string; accessToken?: string; } export class MetaAIClient { private cookies: string; private session: MetaSession = {}; constructor(options: MetaAIOptions) { this.cookies = this.normalizeCookies(options.cookies); this.parseSessionFromCookies(); } /** * Normalize cookies from string or JSON format * Handles cases where user pastes JSON array from extension/devtools */ private normalizeCookies(cookies: string): string { if (!cookies) return ""; try { // Check if it looks like JSON if (cookies.trim().startsWith('[')) { const parsed = JSON.parse(cookies); if (Array.isArray(parsed)) { return parsed .map((c: any) => `${c.name}=${c.value}`) .join('; '); } } } catch (e) { // Not JSON, assume string } return cookies; } /** * Parse session tokens from cookies */ private parseSessionFromCookies(): void { // Extract lsd token if present in cookies const lsdMatch = this.cookies.match(/lsd=([^;]+)/); if (lsdMatch) { this.session.lsd = lsdMatch[1]; } // Extract fb_dtsg if present const dtsgMatch = this.cookies.match(/fb_dtsg=([^;]+)/); if (dtsgMatch) { this.session.fb_dtsg = dtsgMatch[1]; } } /** * Generate images using Meta AI's Imagine model */ async generate(prompt: string, numImages: number = 4): Promise { console.log(`[Meta AI] Generating images for: "${prompt.substring(0, 50)}..."`); // First, get the access token and session info if not already fetched if (!this.session.accessToken) { await this.initSession(); } // Use "Imagine" prefix for image generation const imagePrompt = prompt.toLowerCase().startsWith('imagine') ? prompt : `Imagine ${prompt}`; // Send the prompt via GraphQL const response = await this.sendPrompt(imagePrompt); // Extract image URLs from response const images = this.extractImages(response, prompt); if (images.length === 0) { // Meta AI might return the prompt response without images immediately // We may need to poll for the result console.log("[Meta AI] No images in initial response, polling..."); return this.pollForImages(response, prompt); } return images; } /** * Initialize session - get access token from meta.ai page */ private async initSession(): Promise { console.log("[Meta AI] Initializing session..."); const response = await fetch(META_AI_BASE, { headers: { "Cookie": this.cookies, "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", "Sec-Fetch-Site": "same-origin", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Dest": "document", "Accept-Language": "en-US,en;q=0.9" } }); const html = await response.text(); // Extract access token from page HTML // Pattern 1: Simple JSON key let tokenMatch = html.match(/"accessToken":"([^"]+)"/); // Pattern 2: Inside a config object or different spacing if (!tokenMatch) { tokenMatch = html.match(/accessToken["']\s*:\s*["']([^"']+)["']/); } // Pattern 3: LSD token backup (sometimes needed) const lsdMatch = html.match(/"LSD",\[\],{"token":"([^"]+)"/) || html.match(/"lsd":"([^"]+)"/) || html.match(/name="lsd" value="([^"]+)"/); if (lsdMatch) { this.session.lsd = lsdMatch[1]; } // Pattern 4: DTSG token (critical for some requests) const dtsgMatch = html.match(/"DTSGInitialData".*?"token":"([^"]+)"/) || html.match(/"token":"([^"]+)"/); // Less specific fallback if (tokenMatch) { this.session.accessToken = tokenMatch[1]; console.log("[Meta AI] Got access token"); } else if (html.includes('login_form') || html.includes('login_page')) { throw new Error("Meta AI: Cookies expired or invalid (Login page detected). Please update cookies."); } else { console.warn("[Meta AI] Warning: Failed to extract access token. functionality may be limited."); console.log("HTML Preview:", html.substring(0, 200)); // Don't throw here, try to proceed with just cookies/LSD } if (dtsgMatch) { this.session.fb_dtsg = dtsgMatch[1]; } // We no longer strictly enforce accessToken presence here // as some requests might work with just cookies } /** * Send prompt via GraphQL mutation */ private async sendPrompt(prompt: string): Promise { const variables = { message: { text: prompt, content_type: "TEXT" }, source: "PDT_CHAT_INPUT", external_message_id: Math.random().toString(36).substring(2) + Date.now().toString(36) }; const body = new URLSearchParams({ fb_api_caller_class: "RelayModern", fb_api_req_friendly_name: "useAbraSendMessageMutation", variables: JSON.stringify(variables), doc_id: "7783822248314888", ...(this.session.lsd && { lsd: this.session.lsd }), ...(this.session.fb_dtsg && { fb_dtsg: this.session.fb_dtsg }) }); const response = await fetch(GRAPHQL_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", "Cookie": this.cookies, "Origin": META_AI_BASE, "Referer": `${META_AI_BASE}/`, "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", "Sec-Fetch-Site": "same-origin", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Dest": "empty", "Accept-Language": "en-US,en;q=0.9", ...(this.session.accessToken && { "Authorization": `OAuth ${this.session.accessToken}` }) }, body: body.toString() }); if (!response.ok) { const errorText = await response.text(); console.error("[Meta AI] GraphQL Error:", response.status, errorText); throw new Error(`Meta AI Error: ${response.status} - ${errorText.substring(0, 200)}`); } // Check if response is actually JSON const contentType = response.headers.get("content-type"); if (contentType && contentType.includes("text/html")) { const text = await response.text(); if (text.includes("login_form") || text.includes("facebook.com/login")) { throw new Error("Meta AI: Session expired. Please refresh your cookies."); } throw new Error(`Meta AI returned HTML error: ${text.substring(0, 100)}...`); } const data = await response.json(); console.log("[Meta AI] Response:", JSON.stringify(data, null, 2).substring(0, 500)); return data; } /** * Extract image URLs from Meta AI response */ private extractImages(response: any, originalPrompt: string): MetaImageResult[] { const images: MetaImageResult[] = []; // Navigate through the response structure const messageData = response?.data?.node?.bot_response_message || response?.data?.xabraAIPreviewMessageSendMutation?.message; if (!messageData) { return images; } // Check for imagine_card (image generation response) const imagineCard = messageData?.imagine_card; if (imagineCard?.session?.media_sets) { for (const mediaSet of imagineCard.session.media_sets) { if (mediaSet?.imagine_media) { for (const media of mediaSet.imagine_media) { if (media?.uri) { images.push({ url: media.uri, prompt: originalPrompt, model: "imagine" }); } } } } } // Check for attachments const attachments = messageData?.attachments; if (attachments) { for (const attachment of attachments) { if (attachment?.media?.image_uri) { images.push({ url: attachment.media.image_uri, prompt: originalPrompt, model: "imagine" }); } } } return images; } /** * Poll for image generation completion */ private async pollForImages(initialResponse: any, prompt: string): Promise { const maxAttempts = 30; const pollInterval = 2000; // Get the fetch_id from initial response for polling const fetchId = initialResponse?.data?.node?.id || initialResponse?.data?.xabraAIPreviewMessageSendMutation?.message?.id; if (!fetchId) { console.warn("[Meta AI] No fetch ID for polling, returning empty"); return []; } for (let attempt = 0; attempt < maxAttempts; attempt++) { console.log(`[Meta AI] Polling attempt ${attempt + 1}/${maxAttempts}...`); await new Promise(resolve => setTimeout(resolve, pollInterval)); try { // Query for the message status const statusResponse = await this.queryMessageStatus(fetchId); const images = this.extractImages(statusResponse, prompt); if (images.length > 0) { console.log(`[Meta AI] Got ${images.length} images!`); return images; } // Check if generation failed const status = statusResponse?.data?.node?.imagine_card?.session?.status; if (status === "FAILED" || status === "ERROR") { throw new Error("Meta AI image generation failed"); } } catch (e) { console.error("[Meta AI] Poll error:", e); if (attempt === maxAttempts - 1) throw e; } } throw new Error("Meta AI: Image generation timed out"); } /** * Query message status for polling */ private async queryMessageStatus(messageId: string): Promise { const variables = { id: messageId }; const body = new URLSearchParams({ fb_api_caller_class: "RelayModern", fb_api_req_friendly_name: "useAbraMessageQuery", variables: JSON.stringify(variables), doc_id: "7654946557897648", ...(this.session.lsd && { lsd: this.session.lsd }), ...(this.session.fb_dtsg && { fb_dtsg: this.session.fb_dtsg }) }); const response = await fetch(GRAPHQL_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", "Cookie": this.cookies, "Origin": META_AI_BASE, "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", ...(this.session.accessToken && { "Authorization": `OAuth ${this.session.accessToken}` }) }, body: body.toString() }); return response.json(); } /** * Download image from URL and convert to base64 */ async downloadAsBase64(url: string): Promise { const response = await fetch(url, { headers: { "Cookie": this.cookies, "Referer": META_AI_BASE } }); const buffer = await response.arrayBuffer(); return Buffer.from(buffer).toString('base64'); } }