372 lines
13 KiB
TypeScript
372 lines
13 KiB
TypeScript
/**
|
|
* 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<MetaImageResult[]> {
|
|
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<void> {
|
|
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<any> {
|
|
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<MetaImageResult[]> {
|
|
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<any> {
|
|
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<string> {
|
|
const response = await fetch(url, {
|
|
headers: {
|
|
"Cookie": this.cookies,
|
|
"Referer": META_AI_BASE
|
|
}
|
|
});
|
|
const buffer = await response.arrayBuffer();
|
|
return Buffer.from(buffer).toString('base64');
|
|
}
|
|
}
|