Enable live Meta caption updates and partial IG/FB deletes from the pipeline.
Facebook edits in place when possible; Instagram deletes and republishes. Trash and delete-meta no longer block when only one network succeeds. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -555,7 +555,8 @@ async function publishFbPost(
|
||||
}
|
||||
|
||||
export async function publishPipelineItem(
|
||||
item: PipelinePublishRow
|
||||
item: PipelinePublishRow,
|
||||
opts?: { forceIg?: boolean; forceFb?: boolean; skipIg?: boolean; skipFb?: boolean }
|
||||
): Promise<PublishResult> {
|
||||
const channels = normalizeChannels(item.channels);
|
||||
const resolved = await resolveTenantMetaTargets(item.tenant_id);
|
||||
@@ -564,13 +565,16 @@ export async function publishPipelineItem(
|
||||
}
|
||||
const { pageId, igUserId, pageToken } = resolved.targets;
|
||||
|
||||
let igMediaId: string | null = item.meta_ig_media_id || null;
|
||||
let fbPostId: string | null = item.meta_fb_post_id || null;
|
||||
let igMediaId: string | null = opts?.forceIg ? null : item.meta_ig_media_id || null;
|
||||
let fbPostId: string | null = opts?.forceFb ? null : item.meta_fb_post_id || null;
|
||||
let igPermalink: string | null = null;
|
||||
let fbPermalink: string | null = null;
|
||||
const errors: string[] = [];
|
||||
|
||||
if (channels.includes('instagram')) {
|
||||
const wantIg = channels.includes('instagram') && !opts?.skipIg;
|
||||
const wantFb = channels.includes('facebook') && !opts?.skipFb;
|
||||
|
||||
if (wantIg) {
|
||||
if (!igUserId) {
|
||||
errors.push('Sin Instagram Business vinculado a la Página');
|
||||
} else if (!igMediaId) {
|
||||
@@ -584,7 +588,7 @@ export async function publishPipelineItem(
|
||||
}
|
||||
}
|
||||
|
||||
if (channels.includes('facebook')) {
|
||||
if (wantFb) {
|
||||
if (!fbPostId) {
|
||||
const fb = await publishFbPost(pageId, pageToken, item);
|
||||
if (fb.error || !fb.id) {
|
||||
@@ -596,8 +600,6 @@ export async function publishPipelineItem(
|
||||
}
|
||||
}
|
||||
|
||||
const wantIg = channels.includes('instagram');
|
||||
const wantFb = channels.includes('facebook');
|
||||
const igOk = !wantIg || Boolean(igMediaId);
|
||||
const fbOk = !wantFb || Boolean(fbPostId);
|
||||
const ok = igOk && fbOk && errors.length === 0;
|
||||
@@ -613,36 +615,229 @@ export async function publishPipelineItem(
|
||||
};
|
||||
}
|
||||
|
||||
export type DeleteMetaResult = {
|
||||
ok: boolean;
|
||||
/** At least one network deleted or nothing to delete */
|
||||
partialOk: boolean;
|
||||
error?: string;
|
||||
igDeleted: boolean;
|
||||
fbDeleted: boolean;
|
||||
/** Clear these ids in DB after the call */
|
||||
clearIg: boolean;
|
||||
clearFb: boolean;
|
||||
};
|
||||
|
||||
async function deleteFacebookObject(
|
||||
fbId: string,
|
||||
pageToken: string,
|
||||
pageId: string
|
||||
): Promise<{ ok: boolean; error?: string }> {
|
||||
const candidates = [fbId];
|
||||
// pageId_postId → also try bare post id
|
||||
if (fbId.includes('_')) {
|
||||
const bare = fbId.split('_').slice(1).join('_');
|
||||
if (bare) candidates.push(bare);
|
||||
}
|
||||
// Sometimes we stored photo id; try deleting as page post via feed
|
||||
candidates.push(`${pageId}_${fbId}`);
|
||||
|
||||
const errors: string[] = [];
|
||||
for (const cand of [...new Set(candidates)]) {
|
||||
const d = await graphDelete(encodeURIComponent(cand), pageToken);
|
||||
if (d.ok) return { ok: true };
|
||||
// Already gone counts as success
|
||||
const msg = formatGraphError(d.raw).toLowerCase();
|
||||
if (
|
||||
msg.includes('does not exist') ||
|
||||
msg.includes('unsupported get request') ||
|
||||
msg.includes('no existe') ||
|
||||
msg.includes('(#100)')
|
||||
) {
|
||||
return { ok: true };
|
||||
}
|
||||
errors.push(formatGraphError(d.raw));
|
||||
}
|
||||
return { ok: false, error: errors[0] || 'No se pudo borrar en Facebook' };
|
||||
}
|
||||
|
||||
export async function deleteMetaPosts(item: {
|
||||
tenant_id: string;
|
||||
meta_ig_media_id?: string | null;
|
||||
meta_fb_post_id?: string | null;
|
||||
}): Promise<{ ok: boolean; error?: string; cleared: boolean }> {
|
||||
}): Promise<DeleteMetaResult> {
|
||||
const igId = item.meta_ig_media_id?.trim();
|
||||
const fbId = item.meta_fb_post_id?.trim();
|
||||
if (!igId && !fbId) {
|
||||
return { ok: true, cleared: false };
|
||||
return {
|
||||
ok: true,
|
||||
partialOk: true,
|
||||
igDeleted: false,
|
||||
fbDeleted: false,
|
||||
clearIg: true,
|
||||
clearFb: true,
|
||||
};
|
||||
}
|
||||
const resolved = await resolveTenantMetaTargets(item.tenant_id);
|
||||
if (!resolved.targets) {
|
||||
return { ok: false, error: resolved.error, cleared: false };
|
||||
return {
|
||||
ok: false,
|
||||
partialOk: false,
|
||||
error: resolved.error,
|
||||
igDeleted: false,
|
||||
fbDeleted: false,
|
||||
clearIg: false,
|
||||
clearFb: false,
|
||||
};
|
||||
}
|
||||
const { pageToken } = resolved.targets;
|
||||
const { pageToken, pageId } = resolved.targets;
|
||||
const errors: string[] = [];
|
||||
let igDeleted = false;
|
||||
let fbDeleted = false;
|
||||
|
||||
if (igId) {
|
||||
const d = await graphDelete(encodeURIComponent(igId), pageToken);
|
||||
if (!d.ok) errors.push(`IG: ${formatGraphError(d.raw)}`);
|
||||
if (d.ok) {
|
||||
igDeleted = true;
|
||||
} else {
|
||||
const msg = formatGraphError(d.raw).toLowerCase();
|
||||
if (
|
||||
msg.includes('does not exist') ||
|
||||
msg.includes('unsupported') ||
|
||||
msg.includes('no existe')
|
||||
) {
|
||||
igDeleted = true; // already gone
|
||||
} else {
|
||||
errors.push(`IG: ${formatGraphError(d.raw)}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
igDeleted = true;
|
||||
}
|
||||
|
||||
if (fbId) {
|
||||
const d = await graphDelete(encodeURIComponent(fbId), pageToken);
|
||||
if (!d.ok) errors.push(`FB: ${formatGraphError(d.raw)}`);
|
||||
const fb = await deleteFacebookObject(fbId, pageToken, pageId);
|
||||
if (fb.ok) {
|
||||
fbDeleted = true;
|
||||
} else {
|
||||
errors.push(`FB: ${fb.error || 'fallo'}`);
|
||||
}
|
||||
} else {
|
||||
fbDeleted = true;
|
||||
}
|
||||
|
||||
const ok = igDeleted && fbDeleted;
|
||||
return {
|
||||
ok,
|
||||
partialOk: igDeleted || fbDeleted,
|
||||
error: errors.length ? errors.join(' · ') : undefined,
|
||||
igDeleted,
|
||||
fbDeleted,
|
||||
clearIg: igDeleted,
|
||||
clearFb: fbDeleted,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update live posts with current caption/media:
|
||||
* - Facebook: edit message in place when possible
|
||||
* - Instagram: delete + republish (API no permite editar caption)
|
||||
*/
|
||||
export async function updateLivePipelineItem(
|
||||
item: PipelinePublishRow
|
||||
): Promise<{
|
||||
ok: boolean;
|
||||
error?: string;
|
||||
igMediaId?: string | null;
|
||||
fbPostId?: string | null;
|
||||
igPermalink?: string | null;
|
||||
detail?: string;
|
||||
}> {
|
||||
const channels = normalizeChannels(item.channels);
|
||||
const resolved = await resolveTenantMetaTargets(item.tenant_id);
|
||||
if (!resolved.targets) {
|
||||
return { ok: false, error: resolved.error };
|
||||
}
|
||||
const { pageId, igUserId, pageToken } = resolved.targets;
|
||||
const caption = (item.caption || item.title || '').slice(0, 2200);
|
||||
const notes: string[] = [];
|
||||
const errors: string[] = [];
|
||||
|
||||
let igMediaId: string | null = item.meta_ig_media_id || null;
|
||||
let fbPostId: string | null = item.meta_fb_post_id || null;
|
||||
let igPermalink: string | null = null;
|
||||
|
||||
// Facebook: try in-place caption edit
|
||||
if (channels.includes('facebook') && fbPostId) {
|
||||
const edit = await graphPost(encodeURIComponent(fbPostId), pageToken, {
|
||||
message: caption,
|
||||
});
|
||||
if (edit.ok) {
|
||||
notes.push('FB: texto actualizado');
|
||||
} else {
|
||||
// Fallback: delete + republish FB
|
||||
const del = await deleteFacebookObject(fbPostId, pageToken, pageId);
|
||||
if (!del.ok) {
|
||||
errors.push(`FB edit: ${formatGraphError(edit.raw)}`);
|
||||
} else {
|
||||
const pub = await publishFbPost(pageId, pageToken, {
|
||||
...item,
|
||||
meta_fb_post_id: null,
|
||||
});
|
||||
if (!pub.id) {
|
||||
errors.push(`FB republicar: ${pub.error || 'fallo'}`);
|
||||
fbPostId = null;
|
||||
} else {
|
||||
fbPostId = pub.id;
|
||||
notes.push('FB: republicado (no se pudo editar in-place)');
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (channels.includes('facebook') && !fbPostId) {
|
||||
const pub = await publishFbPost(pageId, pageToken, item);
|
||||
if (!pub.id) errors.push(`FB: ${pub.error || 'fallo'}`);
|
||||
else {
|
||||
fbPostId = pub.id;
|
||||
notes.push('FB: publicado');
|
||||
}
|
||||
}
|
||||
|
||||
// Instagram: must delete + republish
|
||||
if (channels.includes('instagram')) {
|
||||
if (!igUserId) {
|
||||
errors.push('Sin Instagram Business vinculado');
|
||||
} else {
|
||||
if (igMediaId) {
|
||||
const d = await graphDelete(encodeURIComponent(igMediaId), pageToken);
|
||||
if (
|
||||
!d.ok &&
|
||||
!formatGraphError(d.raw).toLowerCase().includes('does not exist') &&
|
||||
!formatGraphError(d.raw).toLowerCase().includes('unsupported')
|
||||
) {
|
||||
errors.push(`IG borrar: ${formatGraphError(d.raw)}`);
|
||||
}
|
||||
igMediaId = null;
|
||||
}
|
||||
const pub = await publishIgMedia(igUserId, pageToken, {
|
||||
...item,
|
||||
meta_ig_media_id: null,
|
||||
});
|
||||
if (!pub.id) {
|
||||
errors.push(`IG republicar: ${pub.error || 'fallo'}`);
|
||||
} else {
|
||||
igMediaId = pub.id;
|
||||
igPermalink = pub.permalink;
|
||||
notes.push('IG: republicado con nuevo copy');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ok: errors.length === 0,
|
||||
error: errors.length ? errors.join(' · ') : undefined,
|
||||
cleared: true,
|
||||
igMediaId,
|
||||
fbPostId,
|
||||
igPermalink,
|
||||
detail: notes.join(' · ') || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user