fix: Social FB — Page token + aviso si el Meta token no tiene pages_*
El token de ads no sirve para /posts; intentamos Page Access Token y explicamos los permisos necesarios. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -46,6 +46,71 @@ function isVideoType(t?: string | null): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function formatGraphError(raw: string): string {
|
||||
try {
|
||||
const j = JSON.parse(raw) as {
|
||||
error?: {
|
||||
message?: string;
|
||||
code?: number;
|
||||
error_subcode?: number;
|
||||
error_user_title?: string;
|
||||
error_user_msg?: string;
|
||||
};
|
||||
};
|
||||
const e = j.error;
|
||||
if (!e) return raw.slice(0, 220);
|
||||
if (e.code === 190 || e.error_subcode === 2069032) {
|
||||
return (
|
||||
'Token Meta inválido o sin permisos de Página. En Business Manager → ' +
|
||||
'Usuarios del sistema → genera un token con pages_show_list, ' +
|
||||
'pages_read_engagement, pages_read_user_content, ' +
|
||||
'instagram_basic, instagram_manage_insights (además de ads_*) y ' +
|
||||
'pégalo en Agencia. Asigna el system user a cada Página.'
|
||||
);
|
||||
}
|
||||
return (
|
||||
e.error_user_title ||
|
||||
e.error_user_msg ||
|
||||
e.message ||
|
||||
raw
|
||||
).slice(0, 280);
|
||||
} catch {
|
||||
return raw.slice(0, 220);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Posts/insights need a Page Access Token. System-user tokens with ads_* alone
|
||||
* fail with OAuthException 190 — exchange when Graph allows it.
|
||||
*/
|
||||
async function resolvePageAccessToken(
|
||||
userToken: string,
|
||||
pageId: string
|
||||
): Promise<{ token: string; via: 'page' | 'user' }> {
|
||||
const r = await fetch(
|
||||
`${META}/${encodeURIComponent(pageId)}?fields=access_token,name&access_token=${encodeURIComponent(userToken)}`,
|
||||
{ cache: 'no-store' }
|
||||
);
|
||||
if (r.ok) {
|
||||
const j = (await r.json()) as { access_token?: string };
|
||||
if (j.access_token) return { token: j.access_token, via: 'page' };
|
||||
}
|
||||
|
||||
const accounts = await fetch(
|
||||
`${META}/me/accounts?fields=id,access_token&limit=100&access_token=${encodeURIComponent(userToken)}`,
|
||||
{ cache: 'no-store' }
|
||||
);
|
||||
if (accounts.ok) {
|
||||
const j = (await accounts.json()) as {
|
||||
data?: { id?: string; access_token?: string }[];
|
||||
};
|
||||
const hit = (j.data || []).find((p) => p.id === pageId && p.access_token);
|
||||
if (hit?.access_token) return { token: hit.access_token, via: 'page' };
|
||||
}
|
||||
|
||||
return { token: userToken, via: 'user' };
|
||||
}
|
||||
|
||||
function insightValue(
|
||||
data: { name?: string; values?: { value?: number }[] }[] | undefined,
|
||||
name: string
|
||||
@@ -55,14 +120,15 @@ function insightValue(
|
||||
return typeof v === 'number' && Number.isFinite(v) ? v : null;
|
||||
}
|
||||
|
||||
async function fetchPageBundle(token: string, pageId: string) {
|
||||
async function fetchPageBundle(agencyToken: string, pageId: string) {
|
||||
const { token } = await resolvePageAccessToken(agencyToken, pageId);
|
||||
const fields =
|
||||
'name,fan_count,followers_count,link,picture{url},engagement,about,website';
|
||||
const r = await fetch(
|
||||
`${META}/${encodeURIComponent(pageId)}?fields=${fields}&access_token=${encodeURIComponent(token)}`,
|
||||
{ cache: 'no-store' }
|
||||
);
|
||||
if (!r.ok) return { error: (await r.text()).slice(0, 200) };
|
||||
if (!r.ok) return { error: formatGraphError(await r.text()) };
|
||||
const profile = await r.json();
|
||||
|
||||
const insightsRes = await fetch(
|
||||
@@ -83,7 +149,7 @@ async function fetchPageBundle(token: string, pageId: string) {
|
||||
}
|
||||
|
||||
const postsRes = await fetch(
|
||||
`${META}/${encodeURIComponent(pageId)}/posts?fields=id,message,created_time,full_picture,permalink_url,status_type,shares,likes.summary(true),comments.summary(true),attachments{media_type,type,url,media,subattachments}&limit=16&access_token=${encodeURIComponent(token)}`,
|
||||
`${META}/${encodeURIComponent(pageId)}/published_posts?fields=id,message,created_time,full_picture,permalink_url,status_type,shares,likes.summary(true),comments.summary(true),attachments{media_type,type,url,media,subattachments}&limit=16&access_token=${encodeURIComponent(token)}`,
|
||||
{ cache: 'no-store' }
|
||||
);
|
||||
const posts: SocialPost[] = [];
|
||||
@@ -123,10 +189,34 @@ async function fetchPageBundle(token: string, pageId: string) {
|
||||
seen.add(p.id);
|
||||
}
|
||||
} else {
|
||||
profile.postsError = (await postsRes.text()).slice(0, 160);
|
||||
// Fallback /posts then surface clear error
|
||||
const posts2 = await fetch(
|
||||
`${META}/${encodeURIComponent(pageId)}/posts?fields=id,message,created_time,full_picture,permalink_url,status_type,shares,likes.summary(true),comments.summary(true),attachments{media_type,type,url,media,subattachments}&limit=16&access_token=${encodeURIComponent(token)}`,
|
||||
{ cache: 'no-store' }
|
||||
);
|
||||
if (posts2.ok) {
|
||||
const j = await posts2.json();
|
||||
for (const p of j.data || []) {
|
||||
posts.push({
|
||||
id: p.id,
|
||||
platform: 'facebook',
|
||||
message: p.message || '',
|
||||
mediaUrl: p.full_picture || null,
|
||||
thumbnailUrl: p.full_picture || null,
|
||||
permalink: p.permalink_url || null,
|
||||
createdAt: p.created_time || null,
|
||||
likes: Number(p.likes?.summary?.total_count || 0),
|
||||
comments: Number(p.comments?.summary?.total_count || 0),
|
||||
shares: Number(p.shares?.count || 0),
|
||||
mediaType: p.full_picture ? 'IMAGE' : 'status',
|
||||
});
|
||||
seen.add(p.id);
|
||||
}
|
||||
} else {
|
||||
profile.postsError = formatGraphError(await postsRes.text());
|
||||
}
|
||||
}
|
||||
|
||||
// Native page videos / reels often missing from /posts
|
||||
const videosRes = await fetch(
|
||||
`${META}/${encodeURIComponent(pageId)}/videos?fields=id,description,created_time,source,permalink_url,picture,length,title&limit=10&access_token=${encodeURIComponent(token)}`,
|
||||
{ cache: 'no-store' }
|
||||
@@ -160,7 +250,7 @@ async function fetchIgBundle(token: string, igId: string) {
|
||||
`${META}/${encodeURIComponent(igId)}?fields=username,name,followers_count,follows_count,media_count,profile_picture_url,biography,website&access_token=${encodeURIComponent(token)}`,
|
||||
{ cache: 'no-store' }
|
||||
);
|
||||
if (!r.ok) return { error: (await r.text()).slice(0, 200) };
|
||||
if (!r.ok) return { error: formatGraphError(await r.text()) };
|
||||
const profile = await r.json();
|
||||
|
||||
const ins = await fetch(
|
||||
@@ -252,7 +342,7 @@ async function fetchIgBundle(token: string, igId: string) {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
profile.postsError = (await mediaRes.text()).slice(0, 160);
|
||||
profile.postsError = formatGraphError(await mediaRes.text());
|
||||
}
|
||||
|
||||
return { ...profile, igInsights, posts };
|
||||
|
||||
Reference in New Issue
Block a user