Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions solutions/social-listening/facebook-page-profile-posts/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,11 @@ Error handling: If response contains `"error": true`, check that the target page
- `viewsCount` for video posts is always `null` — Facebook does not include video view counts in the timeline feed GraphQL response; this field requires a separate video-specific API call not covered by this Skill
- `media.playableUrlSd/Hd` requires the user to be logged in; unauthenticated sessions may return null
- `collaborators` is an empty array for most posts (only populated when the post has tagged co-authors)
- The `doc_id: 27278869228466784` is a Facebook internal query ID that may change when Facebook deploys updates; if all calls return `"Unexpected response format"` errors, recapture via:
1. Open any Facebook page while logged in
2. Scroll down to trigger a new batch of posts
3. `network requests --filter api/graphql --method POST`
4. Find the request with `X-FB-Friendly-Name: ProfileCometTimelineFeedRefetchQuery`
5. Extract `doc_id` from the POST body and update the value in `scripts/get-page-posts.py`
- The persisted query `doc_id` and its `__relay_internal__pv__*` provided variables change when Facebook deploys updates (a stale set fails with `missing_required_variable_value`). `scripts/get-page-posts.py` reads both at runtime from the page's own Relay modules, so no manual recapture is normally needed:
- `require('ProfileCometTimelineFeedRefetchQuery_facebookRelayOperation')` → current `doc_id` string
- `require('ProfileCometTimelineFeedRefetchQuery.graphql').params.providedVariables` → provider objects; call `.get()` on each for its value
- The hard-coded `doc_id` in the script is only a fallback for when those modules aren't loaded — open the target page (not a bare facebook.com tab) before calling
- If the query succeeds but fields come back `null` (e.g. likes/comments/shares, media), Facebook changed the response shape rather than the query — inspect a raw node and re-map the paths in `extractPost`
- `pageName` is derived from the URL path and may be incorrect for profile pages with numeric IDs (e.g., `https://www.facebook.com/100012345` → pageName = `100012345`)
- Requires an active Facebook login session; public/unauthenticated access is not supported
- Date filtering (`afterTime`/`beforeTime`) applies to the timeline cursor position, not a strict server-side filter; posts near the boundary may occasionally appear outside the specified range
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@ def main():
const lsdM = allScripts.match(/"LSD"[^}}]*"token":"([^"]+)"/);
if (lsdM) lsd = lsdM[1];

// Facebook rotates the persisted query doc_id and its provided (__relay_internal__pv__*) variables
// on deploys. Read both from the page's own Relay modules at runtime; the hard-coded values below
// are only a fallback for when the modules are not loaded.
let docId = '29183747561227112';
let providedVars = {{}};
try {{
const liveDocId = require('ProfileCometTimelineFeedRefetchQuery_facebookRelayOperation');
if (typeof liveDocId === 'string' && liveDocId) docId = liveDocId;
}} catch(e) {{}}
try {{
const op = require('ProfileCometTimelineFeedRefetchQuery.graphql');
for (const [k, p] of Object.entries(op?.params?.providedVariables || {{}})) {{
try {{ providedVars[k] = typeof p?.get === 'function' ? p.get() : p; }} catch(e) {{ providedVars[k] = null; }}
}}
}} catch(e) {{}}

const variables = {{
afterTime: {after_val},
beforeTime: {before_val},
Expand All @@ -50,33 +66,14 @@ def main():
trackingCode: null,
useDefaultActor: false,
id: '{args.page_id}',
'__relay_internal__pv__GHLShouldChangeAdIdFieldNamerelayprovider': true,
'__relay_internal__pv__GHLShouldChangeSponsoredDataFieldNamerelayprovider': true,
'__relay_internal__pv__IsWorkUserrelayprovider': false,
'__relay_internal__pv__IsMergQAPollsrelayprovider': false,
'__relay_internal__pv__FBReelsEnableVideoWindowedReplayrelayprovider': false,
'__relay_internal__pv__StoriesArmadilloReplyEnabledrelayprovider': false,
'__relay_internal__pv__EventCometCardImage_prefetchEventImagerelayprovider': false,
'__relay_internal__pv__FBReelsMediaFooter_comet_enable_reels_ads_gkrelayprovider': false,
'__relay_internal__pv__CometUFIReactionsEnableShortNamerelayprovider': false,
'__relay_internal__pv__CometUFIShareActionMigrationrelayprovider': true,
'__relay_internal__pv__IncludeCommentWithAttachmentrelayprovider': true,
'__relay_internal__pv__GHLShouldUpdateVideoPreviewImagerelayprovider': false,
'__relay_internal__pv__GHLVideoTimestampOnShortsrelayprovider': false,
'__relay_internal__pv__CometFeedStoryDangerouslySetInnerFeedItemDisplayContentrelayprovider': false,
'__relay_internal__pv__StoriesRingrelayprovider': false,
'__relay_internal__pv__UseCometRouter_cometRouterrelayprovider': false,
'__relay_internal__pv__FBReelsFeedbackActionsrelayprovider': false,
'__relay_internal__pv__FBReelsMediaFooter_comet_enable_reels_ads_gkv2relayprovider': false,
'__relay_internal__pv__GHLShouldChangeSponsoredDataFieldNameForFeedV2relayprovider': true,
'__relay_internal__pv__GHLShouldChangeAdIdFieldNameForFeedV2relayprovider': true
...providedVars
}};

const bodyParams = new URLSearchParams({{
fb_dtsg: fbDtsg,
lsd: lsd,
variables: JSON.stringify(variables),
doc_id: '27278869228466784',
doc_id: docId,
server_timestamps: 'true',
fb_api_caller_class: 'RelayModern',
fb_api_req_friendly_name: 'ProfileCometTimelineFeedRefetchQuery'
Expand Down Expand Up @@ -136,14 +133,23 @@ def main():
function extractMedia(node) {{
const atts = node?.attachments;
if (!atts || atts.length === 0) return [];
return atts.map(att => {{
const m = att?.styles?.attachment?.media;
// Multi-photo posts (StoryAttachmentAlbumFrameStyleRenderer) list each photo under a per-layout
// subattachments list; frame_sublayout_subattachments only carries dimensions, so prefer the others.
const subLists = ['five_photos_subattachments', 'four_photos_subattachments', 'three_photos_subattachments',
'two_photos_subattachments', 'all_subattachments', 'frame_sublayout_subattachments'];
const mediaItems = atts.flatMap(att => {{
const sa = att?.styles?.attachment;
const listKey = subLists.find(k => sa?.[k]?.nodes?.some(n => n?.media?.image?.uri || n?.media?.viewer_image?.uri));
if (listKey) return sa[listKey].nodes.map(n => n?.media);
return [sa?.media || att?.media];
}});
return mediaItems.map(m => {{
if (!m) return null;
const t = m.__typename;
const base = {{ __typename: t, id: m.id || null }};

if (t === 'Photo') {{
const pi = m.photo_image;
const pi = m.photo_image || m.viewer_image || m.image;
return Object.assign(base, {{
thumbnail: pi?.uri || null,
photo_image: pi ? {{ uri: pi.uri, width: pi.width, height: pi.height }} : null,
Expand Down Expand Up @@ -212,9 +218,14 @@ def main():
}}));
const flat = flatReactions(topReactions);

const likes = renderers?.[0]?.feedback?.reaction_count?.count ?? null;
const comments = renderers?.[1]?.feedback?.comment_rendering_instance?.comments?.total_count ?? null;
const shares = renderers?.[2]?.feedback?.share_count?.count ?? null;
// Counts now live directly on the UFI feedback object; the per-action renderer paths are older fallbacks
const likes = ufi?.reaction_count?.count
?? renderers?.[0]?.feedback?.reaction_count?.count ?? null;
const comments = ufi?.comment_rendering_instance?.comments?.total_count
?? ufi?.comments_count_summary_renderer?.feedback?.comment_rendering_instance?.comments?.total_count
?? renderers?.[1]?.feedback?.comment_rendering_instance?.comments?.total_count ?? null;
const shares = ufi?.share_count?.count
?? renderers?.[2]?.feedback?.share_count?.count ?? null;

const creationTime = node?.comet_sections?.timestamp?.story?.creation_time || null;
const facebookId = actor0?.id || null;
Expand Down