metaai-sdk 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
metaai_api/main.py ADDED
@@ -0,0 +1,534 @@
1
+ import json
2
+ import logging
3
+ import time
4
+ import urllib.parse
5
+ import uuid
6
+ from typing import Dict, List, Generator, Iterator, Optional, Union
7
+
8
+ import requests
9
+ from requests_html import HTMLSession
10
+
11
+ from metaai_api.utils import (
12
+ generate_offline_threading_id,
13
+ extract_value,
14
+ format_response,
15
+ )
16
+
17
+ from metaai_api.utils import get_fb_session, get_session
18
+
19
+ from metaai_api.exceptions import FacebookRegionBlocked
20
+
21
+ MAX_RETRIES = 3
22
+
23
+
24
+ class MetaAI:
25
+ """
26
+ A class to interact with the Meta AI API to obtain and use access tokens for sending
27
+ and receiving messages from the Meta AI Chat API.
28
+ """
29
+
30
+ def __init__(
31
+ self, fb_email: Optional[str] = None, fb_password: Optional[str] = None, cookies: Optional[dict] = None, proxy: Optional[dict] = None
32
+ ):
33
+ self.session = get_session()
34
+ self.session.headers.update(
35
+ {
36
+ "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
37
+ "(KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
38
+ }
39
+ )
40
+ self.access_token = None
41
+ self.fb_email = fb_email
42
+ self.fb_password = fb_password
43
+ self.proxy = proxy
44
+
45
+ self.is_authed = (fb_password is not None and fb_email is not None) or cookies is not None
46
+
47
+ if cookies is not None:
48
+ self.cookies = cookies
49
+ # Auto-fetch lsd and fb_dtsg if not present in cookies
50
+ if "lsd" not in self.cookies or "fb_dtsg" not in self.cookies:
51
+ self._fetch_missing_tokens()
52
+ else:
53
+ self.cookies = self.get_cookies()
54
+
55
+ self.external_conversation_id = None
56
+ self.offline_threading_id = None
57
+
58
+ def _fetch_missing_tokens(self):
59
+ """
60
+ Fetch lsd and fb_dtsg tokens if they're missing from cookies.
61
+ """
62
+ try:
63
+ cookies_str = "; ".join([f"{k}={v}" for k, v in self.cookies.items() if v])
64
+
65
+ session = HTMLSession()
66
+ headers = {"cookie": cookies_str}
67
+ response = session.get("https://www.meta.ai/", headers=headers)
68
+
69
+ if "lsd" not in self.cookies:
70
+ lsd = extract_value(response.text, start_str='"LSD",[],{"token":"', end_str='"')
71
+ if lsd:
72
+ self.cookies["lsd"] = lsd
73
+
74
+ if "fb_dtsg" not in self.cookies:
75
+ fb_dtsg = extract_value(response.text, start_str='DTSGInitData",[],{"token":"', end_str='"')
76
+ if fb_dtsg:
77
+ self.cookies["fb_dtsg"] = fb_dtsg
78
+ except Exception as e:
79
+ pass # Silent fail, features may not work without tokens
80
+
81
+ def get_access_token(self) -> str:
82
+ """
83
+ Retrieves an access token using Meta's authentication API.
84
+
85
+ Returns:
86
+ str: A valid access token.
87
+ """
88
+
89
+ if self.access_token:
90
+ return self.access_token
91
+
92
+ url = "https://www.meta.ai/api/graphql/"
93
+ payload = {
94
+ "lsd": self.cookies["lsd"],
95
+ "fb_api_caller_class": "RelayModern",
96
+ "fb_api_req_friendly_name": "useAbraAcceptTOSForTempUserMutation",
97
+ "variables": {
98
+ "dob": "1999-01-01",
99
+ "icebreaker_type": "TEXT",
100
+ "__relay_internal__pv__WebPixelRatiorelayprovider": 1,
101
+ },
102
+ "doc_id": "7604648749596940",
103
+ }
104
+ payload = urllib.parse.urlencode(payload) # noqa
105
+ headers = {
106
+ "content-type": "application/x-www-form-urlencoded",
107
+ "cookie": f'_js_datr={self.cookies["_js_datr"]}; '
108
+ f'abra_csrf={self.cookies["abra_csrf"]}; datr={self.cookies["datr"]};',
109
+ "sec-fetch-site": "same-origin",
110
+ "x-fb-friendly-name": "useAbraAcceptTOSForTempUserMutation",
111
+ }
112
+
113
+ response = self.session.post(url, headers=headers, data=payload)
114
+
115
+ try:
116
+ auth_json = response.json()
117
+ except json.JSONDecodeError:
118
+ raise FacebookRegionBlocked(
119
+ "Unable to receive a valid response from Meta AI. This is likely due to your region being blocked. "
120
+ "Try manually accessing https://www.meta.ai/ to confirm."
121
+ )
122
+
123
+ access_token = auth_json["data"]["xab_abra_accept_terms_of_service"][
124
+ "new_temp_user_auth"
125
+ ]["access_token"]
126
+
127
+ # Need to sleep for a bit, for some reason the API doesn't like it when we send request too quickly
128
+ # (maybe Meta needs to register Cookies on their side?)
129
+ time.sleep(1)
130
+
131
+ return access_token
132
+
133
+ def prompt(
134
+ self,
135
+ message: str,
136
+ stream: bool = False,
137
+ attempts: int = 0,
138
+ new_conversation: bool = False,
139
+ images: Optional[list] = None,
140
+ ) -> Union[Dict, Generator[Dict, None, None]]:
141
+ """
142
+ Sends a message to the Meta AI and returns the response.
143
+
144
+ Args:
145
+ message (str): The message to send.
146
+ stream (bool): Whether to stream the response or not. Defaults to False.
147
+ attempts (int): The number of attempts to retry if an error occurs. Defaults to 0.
148
+ new_conversation (bool): Whether to start a new conversation or not. Defaults to False.
149
+ images (list): List of image URLs to animate (for video generation). Defaults to None.
150
+
151
+ Returns:
152
+ dict: A dictionary containing the response message and sources.
153
+
154
+ Raises:
155
+ Exception: If unable to obtain a valid response after several attempts.
156
+ """
157
+ if not self.is_authed:
158
+ self.access_token = self.get_access_token()
159
+ auth_payload = {"access_token": self.access_token}
160
+ url = "https://graph.meta.ai/graphql?locale=user"
161
+
162
+ else:
163
+ auth_payload = {"fb_dtsg": self.cookies["fb_dtsg"]}
164
+ url = "https://www.meta.ai/api/graphql/"
165
+
166
+ if not self.external_conversation_id or new_conversation:
167
+ external_id = str(uuid.uuid4())
168
+ self.external_conversation_id = external_id
169
+
170
+ # Handle video generation with images
171
+ flash_video_input = {"images": []}
172
+ if images:
173
+ flash_video_input = {"images": images}
174
+
175
+ payload = {
176
+ **auth_payload,
177
+ "fb_api_caller_class": "RelayModern",
178
+ "fb_api_req_friendly_name": "useAbraSendMessageMutation",
179
+ "variables": json.dumps(
180
+ {
181
+ "message": {"sensitive_string_value": message},
182
+ "externalConversationId": self.external_conversation_id,
183
+ "offlineThreadingId": generate_offline_threading_id(),
184
+ "suggestedPromptIndex": None,
185
+ "flashVideoRecapInput": flash_video_input,
186
+ "flashPreviewInput": None,
187
+ "promptPrefix": None,
188
+ "entrypoint": "ABRA__CHAT__TEXT",
189
+ "icebreaker_type": "TEXT",
190
+ "__relay_internal__pv__AbraDebugDevOnlyrelayprovider": False,
191
+ "__relay_internal__pv__WebPixelRatiorelayprovider": 1,
192
+ }
193
+ ),
194
+ "server_timestamps": "true",
195
+ "doc_id": "7783822248314888",
196
+ }
197
+ payload = urllib.parse.urlencode(payload) # noqa
198
+ headers = {
199
+ "content-type": "application/x-www-form-urlencoded",
200
+ "x-fb-friendly-name": "useAbraSendMessageMutation",
201
+ }
202
+ if self.is_authed:
203
+ headers["cookie"] = f'abra_sess={self.cookies["abra_sess"]}'
204
+ # Recreate the session to avoid cookie leakage when user is authenticated
205
+ self.session = requests.Session()
206
+ if self.proxy:
207
+ self.session.proxies = self.proxy
208
+
209
+ response = self.session.post(url, headers=headers, data=payload, stream=stream)
210
+ if not stream:
211
+ raw_response = response.text
212
+ last_streamed_response = self.extract_last_response(raw_response)
213
+ if not last_streamed_response:
214
+ return self.retry(message, stream=stream, attempts=attempts)
215
+
216
+ extracted_data = self.extract_data(last_streamed_response)
217
+ return extracted_data
218
+
219
+ else:
220
+ lines = response.iter_lines()
221
+ is_error = json.loads(next(lines))
222
+ if len(is_error.get("errors", [])) > 0:
223
+ return self.retry(message, stream=stream, attempts=attempts)
224
+ return self.stream_response(lines)
225
+
226
+ def retry(self, message: str, stream: bool = False, attempts: int = 0):
227
+ """
228
+ Retries the prompt function if an error occurs.
229
+ """
230
+ if attempts <= MAX_RETRIES:
231
+ logging.warning(
232
+ f"Was unable to obtain a valid response from Meta AI. Retrying... Attempt {attempts + 1}/{MAX_RETRIES}."
233
+ )
234
+ time.sleep(3)
235
+ return self.prompt(message, stream=stream, attempts=attempts + 1)
236
+ else:
237
+ raise Exception(
238
+ "Unable to obtain a valid response from Meta AI. Try again later."
239
+ )
240
+
241
+ def extract_last_response(self, response: str) -> Optional[Dict]:
242
+ """
243
+ Extracts the last response from the Meta AI API.
244
+
245
+ Args:
246
+ response (str): The response to extract the last response from.
247
+
248
+ Returns:
249
+ dict: A dictionary containing the last response.
250
+ """
251
+ last_streamed_response = None
252
+ for line in response.split("\n"):
253
+ try:
254
+ json_line = json.loads(line)
255
+ except json.JSONDecodeError:
256
+ continue
257
+
258
+ bot_response_message = (
259
+ json_line.get("data", {})
260
+ .get("node", {})
261
+ .get("bot_response_message", {})
262
+ )
263
+ chat_id = bot_response_message.get("id")
264
+ if chat_id:
265
+ external_conversation_id, offline_threading_id, _ = chat_id.split("_")
266
+ self.external_conversation_id = external_conversation_id
267
+ self.offline_threading_id = offline_threading_id
268
+
269
+ streaming_state = bot_response_message.get("streaming_state")
270
+ if streaming_state == "OVERALL_DONE":
271
+ last_streamed_response = json_line
272
+
273
+ return last_streamed_response
274
+
275
+ def stream_response(self, lines: Iterator[str]):
276
+ """
277
+ Streams the response from the Meta AI API.
278
+
279
+ Args:
280
+ lines (Iterator[str]): The lines to stream.
281
+
282
+ Yields:
283
+ dict: A dictionary containing the response message and sources.
284
+ """
285
+ for line in lines:
286
+ if line:
287
+ json_line = json.loads(line)
288
+ extracted_data = self.extract_data(json_line)
289
+ if not extracted_data.get("message"):
290
+ continue
291
+ yield extracted_data
292
+
293
+ def extract_data(self, json_line: dict):
294
+ """
295
+ Extract data and sources from a parsed JSON line.
296
+
297
+ Args:
298
+ json_line (dict): Parsed JSON line.
299
+
300
+ Returns:
301
+ Tuple (str, list): Response message and list of sources.
302
+ """
303
+ bot_response_message = (
304
+ json_line.get("data", {}).get("node", {}).get("bot_response_message", {})
305
+ )
306
+ response = format_response(response=json_line)
307
+ fetch_id = bot_response_message.get("fetch_id")
308
+ sources = self.fetch_sources(fetch_id) if fetch_id else []
309
+ medias = self.extract_media(bot_response_message)
310
+ return {"message": response, "sources": sources, "media": medias}
311
+
312
+ @staticmethod
313
+ def extract_media(json_line: dict) -> List[Dict]:
314
+ """
315
+ Extract media from a parsed JSON line.
316
+ Supports images from imagine_card and videos from various fields.
317
+
318
+ Args:
319
+ json_line (dict): Parsed JSON line.
320
+
321
+ Returns:
322
+ list: A list of dictionaries containing the extracted media.
323
+ """
324
+ medias = []
325
+
326
+ # Extract images from imagine_card (standard image generation)
327
+ imagine_card = json_line.get("imagine_card", {})
328
+ session = imagine_card.get("session", {}) if imagine_card else {}
329
+ media_sets = (
330
+ (json_line.get("imagine_card", {}).get("session", {}).get("media_sets", []))
331
+ if imagine_card and session
332
+ else []
333
+ )
334
+ for media_set in media_sets:
335
+ imagine_media = media_set.get("imagine_media", [])
336
+ for media in imagine_media:
337
+ medias.append(
338
+ {
339
+ "url": media.get("uri"),
340
+ "type": media.get("media_type"),
341
+ "prompt": media.get("prompt"),
342
+ }
343
+ )
344
+
345
+ # Extract from image_attachments (may contain both images and videos)
346
+ image_attachments = json_line.get("image_attachments", [])
347
+ if isinstance(image_attachments, list):
348
+ for attachment in image_attachments:
349
+ if isinstance(attachment, dict):
350
+ # Check for video URLs
351
+ uri = attachment.get("uri") or attachment.get("url")
352
+ if uri:
353
+ media_type = "VIDEO" if ".mp4" in uri.lower() or ".m4v" in uri.lower() else "IMAGE"
354
+ medias.append(
355
+ {
356
+ "url": uri,
357
+ "type": media_type,
358
+ "prompt": attachment.get("prompt"),
359
+ }
360
+ )
361
+
362
+ # Extract videos from video_generation field (if present)
363
+ video_generation = json_line.get("video_generation", {})
364
+ if isinstance(video_generation, dict):
365
+ video_media_sets = video_generation.get("media_sets", [])
366
+ for media_set in video_media_sets:
367
+ video_media = media_set.get("video_media", [])
368
+ for media in video_media:
369
+ uri = media.get("uri")
370
+ if uri: # Only add if URI is not null
371
+ medias.append(
372
+ {
373
+ "url": uri,
374
+ "type": "VIDEO",
375
+ "prompt": media.get("prompt"),
376
+ }
377
+ )
378
+
379
+ # Extract from direct video fields
380
+ for possible_video_field in ["video_media", "generated_video", "reels"]:
381
+ field_data = json_line.get(possible_video_field)
382
+ if field_data:
383
+ if isinstance(field_data, list):
384
+ for item in field_data:
385
+ if isinstance(item, dict) and ("uri" in item or "url" in item):
386
+ url = item.get("uri") or item.get("url")
387
+ if url: # Only add if URL is not null
388
+ medias.append(
389
+ {
390
+ "url": url,
391
+ "type": "VIDEO",
392
+ "prompt": item.get("prompt"),
393
+ }
394
+ )
395
+
396
+ return medias
397
+
398
+ def get_cookies(self) -> dict:
399
+ """
400
+ Extracts necessary cookies from the Meta AI main page.
401
+
402
+ Returns:
403
+ dict: A dictionary containing essential cookies.
404
+ """
405
+ session = HTMLSession()
406
+ headers = {}
407
+ fb_session = None
408
+ if self.fb_email is not None and self.fb_password is not None:
409
+ fb_session = get_fb_session(self.fb_email, self.fb_password)
410
+ headers = {"cookie": f"abra_sess={fb_session['abra_sess']}"}
411
+ response = session.get(
412
+ "https://www.meta.ai/",
413
+ headers=headers,
414
+ )
415
+ cookies = {
416
+ "_js_datr": extract_value(
417
+ response.text, start_str='_js_datr":{"value":"', end_str='",'
418
+ ),
419
+ "datr": extract_value(
420
+ response.text, start_str='datr":{"value":"', end_str='",'
421
+ ),
422
+ "lsd": extract_value(
423
+ response.text, start_str='"LSD",[],{"token":"', end_str='"}'
424
+ ),
425
+ "fb_dtsg": extract_value(
426
+ response.text, start_str='DTSGInitData",[],{"token":"', end_str='"'
427
+ ),
428
+ }
429
+
430
+ if len(headers) > 0 and fb_session is not None:
431
+ cookies["abra_sess"] = fb_session["abra_sess"]
432
+ else:
433
+ cookies["abra_csrf"] = extract_value(
434
+ response.text, start_str='abra_csrf":{"value":"', end_str='",'
435
+ )
436
+ return cookies
437
+
438
+ def fetch_sources(self, fetch_id: str) -> List[Dict]:
439
+ """
440
+ Fetches sources from the Meta AI API based on the given query.
441
+
442
+ Args:
443
+ fetch_id (str): The fetch ID to use for the query.
444
+
445
+ Returns:
446
+ list: A list of dictionaries containing the fetched sources.
447
+ """
448
+
449
+ url = "https://graph.meta.ai/graphql?locale=user"
450
+ payload = {
451
+ "access_token": self.access_token,
452
+ "fb_api_caller_class": "RelayModern",
453
+ "fb_api_req_friendly_name": "AbraSearchPluginDialogQuery",
454
+ "variables": json.dumps({"abraMessageFetchID": fetch_id}),
455
+ "server_timestamps": "true",
456
+ "doc_id": "6946734308765963",
457
+ }
458
+
459
+ payload = urllib.parse.urlencode(payload) # noqa
460
+
461
+ headers = {
462
+ "authority": "graph.meta.ai",
463
+ "accept-language": "en-US,en;q=0.9,fr-FR;q=0.8,fr;q=0.7",
464
+ "content-type": "application/x-www-form-urlencoded",
465
+ "cookie": f'dpr=2; abra_csrf={self.cookies.get("abra_csrf")}; datr={self.cookies.get("datr")}; ps_n=1; ps_l=1',
466
+ "x-fb-friendly-name": "AbraSearchPluginDialogQuery",
467
+ }
468
+
469
+ response = self.session.post(url, headers=headers, data=payload)
470
+ response_json = response.json()
471
+ message = response_json.get("data", {}).get("message", {})
472
+ search_results = (
473
+ (response_json.get("data", {}).get("message", {}).get("searchResults"))
474
+ if message
475
+ else None
476
+ )
477
+ if search_results is None:
478
+ return []
479
+
480
+ references = search_results["references"]
481
+ return references
482
+
483
+ def generate_video(
484
+ self,
485
+ prompt: str,
486
+ wait_before_poll: int = 10,
487
+ max_attempts: int = 30,
488
+ wait_seconds: int = 5,
489
+ verbose: bool = True
490
+ ) -> Dict:
491
+ """
492
+ Generate a video from a text prompt using Meta AI.
493
+ Automatically fetches lsd and fb_dtsg tokens from cookies.
494
+
495
+ Args:
496
+ prompt: Text prompt for video generation
497
+ wait_before_poll: Seconds to wait before starting to poll (default: 10)
498
+ max_attempts: Maximum polling attempts (default: 30)
499
+ wait_seconds: Seconds between polling attempts (default: 5)
500
+ verbose: Whether to print status messages (default: True)
501
+
502
+ Returns:
503
+ Dictionary with success status, conversation_id, prompt, video_urls, and timestamp
504
+
505
+ Example:
506
+ ai = MetaAI(cookies={"datr": "...", "abra_sess": "..."})
507
+ result = ai.generate_video("Generate a video of a sunset")
508
+ if result["success"]:
509
+ print(f"Video URLs: {result['video_urls']}")
510
+ """
511
+ from metaai_api.video_generation import VideoGenerator
512
+
513
+ # Convert cookies dict to string format if needed
514
+ if isinstance(self.cookies, dict):
515
+ cookies_str = "; ".join([f"{k}={v}" for k, v in self.cookies.items() if v])
516
+ else:
517
+ cookies_str = str(self.cookies)
518
+
519
+ # Use VideoGenerator for video generation
520
+ video_gen = VideoGenerator(cookies_str=cookies_str)
521
+
522
+ return video_gen.generate_video(
523
+ prompt=prompt,
524
+ wait_before_poll=wait_before_poll,
525
+ max_attempts=max_attempts,
526
+ wait_seconds=wait_seconds,
527
+ verbose=verbose
528
+ )
529
+
530
+
531
+ if __name__ == "__main__":
532
+ meta = MetaAI()
533
+ resp = meta.prompt("What was the Warriors score last game?", stream=False)
534
+ print(resp)