Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save 0xdevalias/0e723b6c46fec7947a3119de9dd5045d to your computer and use it in GitHub Desktop.
Save 0xdevalias/0e723b6c46fec7947a3119de9dd5045d to your computer and use it in GitHub Desktop.
Random Facebook React / Relay JavaScript Snippets

Random Facebook React / Relay JavaScript Snippets


In Relay DevTools, while viewing:

Some interesting paths:

  • Story -> .message (TextWithEntities) -> __ref (client: 2:message) -> .text
  • Story -> .message(location:"homepage_stream") (TextWithEntities) -> __ref (client: 2:message(location:"homepage_stream")) -> .text
findAllReactElements(({ props, fiber, element }) => {
  return props?.children?.props?.hasOwnProperty('feedEdge')
}).map(el => {
  const props = getReactProps(el)
  return props?.children?.props?.feedEdge?.node
})

// __id
// __typename: Story
// This is basically a wrapper around a Map
__RELAY_DEVTOOLS_HOOK__.environments.get(1).getStore().getSource()

// This gives us all of the record IDs
__RELAY_DEVTOOLS_HOOK__.environments.get(1).getStore().getSource().getRecordIDs()

// We can do a crude search through them if we want
__RELAY_DEVTOOLS_HOOK__.environments.get(1).getStore().getSource().getRecordIDs().filter(id => id.toLowerCase().includes('story'))

// Or access a record by it's ID
__RELAY_DEVTOOLS_HOOK__.environments.get(1).getStore().getSource().get('UzpfSUZTOjE6LTIxNTE1MTA2NzU0ODcxMTY2NDI6ZUp3VG0zbjIrTjhmUzQrdUROS1VFZHR6ZmVyaFcvdGJaRVc1eEdhMWJyajdlSSt1MkxtSmt4N3hpSjJaTk9rUmo2V01TQitMR29NcFdNanNSOStqUDg5dmYyU0hhMHFEYWxrdDRuYUFHY3BtZ2ttMk1mZWR2clhvZDg4NmlWT01USitZVGpBZVluWjR4d3lUbmNJUzhJM3RBdU14RG9ZZlhBNHp1QzNlY1o5aWJCTTh3YmpDZ09tUWdjbUtPcGk2TjZzWkdXNHg3Z3Yrd0RUcHliLzlJQkZwSm84cERVeE1xWG5mbXBrRVVpdEtVb3Z5RW5QaWkxS0xNeEtMVXVGT2syYjBuTmZJTEhaczdwdVRkKy8vazJSZ0VHZUFBbFVoYVVZM0taZzZNM2hncERLSU12RjRpVEhFQ0FreE1DU1pNakFBQUFjNWh2UT0=')

// Theoretically we should be able to get a RecordProxy that would allow us to navigate the links..
//   https://relay.dev/docs/api-reference/store/#recordproxy
//   TODO: figure out how to do that..

// But, we can always do it manually..
messageRef = __RELAY_DEVTOOLS_HOOK__.environments.get(1).getStore().getSource().get("UzpfSTM5MTMxMzg2NzY3MzE2NDpTY2FfSUZTY2ExY2EtMjE1MTUxMDY3NTUzMDEyMDA3N2NhZUp4RlUyMUlrd0VRdnJ0M2pUVFJHdk10bW83QXBaUi9wRkFTKzZHUUdtRmYrS2NjZndsaTZrU0F1Uk9wUDVFb2RZOG1ZTm14K3BqbHNUQnM1NWxwQmNjNG1zYWJicE1BbFpvTEUwNUUybTZSaGlIeWh6M3YwNTdwNDdIdTZlWXhkYjV6a3IxOTZjY2ZpYU5IVFFIM0w4ZFAwYVlWR0IzSGZQVGhEcGVKdVJ1Um5lV2t2TEMvY2NPZm1ycnlEbUNLaU91THZmMmZhS2loaEJjY0wrWE5QeDllN1JqZzg3TVhFT0RCNWJ3REJKdTJYclRuN1VaUTlaeWdBM2RId2NjK09PRWNjTTB1TEgxYnE5cGc3eTR4c1JubURQQWttRVZtSFBJem9KTkNPTk1vZnFZRjZlUWhsbXJRTTIwUFdyczlBb2ovRGtNbDRTNk5QYU9YMC80RTJMd2lzRWpoaGt4Qk1Rd0o0WjVNYmlGaVg4VklYU3c0R1RKeG9MN29vcm5rVnBlcnN4d3JnZkJjY2xyT0R1Rkc5bXF2Zm1CcStUM2ZWRWkyUW1vcDRQNXNmeHR0ZTh1MzhyVFQvZlpBOElKSi9FTDErSVNCMTRmbWgzR1N2aVlFWnpQQ3RCYkJwMFdZMFNJRXRBamJlb1NvSGlHaVIxaXpJM2hHRVliZklDdzNJcXkyWWlmVFBZb3c2RUN3T0JBTURnVHJaeno3ZWhiQk5vdlE0a0xvN1VIbzYwSGdoaEEwcnhCOEh4RVdzRDNCSTdua2xsQkFjY210ZXNvaldmRFNlcDBoS01PVnJOWG1UUStpNEdSQzBkYUNQSWxrLzA5MU03RVpjY0JwcVUwSmFKMFN4U0R5QUV4eEJXM1FqZEh4Qyt5MmlpTXJZTHN3ZEQzUHJLczU1SjJoNURpaXEyMjhOTFNWUDNlSWI3d1lXQWNjeXQ1cHlHR2JQSWpxZnhJV2orUzJvK2s5eVBwL0VqVFhpU0xpSXhIek14NGpvbUpKbFp3L0oxZEtmU1dxVTdwazZtN0xFYnBaSHlEYVh0U0VjY1R5ckk0bmpNbE94TDlzVUJISmE5V05sSFMxdUx5OHFMUlllcU9vdEhoUVE0ZXUxOStSMTQyclNTU3JxcXF1cjFiV3ltcWtkK3VVaXVvYXViU3lUaTdiVFprMHhMdW1ySlczTkJFcDZnNVVMQUtBNHhDejB4SVJVM0xBeHBhZ016clYra25ORU9OS29MejRzOGdoTlRIcENnc1ZBZ0hBN1Z5QS8vN2FlaTQ9OmFjYTFjYXtpY2ExMjIxMDE3MDk0ODAyODI2NDM7c2NhOTFjYSJBSUBBUUlnY2NhYjdlYmIzMVpkVzRzeXRSRVVqMUxaX1hYM2U0aV81d3p4NEtSNmNjZUZ4bFRYdmEwVF9JMGUzNUtvSlpSVV9ncDB3ay1BeHdzUk9KdHVVZVFVY2NEIjt9").message.__ref

__RELAY_DEVTOOLS_HOOK__.environments.get(1).getStore().getSource().get(messageRef)

Given a page such as:

This will attempt to extract the Story ID's of the posts on the page, then look them up in the Relay store to access the related data:

findAllReactElements(({ props, fiber, element }) => {
  return props?.children?.props?.hasOwnProperty('feedEdge')
}).map(el => {
  const props = getReactProps(el)
  const relayId = props?.children?.props?.feedEdge?.node?.__id
  const relayStore = __RELAY_DEVTOOLS_HOOK__.environments.get(1).getStore().getSource()
  const story = relayStore.get(relayId)
  const storyActors = story.actors.__refs.map(actorRef => {
    const actor = relayStore.get(actorRef)
    return {
      id: actor.id,
      last_active_time: actor.last_active_time,
      name: actor.name,
      url: actor.url,
      __actor: actor,
    }
  })
  const storyMessage = relayStore.get(story?.message?.__ref)
  const storyShareable = relayStore.get(story?.shareable?.__ref)
  return {
    actorName: storyActors?.[0]?.name,
    actors: storyActors,
    text: storyMessage?.text,
    shareable: storyShareable,
    sponsoredData: story.sponsored_data,
    creationTime: new Date(story.creation_time*1000).toString(),
    url: story.url,
    postId: story.post_id,
    storyId: story.__id,
    __story: story,
    __storyMessage: storyMessage,
    __storyShareable: storyShareable,
  }
}).filter(story => !story.sponsoredData)

News feed posts section:

// News Feed posts
$$('[aria-label="Feeds"] > div > div > div > div > div:nth-child(2) > div > div > div > h2')[0].innerText

// Post Container
postsContainer = $$('[aria-label="Feeds"] > div > div > div > div > div:nth-child(2) > div > div > div > h2 + div')
// News Feed posts
Array.from($$('[aria-label="Feeds"] h2')).map(h2 => h2.innerText)
Array.from($$('[aria-label="Feeds"] h2')).filter(h2 => h2.innerText === "News Feed posts")

// Post Container
postsContainer = Array.from($$('[aria-label="Feeds"] h2')).filter(h2 => h2.innerText === "News Feed posts")?.[0]?.nextSibling

postsContainer.children // 18
postsContainer.querySelectorAll('div > [aria-labelledby]') // 15

postsContainer.children[0].querySelectorAll('div > div > div > div > div > div > div > div > [aria-labelledby]')[0].attributes['aria-labelledby'].value
postsContainer.children[0].querySelectorAll('div > div > div > div > div > div > div > div > [aria-labelledby]')[0].attributes['aria-describedby'].value.split(' ').map(id => document.getElementById(id)).filter(Boolean)

postsContainer.children[0].querySelectorAll('div > [aria-labelledby]')[0].attributes['aria-labelledby'].value
postsContainer.children[0].querySelectorAll('div > [aria-labelledby]')[0].attributes['aria-describedby'].value.split(' ').map(id => document.getElementById(id)).filter(Boolean)

Array.from(postsContainer.children[1].querySelectorAll('[dir="auto"]')).map(el => el.innerText).filter(text => !['Facebook', 'Like', 'Comment', 'Share'].includes(text))

Feed Item Person/Page name (seemingly doesn't always match):

labelId = postsContainer.children[0].querySelectorAll('div > [aria-labelledby]')[0].attributes['aria-labelledby'].value
document.getElementById(labelId).querySelector('span > strong a').innerText
Array.from(postsContainer.querySelectorAll('div > [aria-labelledby]'))
  .map(el => el.attributes['aria-labelledby'].value)
  .filter(Boolean)
  .map(id => document.getElementById(id)?.innerText)

React Props/Fibre:

// Function to get find all elements connected to React
function findAllReactElements(customPredicate = () => true, rootNode = document, tagQuery = '*') {
  if (typeof customPredicate !== 'function') throw 'customPredicate must be a function returning a boolean'
  if (typeof rootNode?.['querySelector'] !== 'function') return [];

  const foundElements = rootNode.getElementsByTagName(tagQuery);
  return Array.from(foundElements).filter(element => {
    const keys = Object.getOwnPropertyNames(element);
    const reactPropsKey = keys.find(key => key.startsWith('__reactProps$'));
    const reactFiberKey = keys.find(key => key.startsWith('__reactFiber$'));
    const props = reactPropsKey ? element[reactPropsKey] : null;
    const fiber = reactFiberKey ? element[reactFiberKey] : null;

    return (props || fiber) && customPredicate({ props, fiber, element });
  });
}

// Function to get React props for a given HTML element
function getReactProps(element) {
  const propsKey = Object.getOwnPropertyNames(element).find(propName =>
    propName.startsWith('__reactProps$')
  );
  return propsKey ? element[propsKey] : null;
}

// Function to get React fiber for a given HTML element
function getReactFiber(element) {
  const fiberKey = Object.getOwnPropertyNames(element).find(propName =>
    propName.startsWith('__reactFiber$')
  );
  return fiberKey ? element[fiberKey] : null;
}
postsContainer = Array.from($$('[aria-label="Feeds"] h2')).filter(h2 => h2.innerText === "News Feed posts")?.[0]?.nextSibling

foundReactElements = findAllReactElements(({ props, fiber, element }) => {
  return props?.children?.props?.type === 'body3'
}, postsContainer.children[0])

foundReactElements.map(el => el.innerText)
postsContainer = Array.from($$('[aria-label="Feeds"] h2')).filter(h2 => h2.innerText === "News Feed posts")?.[0]?.nextSibling

foundReactElements = findAllReactElements(({ props, fiber, element }) => {
  return fiber?.return?.elementType?.displayName?.includes('BaseCometTextWithEntities.react')
}, document)

foundReactElements.map(el => el.innerText)
``

```js
postsContainer = Array.from($$('[aria-label="Feeds"] h2')).filter(h2 => h2.innerText === "News Feed posts")?.[0]?.nextSibling

foundReactElements = findAllReactElements(({ props, fiber, element }) => {
  return props?.children?.type?.displayName === 'TetraText'
}, postsContainer.children[0])

foundReactElements.map(el => el.innerText).filter(text => text !== 'Facebook')
getReactProps($0).children.map(child => child.type.displayName)

[
  "a [from CometFeedStoryCopyrightViolationHeaderSection.react]",
  "a [from CometFeedStoryHeaderSection.react]",
  "a [from CometFeedStoryContextSection.react]",
  "a [from CometFeedStoryPostInformTreatmentSection.react]",
  "a [from CometFeedStoryAYMTFooterSection.react]",
  "q [from CometPostInformTreatmentContext]",
  "a [from CometFeedStoryCallToActionSection.react]",
  "a [from CometFeedStoryFeedbackSection.react]",
  "a [from CometFeedStoryOuterFooterSection.react]"
]
postsContainer = Array.from($$('[aria-label="Feeds"] h2')).filter(h2 => h2.innerText === "News Feed posts")?.[0]?.nextSibling

foundReactElements = findAllReactElements(({ props, fiber, element }) => {
  const displayName = fiber?.return?.elementType?.displayName

  return [
    'BaseCometTextWithEntities.react',
    'CometTextWithEntitiesBase.react',
    'CometEmoji.react',
    'CometImage.react',
    'BaseImage.react',
  ].some(n => displayName?.includes(n))
}, document)

function renderReactElement(element) {
  const props = getReactProps(element) || {};

  const children = Array.isArray(props?.children) ? props.children : [props?.children]

  const { text, alt } = props
  const renderedSelf = (text || alt) ? { text, alt } : null

  return [
    renderedSelf,
    ...children.map(renderChild),
  ].flat().filter(Boolean)
}

function renderChild(child) {
  if (!child) return null;

  const props = child?.props || {}
  const displayName = child?.type?.displayName || child?.type?.render?.displayName
  const children = Array.isArray(props?.children) ? props.children : [props?.children]

  const { text, alt } = props
  const renderedSelf = (displayName || text || alt) ? { displayName, text, alt } : null

  return [
    renderedSelf,
    ...children.map(renderChild)
  ].flat().filter(Boolean)

  // if (child.type && child.props) {
  //   if (child.type.displayName?.includes('CometTextWithEntitiesBase.react') || 
  //       child.type.displayName?.includes('BaseCometTextWithEntities.react')) {
  //     return child.props.text;
  //   } 
  //   else if (child.type.render?.displayName?.includes('CometImage.react')) {
  //     return child.props.alt;
  //   } 
  //   else {
  //     // Recursive call to handle nested components or elements
  //     return renderReactElement(child);
  //   }
  // }
  // return null;
}

foundReactElements.map(element => {
  // const fiber = getReactFiber(element)
  // const props = getReactProps(element)

  return renderReactElement(element)
  // const children = Array.isArray(props?.children) ? props.children : [props?.children]

  // return children.map(child => {
  //   if (child?.type?.displayName?.includes('CometTextWithEntitiesBase.react') || child?.type?.displayName?.includes('BaseCometTextWithEntities.react')) {
  //     return child?.props?.text
  //   } 
  //   else if (child?.type?.render?.displayName.includes('CometImage.react')) {
  //     return child?.props?.alt
  //   } 
  //   else {
  //     return child
  //   }
  // })
}).flat()
@0xdevalias
Copy link
Author

0xdevalias commented Jan 29, 2025

To see the custom feeds like All, Favourites, Friends, Groups, Pages:

As we click through the different feeds, we can see that POST's are being made to https://www.facebook.com/api/graphql/ to load the data for each of those feeds.

While there are many parameters included in the payload, some that stand out to me were:

  • fb_api_caller_class: RelayModern
  • fb_api_req_friendly_name: CometModernHomeFeedQuery
  • variables:
    • {
        "RELAY_INCREMENTAL_DELIVERY": true,
        "connectionClass": "EXCELLENT",
        "feedbackSource": 1,
        "feedInitialFetchSize": 2,
        "feedLocation": "NEWSFEED",
        "feedStyle": "MOST_RECENT_PAGES_FEED",
        "orderby": ["MOST_RECENT"],
        "privacySelectorRenderLocation": "COMET_STREAM",
        "recentVPVs": [],
        "refreshMode": "COLD_START",
        "renderLocation": "homepage_stream",
        "scale": 2,
        "shouldChangeBRSLabelFieldName": false,
        "shouldObfuscateCategoryField": true,
        "useDefaultActor": false,
        "__relay_internal__pv__GHLShouldChangeSponsoredAuctionDistanceFieldNamerelayprovider": true,
        "__relay_internal__pv__GHLShouldChangeSponsoredDataFieldNamerelayprovider": false,
        "__relay_internal__pv__GHLShouldChangeAdIdFieldNamerelayprovider": false,
        "__relay_internal__pv__IsWorkUserrelayprovider": false,
        "__relay_internal__pv__CometFeedStoryDynamicResolutionPhotoAttachmentRenderer_experimentWidthrelayprovider": 500,
        "__relay_internal__pv__CometImmersivePhotoCanUserDisable3DMotionrelayprovider": false,
        "__relay_internal__pv__WorkCometIsEmployeeGKProviderrelayprovider": false,
        "__relay_internal__pv__IsMergQAPollsrelayprovider": false,
        "__relay_internal__pv__FBReelsMediaFooter_comet_enable_reels_ads_gkrelayprovider": false,
        "__relay_internal__pv__CometUFIReactionsEnableShortNamerelayprovider": false,
        "__relay_internal__pv__CometUFIShareActionMigrationrelayprovider": true,
        "__relay_internal__pv__StoriesArmadilloReplyEnabledrelayprovider": true,
        "__relay_internal__pv__EventCometCardImage_prefetchEventImagerelayprovider": false
      }
      • Of the variables, these ones particularly stand out to me as potentially interesting to look deeper into, and see what else exists as valid values:
        • "feedLocation": "NEWSFEED"
        • "feedStyle": "MOST_RECENT_PAGES_FEED"
        • "orderby": ["MOST_RECENT"]

Favourites:
image

Friends:
image

Groups:
image

Pages:
image

Searching the frontend code for one of those feedStyle types, I came across this:

image

Which seems to include:

  • DEFAULT
  • MOST_RECENT_FEED_DEFAULT
  • FAVORITES_FEED
    • This shows up in a couple of places in the code
  • MOST_RECENT_FRIENDS_FEED
  • MOST_RECENT_GROUPS_FEED
  • MOST_RECENT_PAGES_FEED
  • MOST_RECENT_FEED_NOTIFICATIONS
  • SEEN_FEED

@0xdevalias
Copy link
Author

On Messenger:

When scrolling back in a conversation and it needs to load more older messages, the following GraphQL request is made:

It has many headers and payload params set, but some that stood out to me:

  • Headers:
    • x-fb-friendly-name: EBMessageRangeQueryWithPersistenceQuery
  • Payload Body:
    • __hs: 20132.HYP:messengerdotcom_comet_pkg.2.1...1
    • fb_api_caller_class: RelayModern
    • fb_api_req_friendly_name: EBMessageRangeQueryWithPersistenceQuery
    • variables: {
        "restore_payload_string": "{\"restore_context\":{\"act_thread_id\":\"REDACTED\",\"site\":\"www\",\"tam_thread_subtype\":0},\"success\":{\"device_context\":{\"device_id\":\"REDACTED\",\"locally_available_epochs\":[\"REDACTED\"],\"raw_tokens\":{\"mailbox_root_key\":\"REDACTED\",\"ocmf_client_state_blob\":\"REDACTED\"}},\"direction\":1,\"include_offset\":true,\"server_thread_key\":\"REDACTED\",\"reference_timestamp\":\"1738829215151\"}}",
        "restore_type": "RANGE_QUERY_RESTORE",
        "app_id": "772021112871879"
      }
      • Unstringifying / pretty printing restore_payload_string:
        • "restore_context": {
            "act_thread_id": "REDACTED",
            "site": "www",
            "tam_thread_subtype": 0
          },
          "success": {
            "device_context": {
              "device_id": "REDACTED",
              "locally_available_epochs": [
                "REDACTED"
              ],
              "raw_tokens": {
                "mailbox_root_key": "REDACTED",
                "ocmf_client_state_blob": "REDACTED"
              }
            },
            "direction": 1,
            "include_offset": true,
            "server_thread_key": "REDACTED",
            "reference_timestamp": "1738829215151"
          }

This returns something like this:

{
  "data": {
    "viewer": {
      "encrypted_backup": {
        "mailbox": {
          "messages": {
            "encrypted_messages": [
              {
                "echo_document": {
                  "encryption_version": "UNKNOWN",
                  "epoch_id": null
                },
                "otid": "REDACTED",
                "protobuf_stanzas": {
                  "top_level_protobuf": {
                    "encrypted_protobuf_stanza": "REDACTED",
                    "encryption_version": "UNKNOWN",
                    "epoch_id": "REDACTED",
                    "protobuf_timestamp_ms": "REDACTED"
                  },
                  "supplemental_protobufs": []
                }
              }
            ],
            "backup_id": "REDACTED",
            "message_range_info": {
              "has_more_before": true,
              "has_more_after": true
            },
            "should_delete_mailbox": null
          }
        },
        "id": "REDACTED"
      }
    }
  },
  "extensions": { "is_final": true }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment