[
  {
    "path": ".github/workflows/python-publish.yml",
    "content": "# This workflow will upload a Python Package using Twine when a release is created\n# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries\n\n# This workflow uses actions that are not certified by GitHub.\n# They are provided by a third-party and are governed by\n# separate terms of service, privacy policy, and support\n# documentation.\n\nname: Upload Python Package\n\non:\n  release:\n    types: [published]\n\npermissions:\n  contents: read\n\njobs:\n  deploy:\n\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v3\n    - name: Set up Python\n      uses: actions/setup-python@v3\n      with:\n        python-version: '3.x'\n    - name: Install dependencies\n      run: |\n        python -m pip install --upgrade pip\n        pip install build\n        pip install wheel\n        pip install setuptools\n    - name: Build package\n      run: python3 setup.py sdist bdist_wheel\n    - name: Publish package\n      uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29\n      with:\n        user: __token__\n        password: ${{ secrets.PYPI_API_TOKEN }}"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.idea\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n#   For a library or package, you might want to ignore these files since the code is\n#   intended to run in multiple environments; otherwise, check them in:\n# .python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/"
  },
  {
    "path": "README.md",
    "content": "# MetaAI API Wrapper\n\nMetaAI is a Python library designed to interact with Meta's AI APIs that run in the backend of https://www.meta.ai/. It encapsulates the complexities of authentication and communication with the APIs, providing a straightforward interface for sending queries and receiving responses.\n\nWith this you can easily prompt the AI with a message and get a response, directly from your Python code. **NO API KEY REQUIRED**\n\n**Meta AI is connected to the internet, so you will be able to get the latest real-time responses from the AI.** (powered by Bing)\n\nMeta AI is running Llama 3 LLM.\n\n## Features\n- **Prompt AI**: Send a message to the AI and get a response from Llama 3.\n- **Image Generation**: Generate images using the AI. (Only for FB authenticated users)\n- **Get Up To Date Information**: Get the latest information from the AI thanks to its connection to the internet.\n- **Get Sources**: Get the sources of the information provided by the AI.\n- **Streaming**: Stream the AI's response in real-time or get the final response.\n- **Follow Conversations**: Start a new conversation or follow up on an existing one.\n\n## Usage\n**Download**:\n\n   ```bash\n   pip install meta-ai-api\n   ```\n   \n**Initialization**:\n\n```python\nfrom meta_ai_api import MetaAI\n\nai = MetaAI()\nresponse = ai.prompt(message=\"Whats the weather in San Francisco today? And what is the date?\")\nprint(response)\n \n```\n```json\n{\n   \"message\":\"The weather in San Francisco today is mostly clear to overcast, with no precipitation, a wind speed between 0 and 8 miles per hour and temperatures ranging from 51 to 55 degrees Fahrenheit ¹. The date is Friday, April 19, 2024 ². Please note that the weather forecast is continually changing ³ ⁴ ⁵ ⁶.\\n\",\n   \"sources\":[\n      {\n         \"link\":\"https://www.wolframalpha.com/input?i=San+Francisco+weather+today+and+date\",\n         \"title\":\"WolframAlpha\"\n      },\n      {\n         \"link\":\"https://www.timeanddate.com/weather/usa/san-francisco\",\n         \"title\":\"Weather for San Francisco, California, USA - timeanddate.com\"\n      },\n      {\n         \"link\":\"https://www.accuweather.com/en/us/san-francisco/94103/weather-today/347629\",\n         \"title\":\"Weather Today for San Francisco, CA | AccuWeather\"\n      },\n      {\n         \"link\":\"https://www.accuweather.com/en/us/san-francisco/94103/weather-forecast/347629\",\n         \"title\":\"San Francisco, CA Weather Forecast | AccuWeather\"\n      },\n      {\n         \"link\":\"https://forecast.weather.gov/zipcity.php?inputstring=San%20francisco%2CCA\",\n         \"title\":\"National Weather Service\"\n      },\n      {\n         \"link\":\"https://www.wunderground.com/weather/us/ca/san-francisco\",\n         \"title\":\"San Francisco, CA Weather Conditions | Weather Underground\"\n      }\n   ]\n}\n```\n---\n### Follow conversations:\n```python\n```python\nmeta = MetaAI()\n\nprint(meta.prompt(\"what is 2 + 2?\"))\nprint(meta.prompt(\"what was my previous question?\"))\n```\n\n```\n{'message': '2 + 2 = 4\\n', 'sources': [], 'media': []}\n{'message': 'Your previous question was \"what is 2 + 2?\"\\n', 'sources': [], 'media': []}\n```\n\nAnd to start a new one:\n```python\nmeta = MetaAI()\n\nprint(meta.prompt(\"what is 2 + 2?\"))\nprint(meta.prompt(\"what was my previous question?\", new_conversation=True))\n```\n\n```\n{'message': '2 + 2 = 4\\n', 'sources': [], 'media': []}\n{'message': \"This is the beginning of our conversation, so I don't have a previous question to refer to. I'm happy to chat with you, though! What's on your mind today?\\n\", 'sources': [], 'media': []}\n```\n\n---\n```python\nfrom meta_ai_api import MetaAI\n\nai = MetaAI()\nresponse = ai.prompt(message=\"What was the Warriors score last game?\")\nprint(response)\n```\n```json\n{\n   \"message\":\"The Golden State Warriors' last game was against the Sacramento Kings, and they lost 118-94 ¹ ². Stephen Curry scored 22 points, and the Kings' win eliminated the Warriors from the playoffs ³. The Warriors finished the season 46-36 and 10th in the Western Conference ⁴ ³.\\n\",\n   \"sources\":[\n      {\n         \"link\":\"https://sportradar.com/\",\n         \"title\":\"Game Info of NBA from sportradar.com\"\n      },\n      {\n         \"link\":\"https://www.sofascore.com/team/basketball/golden-state-warriors/3428\",\n         \"title\":\"Golden State Warriors live scores & schedule - Sofascore\"\n      },\n      {\n         \"link\":\"https://www.foxsports.com/nba/golden-state-warriors-team-schedule\",\n         \"title\":\"Golden State Warriors Schedule & Scores - NBA - FOX Sports\"\n      },\n      {\n         \"link\":\"https://en.wikipedia.org/wiki/History_of_the_Golden_State_Warriors\",\n         \"title\":\"History of the Golden State Warriors\"\n      }\n   ]\n}\n```\n\n**Using proxy**:\n\n```python\nfrom meta_ai_api import MetaAI\n\nproxy = {\n    'http': 'http://proxy_address:port',\n    'https': 'https://proxy_address:port'\n}\n\nai = MetaAI(proxy=proxy)\nresponse = ai.prompt(message=\"How to find out which mushrooms are edible?\")\nprint(response)\n```\n\n**Streaming Response**:\n\n```python\nfrom meta_ai_api import MetaAI\n\nai = MetaAI()\nresponse = ai.prompt(message=\"What was the Warriors score last game?\", stream=True)\nfor r in response:\n    print(r)\n```\n\n```\n{'message': '\\n', 'sources': []}\n{'message': 'The\\n', 'sources': []}\n{'message': 'The Golden\\n', 'sources': []}\n{'message': 'The Golden State\\n', 'sources': []}\n{'message': 'The Golden State Warriors\\n', 'sources': []}\n{'message': \"The Golden State Warriors'\\n\", 'sources': []}\n{'message': \"The Golden State Warriors' last\\n\", 'sources': []}\n{'message': \"The Golden State Warriors' last game\\n\", 'sources': []}\n{'message': \"The Golden State Warriors' last game was\\n\", 'sources': []}\n{'message': \"The Golden State Warriors' last game was against\\n\", 'sources': []}\n{'message': \"The Golden State Warriors' last game was against the\\n\", 'sources': []}\n{'message': \"The Golden State Warriors' last game was against the Sacramento\\n\", 'sources': []}\n{'message': \"The Golden State Warriors' last game was against the Sacramento Kings\\n\", 'sources': []}\n{'message': \"The Golden State Warriors' last game was against the Sacramento Kings on\\n\", 'sources': []}\n{'message': \"The Golden State Warriors' last game was against the Sacramento Kings on April\\n\", 'sources': []}\n...\n{'message': \"The Golden State Warriors' last game was against the Sacramento Kings on April 16, 2024, at the Golden 1 Center in Sacramento, California. The Kings won the game with a score of 118-94, with the Warriors scoring 22 points in the first quarter, 28 in the second, 26 in the third and 18 in the fourth quarter ¹.\\n\", 'sources': [{'link': 'https://sportradar.com/', 'title': 'Game Info of NBA from sportradar.com'}]}\n```\n\n**Generate Image**:\n\nBy default image generation is only available for FB authenticated users. If you go on https://www.meta.ai/ , and ask the AI to generate an image, you will be prompted to authenticate with Facebook.\nSo if you want to generate images using this library, you need to authenticate using your FB credentials.\n\n**Note**: There seems to be higher rate limits for authenticated users. So only authenticate to generate images.\n\n```python\nfrom meta_ai_api import MetaAI\nai = MetaAI(fb_email=\"your_fb_email\", fb_password=\"your_fb_password\")\nresp = ai.prompt(message=\"Generate an image of a tech CEO\")\nprint(resp)\n```\n\n```json\n{\n   \"message\":\"\\n\",\n   \"sources\":[\n      \n   ],\n   \"media\":[\n      {\n         \"url\":\"https://scontent-lax3-1.xx.fbcdn.net/o1/v/t0/f1/m247/4282108942387920518_1946149595_21-04-2024-14-17-48.jpeg?_nc_ht=scontent-lax3-1.xx.fbcdn.net&_nc_cat=103&ccb=9-4&oh=00_AfCnbCX7nl_J5kF6mahnams4d99Rs5WZA780HGS_scfc6A&oe=662771EE&_nc_sid=5b3566\",\n         \"type\":\"IMAGE\",\n         \"prompt\":\"a tech CEO\"\n      },\n      {\n         \"url\":\"https://scontent-lax3-1.xx.fbcdn.net/o1/v/t0/f1/m247/3356467762794691754_1025991308_21-04-2024-14-17-48.jpeg?_nc_ht=scontent-lax3-1.xx.fbcdn.net&_nc_cat=108&ccb=9-4&oh=00_AfBLmSbTSqshNAL82KIFk8hGXyL8iK_CZLGcMmmddPrxuA&oe=66276EDD&_nc_sid=5b3566\",\n         \"type\":\"IMAGE\",\n         \"prompt\":\"a tech CEO\"\n      },\n      {\n         \"url\":\"https://scontent-lax3-1.xx.fbcdn.net/o1/v/t0/f1/m247/127487551948523111_2181921077_21-04-2024-14-17-48.jpeg?_nc_ht=scontent-lax3-1.xx.fbcdn.net&_nc_cat=104&ccb=9-4&oh=00_AfAejXKeKPA4vyKXoc6UR0rEirvZwi41P3KiCSQmHRHsEw&oe=66276E45&_nc_sid=5b3566\",\n         \"type\":\"IMAGE\",\n         \"prompt\":\"a tech CEO\"\n      },\n      {\n         \"url\":\"https://scontent-lax3-1.xx.fbcdn.net/o1/v/t0/f1/m247/3497663176351797954_3954783377_21-04-2024-14-17-47.jpeg?_nc_ht=scontent-lax3-1.xx.fbcdn.net&_nc_cat=110&ccb=9-4&oh=00_AfBp3bAfcuofqtI-z9D4bHw-GuGgCNPH_xhMM0PG_95S9Q&oe=66277AE9&_nc_sid=5b3566\",\n         \"type\":\"IMAGE\",\n         \"prompt\":\"a tech CEO\"\n      }\n   ]\n}\n```\n![Tech CEO](https://i.imgur.com/9YR6qHq.jpeg)\n\n# Educational Purpose:\nThis repository is intended for educational purposes only. It is a tool to demonstrate how to interact with Meta's AI APIs, providing an example for learning and experimentation. Users should adhere to Meta's terms of service and use the library responsibly.\n\n\n# Copyright:\nThis program is licensed under the GNU GPL v3. All code has been written by me, Strvm.\n\n# Copyright Notice:\n```\nStrvm/meta-ai-api: a reverse engineered API wrapper for MetaAI\nCopyright (C) 2023 Strvm\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n```\n\n# Meta Copyright:\n\nFor more information related to the license tied to Llama, please visit https://www.llama.com/llama3/license/\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"poetry-core>=1.0.0\"]\nbuild-backend = \"poetry.core.masonry.api\"\n\n[tool.poetry]\nname = \"meta_ai_api\"\nversion = \"0.1.0\"\ndescription = \"Interact with the Meta AI API\"\nauthors = [\"Romeo Phillips\"]\n\n[tool.poetry.dependencies]\npython = \"^3.7\"\nrequests = \"2.31.0\"\nrequests-html = \"0.10.0\"\nlxml_html_cleaner = \"0.1.1\"\nbs4 = \"0.0.2\"\n\n[build]\nscript = \"build.py\""
  },
  {
    "path": "requirements.txt",
    "content": "requests==2.31.0\nrequests-html==0.10.0\nlxml_html_clean==0.1.1\nbs4==0.0.2"
  },
  {
    "path": "setup.cfg",
    "content": "[metadata]\nversion = attr: meta_ai_api.__version__"
  },
  {
    "path": "setup.py",
    "content": "import setuptools\n\nwith open(\"README.md\", \"r\", encoding=\"utf-8\") as fh:\n    long_description = fh.read()\n\nsetuptools.setup(\n    name=\"meta_ai_api\",\n    author=\"Roméo Phillips\",\n    author_email=\"phillipsromeo@gmail.com\",\n    description=\"Meta AI API Wrapper to interact with the Meta AI API\",\n    keywords=\"llm, ai, meta_ai_api\",\n    long_description=long_description,\n    long_description_content_type=\"text/markdown\",\n    url=\"https://github.com/tomchen/example_pypi_package\",\n    project_urls={\n        \"Documentation\": \"https://github.com/Strvm/meta-ai-api\",\n        \"Bug Reports\": \"https://github.com/Strvm/meta-ai-api\",\n        \"Source Code\": \"https://github.com/Strvm/meta-ai-api\",\n    },\n    package_dir={\"\": \"src\"},\n    packages=setuptools.find_packages(where=\"src\"),\n    classifiers=[\n        \"Development Status :: 5 - Production/Stable\",\n        \"Intended Audience :: Developers\",\n        \"Topic :: Software Development :: Build Tools\",\n        \"Programming Language :: Python :: 3\",\n        \"Programming Language :: Python :: 3.6\",\n        \"Programming Language :: Python :: 3.7\",\n        \"Programming Language :: Python :: 3.8\",\n        \"Programming Language :: Python :: 3.9\",\n        \"Programming Language :: Python :: 3 :: Only\",\n        \"License :: OSI Approved :: MIT License\",\n        \"Operating System :: OS Independent\",\n    ],\n    python_requires=\">=3.6\",\n    extras_require={\n        \"dev\": [\"check-manifest\"],\n    },\n    install_requires=[\"requests\", \"requests-html\", \"lxml_html_clean\"],\n)\n"
  },
  {
    "path": "src/meta_ai_api/__init__.py",
    "content": "__version__ = \"1.2.5\"\nfrom .main import MetaAI  # noqa\n"
  },
  {
    "path": "src/meta_ai_api/exceptions.py",
    "content": "class FacebookInvalidCredentialsException(Exception):\n    pass\n\n\nclass FacebookRegionBlocked(Exception):\n    pass\n"
  },
  {
    "path": "src/meta_ai_api/main.py",
    "content": "import json\nimport logging\nimport time\nimport urllib\nimport uuid\nfrom typing import Dict, List, Generator, Iterator\n\nimport requests\nfrom requests_html import HTMLSession\n\nfrom meta_ai_api.utils import (\n    generate_offline_threading_id,\n    extract_value,\n    format_response,\n)\n\nfrom meta_ai_api.utils import get_fb_session, get_session\n\nfrom meta_ai_api.exceptions import FacebookRegionBlocked\n\nMAX_RETRIES = 3\n\n\nclass MetaAI:\n    \"\"\"\n    A class to interact with the Meta AI API to obtain and use access tokens for sending\n    and receiving messages from the Meta AI Chat API.\n    \"\"\"\n\n    def __init__(\n        self, fb_email: str = None, fb_password: str = None, proxy: dict = None\n    ):\n        self.session = get_session()\n        self.session.headers.update(\n            {\n                \"user-agent\": \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 \"\n                \"(KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36\",\n            }\n        )\n        self.access_token = None\n        self.fb_email = fb_email\n        self.fb_password = fb_password\n        self.proxy = proxy\n\n        self.is_authed = fb_password is not None and fb_email is not None\n        self.cookies = self.get_cookies()\n        self.external_conversation_id = None\n        self.offline_threading_id = None\n\n    def get_access_token(self) -> str:\n        \"\"\"\n        Retrieves an access token using Meta's authentication API.\n\n        Returns:\n            str: A valid access token.\n        \"\"\"\n\n        if self.access_token:\n            return self.access_token\n\n        url = \"https://www.meta.ai/api/graphql/\"\n        payload = {\n            \"lsd\": self.cookies[\"lsd\"],\n            \"fb_api_caller_class\": \"RelayModern\",\n            \"fb_api_req_friendly_name\": \"useAbraAcceptTOSForTempUserMutation\",\n            \"variables\": {\n                \"dob\": \"1999-01-01\",\n                \"icebreaker_type\": \"TEXT\",\n                \"__relay_internal__pv__WebPixelRatiorelayprovider\": 1,\n            },\n            \"doc_id\": \"7604648749596940\",\n        }\n        payload = urllib.parse.urlencode(payload)  # noqa\n        headers = {\n            \"content-type\": \"application/x-www-form-urlencoded\",\n            \"cookie\": f'_js_datr={self.cookies[\"_js_datr\"]}; '\n            f'abra_csrf={self.cookies[\"abra_csrf\"]}; datr={self.cookies[\"datr\"]};',\n            \"sec-fetch-site\": \"same-origin\",\n            \"x-fb-friendly-name\": \"useAbraAcceptTOSForTempUserMutation\",\n        }\n\n        response = self.session.post(url, headers=headers, data=payload)\n\n        try:\n            auth_json = response.json()\n        except json.JSONDecodeError:\n            raise FacebookRegionBlocked(\n                \"Unable to receive a valid response from Meta AI. This is likely due to your region being blocked. \"\n                \"Try manually accessing https://www.meta.ai/ to confirm.\"\n            )\n\n        access_token = auth_json[\"data\"][\"xab_abra_accept_terms_of_service\"][\n            \"new_temp_user_auth\"\n        ][\"access_token\"]\n\n        # Need to sleep for a bit, for some reason the API doesn't like it when we send request too quickly\n        # (maybe Meta needs to register Cookies on their side?)\n        time.sleep(1)\n\n        return access_token\n\n    def prompt(\n        self,\n        message: str,\n        stream: bool = False,\n        attempts: int = 0,\n        new_conversation: bool = False,\n    ) -> Dict or Generator[Dict, None, None]:\n        \"\"\"\n        Sends a message to the Meta AI and returns the response.\n\n        Args:\n            message (str): The message to send.\n            stream (bool): Whether to stream the response or not. Defaults to False.\n            attempts (int): The number of attempts to retry if an error occurs. Defaults to 0.\n            new_conversation (bool): Whether to start a new conversation or not. Defaults to False.\n\n        Returns:\n            dict: A dictionary containing the response message and sources.\n\n        Raises:\n            Exception: If unable to obtain a valid response after several attempts.\n        \"\"\"\n        if not self.is_authed:\n            self.access_token = self.get_access_token()\n            auth_payload = {\"access_token\": self.access_token}\n            url = \"https://graph.meta.ai/graphql?locale=user\"\n\n        else:\n            auth_payload = {\"fb_dtsg\": self.cookies[\"fb_dtsg\"]}\n            url = \"https://www.meta.ai/api/graphql/\"\n\n        if not self.external_conversation_id or new_conversation:\n            external_id = str(uuid.uuid4())\n            self.external_conversation_id = external_id\n        payload = {\n            **auth_payload,\n            \"fb_api_caller_class\": \"RelayModern\",\n            \"fb_api_req_friendly_name\": \"useAbraSendMessageMutation\",\n            \"variables\": json.dumps(\n                {\n                    \"message\": {\"sensitive_string_value\": message},\n                    \"externalConversationId\": self.external_conversation_id,\n                    \"offlineThreadingId\": generate_offline_threading_id(),\n                    \"suggestedPromptIndex\": None,\n                    \"flashVideoRecapInput\": {\"images\": []},\n                    \"flashPreviewInput\": None,\n                    \"promptPrefix\": None,\n                    \"entrypoint\": \"ABRA__CHAT__TEXT\",\n                    \"icebreaker_type\": \"TEXT\",\n                    \"__relay_internal__pv__AbraDebugDevOnlyrelayprovider\": False,\n                    \"__relay_internal__pv__WebPixelRatiorelayprovider\": 1,\n                }\n            ),\n            \"server_timestamps\": \"true\",\n            \"doc_id\": \"7783822248314888\",\n        }\n        payload = urllib.parse.urlencode(payload)  # noqa\n        headers = {\n            \"content-type\": \"application/x-www-form-urlencoded\",\n            \"x-fb-friendly-name\": \"useAbraSendMessageMutation\",\n        }\n        if self.is_authed:\n            headers[\"cookie\"] = f'abra_sess={self.cookies[\"abra_sess\"]}'\n            # Recreate the session to avoid cookie leakage when user is authenticated\n            self.session = requests.Session()\n            self.session.proxies = self.proxy\n\n        response = self.session.post(url, headers=headers, data=payload, stream=stream)\n        if not stream:\n            raw_response = response.text\n            last_streamed_response = self.extract_last_response(raw_response)\n            if not last_streamed_response:\n                return self.retry(message, stream=stream, attempts=attempts)\n\n            extracted_data = self.extract_data(last_streamed_response)\n            return extracted_data\n\n        else:\n            lines = response.iter_lines()\n            is_error = json.loads(next(lines))\n            if len(is_error.get(\"errors\", [])) > 0:\n                return self.retry(message, stream=stream, attempts=attempts)\n            return self.stream_response(lines)\n\n    def retry(self, message: str, stream: bool = False, attempts: int = 0):\n        \"\"\"\n        Retries the prompt function if an error occurs.\n        \"\"\"\n        if attempts <= MAX_RETRIES:\n            logging.warning(\n                f\"Was unable to obtain a valid response from Meta AI. Retrying... Attempt {attempts + 1}/{MAX_RETRIES}.\"\n            )\n            time.sleep(3)\n            return self.prompt(message, stream=stream, attempts=attempts + 1)\n        else:\n            raise Exception(\n                \"Unable to obtain a valid response from Meta AI. Try again later.\"\n            )\n\n    def extract_last_response(self, response: str) -> Dict:\n        \"\"\"\n        Extracts the last response from the Meta AI API.\n\n        Args:\n            response (str): The response to extract the last response from.\n\n        Returns:\n            dict: A dictionary containing the last response.\n        \"\"\"\n        last_streamed_response = None\n        for line in response.split(\"\\n\"):\n            try:\n                json_line = json.loads(line)\n            except json.JSONDecodeError:\n                continue\n\n            bot_response_message = (\n                json_line.get(\"data\", {})\n                .get(\"node\", {})\n                .get(\"bot_response_message\", {})\n            )\n            chat_id = bot_response_message.get(\"id\")\n            if chat_id:\n                external_conversation_id, offline_threading_id, _ = chat_id.split(\"_\")\n                self.external_conversation_id = external_conversation_id\n                self.offline_threading_id = offline_threading_id\n\n            streaming_state = bot_response_message.get(\"streaming_state\")\n            if streaming_state == \"OVERALL_DONE\":\n                last_streamed_response = json_line\n\n        return last_streamed_response\n\n    def stream_response(self, lines: Iterator[str]):\n        \"\"\"\n        Streams the response from the Meta AI API.\n\n        Args:\n            lines (Iterator[str]): The lines to stream.\n\n        Yields:\n            dict: A dictionary containing the response message and sources.\n        \"\"\"\n        for line in lines:\n            if line:\n                json_line = json.loads(line)\n                extracted_data = self.extract_data(json_line)\n                if not extracted_data.get(\"message\"):\n                    continue\n                yield extracted_data\n\n    def extract_data(self, json_line: dict):\n        \"\"\"\n        Extract data and sources from a parsed JSON line.\n\n        Args:\n            json_line (dict): Parsed JSON line.\n\n        Returns:\n            Tuple (str, list): Response message and list of sources.\n        \"\"\"\n        bot_response_message = (\n            json_line.get(\"data\", {}).get(\"node\", {}).get(\"bot_response_message\", {})\n        )\n        response = format_response(response=json_line)\n        fetch_id = bot_response_message.get(\"fetch_id\")\n        sources = self.fetch_sources(fetch_id) if fetch_id else []\n        medias = self.extract_media(bot_response_message)\n        return {\"message\": response, \"sources\": sources, \"media\": medias}\n\n    @staticmethod\n    def extract_media(json_line: dict) -> List[Dict]:\n        \"\"\"\n        Extract media from a parsed JSON line.\n\n        Args:\n            json_line (dict): Parsed JSON line.\n\n        Returns:\n            list: A list of dictionaries containing the extracted media.\n        \"\"\"\n        medias = []\n        imagine_card = json_line.get(\"imagine_card\", {})\n        session = imagine_card.get(\"session\", {}) if imagine_card else {}\n        media_sets = (\n            (json_line.get(\"imagine_card\", {}).get(\"session\", {}).get(\"media_sets\", []))\n            if imagine_card and session\n            else []\n        )\n        for media_set in media_sets:\n            imagine_media = media_set.get(\"imagine_media\", [])\n            for media in imagine_media:\n                medias.append(\n                    {\n                        \"url\": media.get(\"uri\"),\n                        \"type\": media.get(\"media_type\"),\n                        \"prompt\": media.get(\"prompt\"),\n                    }\n                )\n        return medias\n\n    def get_cookies(self) -> dict:\n        \"\"\"\n        Extracts necessary cookies from the Meta AI main page.\n\n        Returns:\n            dict: A dictionary containing essential cookies.\n        \"\"\"\n        session = HTMLSession()\n        headers = {}\n        if self.fb_email is not None and self.fb_password is not None:\n            fb_session = get_fb_session(self.fb_email, self.fb_password)\n            headers = {\"cookie\": f\"abra_sess={fb_session['abra_sess']}\"}\n        response = session.get(\n            \"https://www.meta.ai/\",\n            headers=headers,\n        )\n        cookies = {\n            \"_js_datr\": extract_value(\n                response.text, start_str='_js_datr\":{\"value\":\"', end_str='\",'\n            ),\n            \"datr\": extract_value(\n                response.text, start_str='datr\":{\"value\":\"', end_str='\",'\n            ),\n            \"lsd\": extract_value(\n                response.text, start_str='\"LSD\",[],{\"token\":\"', end_str='\"}'\n            ),\n            \"fb_dtsg\": extract_value(\n                response.text, start_str='DTSGInitData\",[],{\"token\":\"', end_str='\"'\n            ),\n        }\n\n        if len(headers) > 0:\n            cookies[\"abra_sess\"] = fb_session[\"abra_sess\"]\n        else:\n            cookies[\"abra_csrf\"] = extract_value(\n                response.text, start_str='abra_csrf\":{\"value\":\"', end_str='\",'\n            )\n        return cookies\n\n    def fetch_sources(self, fetch_id: str) -> List[Dict]:\n        \"\"\"\n        Fetches sources from the Meta AI API based on the given query.\n\n        Args:\n            fetch_id (str): The fetch ID to use for the query.\n\n        Returns:\n            list: A list of dictionaries containing the fetched sources.\n        \"\"\"\n\n        url = \"https://graph.meta.ai/graphql?locale=user\"\n        payload = {\n            \"access_token\": self.access_token,\n            \"fb_api_caller_class\": \"RelayModern\",\n            \"fb_api_req_friendly_name\": \"AbraSearchPluginDialogQuery\",\n            \"variables\": json.dumps({\"abraMessageFetchID\": fetch_id}),\n            \"server_timestamps\": \"true\",\n            \"doc_id\": \"6946734308765963\",\n        }\n\n        payload = urllib.parse.urlencode(payload)  # noqa\n\n        headers = {\n            \"authority\": \"graph.meta.ai\",\n            \"accept-language\": \"en-US,en;q=0.9,fr-FR;q=0.8,fr;q=0.7\",\n            \"content-type\": \"application/x-www-form-urlencoded\",\n            \"cookie\": f'dpr=2; abra_csrf={self.cookies.get(\"abra_csrf\")}; datr={self.cookies.get(\"datr\")}; ps_n=1; ps_l=1',\n            \"x-fb-friendly-name\": \"AbraSearchPluginDialogQuery\",\n        }\n\n        response = self.session.post(url, headers=headers, data=payload)\n        response_json = response.json()\n        message = response_json.get(\"data\", {}).get(\"message\", {})\n        search_results = (\n            (response_json.get(\"data\", {}).get(\"message\", {}).get(\"searchResults\"))\n            if message\n            else None\n        )\n        if search_results is None:\n            return []\n\n        references = search_results[\"references\"]\n        return references\n\n\nif __name__ == \"__main__\":\n    meta = MetaAI()\n    resp = meta.prompt(\"What was the Warriors score last game?\", stream=False)\n    print(resp)\n"
  },
  {
    "path": "src/meta_ai_api/utils.py",
    "content": "import logging\nimport random\nimport time\nfrom typing import Dict, Optional\n\nfrom requests_html import HTMLSession\nimport requests\nfrom bs4 import BeautifulSoup\n\nfrom meta_ai_api.exceptions import FacebookInvalidCredentialsException\n\n\ndef generate_offline_threading_id() -> str:\n    \"\"\"\n    Generates an offline threading ID.\n\n    Returns:\n        str: The generated offline threading ID.\n    \"\"\"\n    # Maximum value for a 64-bit integer in Python\n    max_int = (1 << 64) - 1\n    mask22_bits = (1 << 22) - 1\n\n    # Function to get the current timestamp in milliseconds\n    def get_current_timestamp():\n        return int(time.time() * 1000)\n\n    # Function to generate a random 64-bit integer\n    def get_random_64bit_int():\n        return random.getrandbits(64)\n\n    # Combine timestamp and random value\n    def combine_and_mask(timestamp, random_value):\n        shifted_timestamp = timestamp << 22\n        masked_random = random_value & mask22_bits\n        return (shifted_timestamp | masked_random) & max_int\n\n    timestamp = get_current_timestamp()\n    random_value = get_random_64bit_int()\n    threading_id = combine_and_mask(timestamp, random_value)\n\n    return str(threading_id)\n\n\ndef extract_value(text: str, start_str: str, end_str: str) -> str:\n    \"\"\"\n    Helper function to extract a specific value from the given text using a key.\n\n    Args:\n        text (str): The text from which to extract the value.\n        start_str (str): The starting key.\n        end_str (str): The ending key.\n\n    Returns:\n        str: The extracted value.\n    \"\"\"\n    start = text.find(start_str) + len(start_str)\n    end = text.find(end_str, start)\n    return text[start:end]\n\n\ndef format_response(response: dict) -> str:\n    \"\"\"\n    Formats the response from Meta AI to remove unnecessary characters.\n\n    Args:\n        response (dict): The dictionnary containing the response to format.\n\n    Returns:\n        str: The formatted response.\n    \"\"\"\n    text = \"\"\n    for content in (\n        response.get(\"data\", {})\n        .get(\"node\", {})\n        .get(\"bot_response_message\", {})\n        .get(\"composed_text\", {})\n        .get(\"content\", [])\n    ):\n        text += content[\"text\"] + \"\\n\"\n    return text\n\n\n# Function to perform the login\ndef get_fb_session(email, password, proxies=None):\n    login_url = \"https://www.facebook.com/login/?next\"\n    headers = {\n        \"authority\": \"mbasic.facebook.com\",\n        \"accept\": \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\",\n        \"accept-language\": \"en-US,en;q=0.9\",\n        \"sec-ch-ua\": '\"Chromium\";v=\"122\", \"Not(A:Brand\";v=\"24\", \"Google Chrome\";v=\"122\"',\n        \"sec-ch-ua-mobile\": \"?0\",\n        \"sec-ch-ua-platform\": '\"macOS\"',\n        \"sec-fetch-dest\": \"document\",\n        \"sec-fetch-mode\": \"navigate\",\n        \"sec-fetch-site\": \"none\",\n        \"sec-fetch-user\": \"?1\",\n        \"upgrade-insecure-requests\": \"1\",\n        \"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\",\n    }\n    # Send the GET request\n    response = requests.get(login_url, headers=headers, proxies=proxies)\n    soup = BeautifulSoup(response.text, \"html.parser\")\n\n    # Parse necessary parameters from the login form\n    lsd = soup.find(\"input\", {\"name\": \"lsd\"})[\"value\"]\n    jazoest = soup.find(\"input\", {\"name\": \"jazoest\"})[\"value\"]\n\n    # Define the URL and body for the POST request to submit the login form\n    post_url = \"https://www.facebook.com/login/?next\"\n    data = {\n        \"lsd\": lsd,\n        \"jazoest\": jazoest,\n        \"login_source\": \"comet_headerless_login\",\n        \"email\": email,\n        \"pass\": password,\n        \"login\": \"1\",\n        \"next\": None,\n    }\n\n    headers = {\n        \"User-Agent\": \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:132.0) Gecko/20100101 Firefox/132.0\",\n        \"Accept\": \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\",\n        \"Accept-Language\": \"en-US,en;q=0.5\",\n        \"Accept-Encoding\": None,\n        \"Referer\": \"https://www.facebook.com/\",\n        \"Content-Type\": \"application/x-www-form-urlencoded\",\n        \"Origin\": \"https://www.facebook.com\",\n        \"DNT\": \"1\",\n        \"Sec-GPC\": \"1\",\n        \"Connection\": \"keep-alive\",\n        \"cookie\": f\"datr={response.cookies.get('datr')};\",\n        \"Upgrade-Insecure-Requests\": \"1\",\n        \"Sec-Fetch-Dest\": \"document\",\n        \"Sec-Fetch-Mode\": \"navigate\",\n        \"Sec-Fetch-Site\": \"same-origin\",\n        \"Sec-Fetch-User\": \"?1\",\n        \"Priority\": \"u=0, i\",\n    }\n\n    from requests import cookies\n\n    # Send the POST request\n    session = requests.session()\n    jar = cookies.RequestsCookieJar()\n    session.proxies = proxies\n    session.cookies = jar\n\n    result = session.post(post_url, headers=headers, data=data)\n    if \"sb\" not in jar or \"xs\" not in jar:\n        raise FacebookInvalidCredentialsException(\n            \"Was not able to login to Facebook. Please check your credentials. \"\n            \"You may also have been rate limited. Try to connect to Facebook manually.\"\n        )\n\n    cookies = {\n        **result.cookies.get_dict(),\n        \"sb\": jar[\"sb\"],\n        \"xs\": jar[\"xs\"],\n        \"fr\": jar[\"fr\"],\n        \"c_user\": jar[\"c_user\"],\n    }\n\n    response_login = {\n        \"cookies\": cookies,\n        \"headers\": result.headers,\n        \"response\": response.text,\n    }\n    meta_ai_cookies = get_cookies()\n\n    url = \"https://www.meta.ai/state/\"\n\n    payload = f'__a=1&lsd={meta_ai_cookies[\"lsd\"]}'\n    headers = {\n        \"authority\": \"www.meta.ai\",\n        \"accept\": \"*/*\",\n        \"accept-language\": \"en-US,en;q=0.9\",\n        \"cache-control\": \"no-cache\",\n        \"content-type\": \"application/x-www-form-urlencoded\",\n        \"cookie\": f'ps_n=1; ps_l=1; dpr=2; _js_datr={meta_ai_cookies[\"_js_datr\"]}; abra_csrf={meta_ai_cookies[\"abra_csrf\"]}; datr={meta_ai_cookies[\"datr\"]};; ps_l=1; ps_n=1',\n        \"origin\": \"https://www.meta.ai\",\n        \"pragma\": \"no-cache\",\n        \"referer\": \"https://www.meta.ai/\",\n        \"sec-fetch-mode\": \"cors\",\n        \"sec-fetch-site\": \"same-origin\",\n        \"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\",\n    }\n\n    response = requests.request(\n        \"POST\", url, headers=headers, data=payload, proxies=proxies\n    )\n\n    state = extract_value(response.text, start_str='\"state\":\"', end_str='\"')\n\n    url = f\"https://www.facebook.com/oidc/?app_id=1358015658191005&scope=openid%20linking&response_type=code&redirect_uri=https%3A%2F%2Fwww.meta.ai%2Fauth%2F&no_universal_links=1&deoia=1&state={state}\"\n    payload = {}\n    headers = {\n        \"authority\": \"www.facebook.com\",\n        \"accept\": \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\",\n        \"accept-language\": \"en-US,en;q=0.9\",\n        \"cache-control\": \"no-cache\",\n        \"cookie\": f\"datr={response_login['cookies']['datr']}; sb={response_login['cookies']['sb']}; c_user={response_login['cookies']['c_user']}; xs={response_login['cookies']['xs']}; fr={response_login['cookies']['fr']}; abra_csrf={meta_ai_cookies['abra_csrf']};\",\n        \"sec-fetch-dest\": \"document\",\n        \"sec-fetch-mode\": \"navigate\",\n        \"sec-fetch-site\": \"cross-site\",\n        \"sec-fetch-user\": \"?1\",\n        \"upgrade-insecure-requests\": \"1\",\n        \"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\",\n    }\n    session = requests.session()\n    session.proxies = proxies\n    response = session.get(url, headers=headers, data=payload, allow_redirects=False)\n\n    next_url = response.headers[\"Location\"]\n\n    url = next_url\n\n    payload = {}\n    headers = {\n        \"User-Agent\": \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:125.0) Gecko/20100101 Firefox/125.0\",\n        \"Accept\": \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8\",\n        \"Accept-Language\": \"en-US,en;q=0.5\",\n        \"Accept-Encoding\": \"gzip, deflate, br\",\n        \"Referer\": \"https://www.meta.ai/\",\n        \"Connection\": \"keep-alive\",\n        \"Cookie\": f'dpr=2; abra_csrf={meta_ai_cookies[\"abra_csrf\"]}; datr={meta_ai_cookies[\"_js_datr\"]}',\n        \"Upgrade-Insecure-Requests\": \"1\",\n        \"Sec-Fetch-Dest\": \"document\",\n        \"Sec-Fetch-Mode\": \"navigate\",\n        \"Sec-Fetch-Site\": \"cross-site\",\n        \"Sec-Fetch-User\": \"?1\",\n        \"TE\": \"trailers\",\n    }\n    session.get(url, headers=headers, data=payload)\n    cookies = session.cookies.get_dict()\n    if \"abra_sess\" not in cookies:\n        raise FacebookInvalidCredentialsException(\n            \"Was not able to login to Facebook. Please check your credentials. \"\n            \"You may also have been rate limited. Try to connect to Facebook manually.\"\n        )\n    logging.info(\"Successfully logged in to Facebook.\")\n    return cookies\n\n\ndef get_cookies() -> dict:\n    \"\"\"\n    Extracts necessary cookies from the Meta AI main page.\n\n    Returns:\n        dict: A dictionary containing essential cookies.\n    \"\"\"\n    session = HTMLSession()\n    response = session.get(\"https://www.meta.ai/\")\n    return {\n        \"_js_datr\": extract_value(\n            response.text, start_str='_js_datr\":{\"value\":\"', end_str='\",'\n        ),\n        \"abra_csrf\": extract_value(\n            response.text, start_str='abra_csrf\":{\"value\":\"', end_str='\",'\n        ),\n        \"datr\": extract_value(\n            response.text, start_str='datr\":{\"value\":\"', end_str='\",'\n        ),\n        \"lsd\": extract_value(\n            response.text, start_str='\"LSD\",[],{\"token\":\"', end_str='\"}'\n        ),\n    }\n\n\ndef get_session(\n    proxy: Optional[Dict] = None, test_url: str = \"https://api.ipify.org/?format=json\"\n) -> requests.Session:\n    \"\"\"\n    Get a session with the proxy set.\n\n    Args:\n        proxy (Dict): The proxy to use\n        test_url (str): A test site from which we check that the proxy is installed correctly.\n\n    Returns:\n        requests.Session: A session with the proxy set.\n    \"\"\"\n    session = requests.Session()\n    if not proxy:\n        return session\n    response = session.get(test_url, proxies=proxy, timeout=10)\n    if response.status_code == 200:\n        session.proxies = proxy\n        return session\n    else:\n        raise Exception(\"Proxy is not working.\")\n"
  }
]