mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-11 17:26:45 +02:00
Improve Facebook post publishing to support multiple images and stories
Update FacebookService to handle multi-photo carousel posts and refine story publishing logic; modify SchedulerService to pass all media for a post. Replit-Commit-Author: Agent Replit-Commit-Session-Id: ae4037a0-2a6f-4530-9bac-79b543286bda Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/397bca8c-984f-43ff-841a-10897aeb8140/ae4037a0-2a6f-4530-9bac-79b543286bda/hAPGvGl
This commit is contained in:
1 parent
008ed91fef
commit
168e9bf58d
2 files changed
+101
-9
No files matched your search
@@ -21,7 +21,7 @@ interface FacebookError {
|
||||
export class FacebookService {
|
||||
private baseUrl = 'https://graph.facebook.com/v23.0';
|
||||
|
||||
async publishPost(post: Post, page: SocialPage, postType: string, media?: Media): Promise<string> {
|
||||
async publishPost(post: Post, page: SocialPage, postType: string, mediaList: Media[] = []): Promise<string> {
|
||||
if (page.platform !== 'facebook') {
|
||||
throw new Error('This service only supports Facebook pages');
|
||||
}
|
||||
@@ -39,15 +39,28 @@ export class FacebookService {
|
||||
|
||||
if (postType === 'story') {
|
||||
// Publish as story only
|
||||
if (!media) {
|
||||
if (mediaList.length === 0) {
|
||||
throw new Error('Stories require media (photo or video)');
|
||||
}
|
||||
return await this.publishStory(post, page, media);
|
||||
return await this.publishStory(post, page, mediaList[0]);
|
||||
} else {
|
||||
// Default to feed
|
||||
if (media) {
|
||||
return await this.publishPhotoPost(post, page, media);
|
||||
// Count images only (videos not supported in multi-photo carousel)
|
||||
const imageMedia = mediaList.filter(m => m.type === 'image');
|
||||
|
||||
if (imageMedia.length > 1) {
|
||||
// Multi-photo carousel (images only)
|
||||
return await this.publishMultiPhotoPost(post, page, mediaList);
|
||||
} else if (imageMedia.length === 1) {
|
||||
// Single image (even if mixed with videos, prioritize the image)
|
||||
return await this.publishPhotoPost(post, page, imageMedia[0]);
|
||||
} else if (mediaList.length >= 1) {
|
||||
// No images, but has media (likely video) - publish first media
|
||||
// Note: Facebook /photos endpoint doesn't support videos well, this may fail
|
||||
console.warn(`Publishing non-image media to Facebook feed - this may fail`);
|
||||
return await this.publishPhotoPost(post, page, mediaList[0]);
|
||||
} else {
|
||||
// Text only
|
||||
return await this.publishTextPost(post, page);
|
||||
}
|
||||
}
|
||||
@@ -100,6 +113,79 @@ export class FacebookService {
|
||||
return data.post_id || data.id;
|
||||
}
|
||||
|
||||
private async publishMultiPhotoPost(post: Post, page: SocialPage, mediaList: Media[]): Promise<string> {
|
||||
// Validate that all media are images
|
||||
const imageMedia = mediaList.filter(m => m.type === 'image');
|
||||
if (imageMedia.length === 0) {
|
||||
throw new Error('Multi-photo posts require at least one image');
|
||||
}
|
||||
|
||||
if (imageMedia.length !== mediaList.length) {
|
||||
console.warn(`Filtered out ${mediaList.length - imageMedia.length} non-image media from multi-photo post`);
|
||||
}
|
||||
|
||||
// Step 1: Upload all photos as unpublished and collect photo IDs
|
||||
const photoIds: string[] = [];
|
||||
|
||||
for (const media of imageMedia) {
|
||||
const photoUrl = media.facebookFeedUrl || media.originalUrl;
|
||||
|
||||
if (!photoUrl) {
|
||||
console.warn(`Skipping media ${media.id} - no valid URL found`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const uploadParams = new URLSearchParams({
|
||||
access_token: page.accessToken!,
|
||||
url: photoUrl,
|
||||
published: 'false',
|
||||
});
|
||||
|
||||
const uploadUrl = `${this.baseUrl}/${page.pageId}/photos?${uploadParams.toString()}`;
|
||||
|
||||
const uploadResponse = await fetch(uploadUrl, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
const error = await uploadResponse.json() as FacebookError;
|
||||
throw new Error(`Facebook API error uploading photo: ${error.error.message} (code: ${error.error.code})`);
|
||||
}
|
||||
|
||||
const uploadData = await uploadResponse.json() as FacebookPhotoResponse;
|
||||
photoIds.push(uploadData.id);
|
||||
}
|
||||
|
||||
if (photoIds.length === 0) {
|
||||
throw new Error('No photos were successfully uploaded');
|
||||
}
|
||||
|
||||
// Step 2: Create feed post with all attached photos
|
||||
const feedParams = new URLSearchParams({
|
||||
access_token: page.accessToken!,
|
||||
message: post.content,
|
||||
});
|
||||
|
||||
// Add each photo as attached_media
|
||||
photoIds.forEach((photoId, index) => {
|
||||
feedParams.append(`attached_media[${index}]`, JSON.stringify({ media_fbid: photoId }));
|
||||
});
|
||||
|
||||
const feedUrl = `${this.baseUrl}/${page.pageId}/feed?${feedParams.toString()}`;
|
||||
|
||||
const feedResponse = await fetch(feedUrl, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!feedResponse.ok) {
|
||||
const error = await feedResponse.json() as FacebookError;
|
||||
throw new Error(`Facebook API error creating feed post: ${error.error.message} (code: ${error.error.code})`);
|
||||
}
|
||||
|
||||
const feedData = await feedResponse.json() as FacebookFeedResponse;
|
||||
return feedData.id;
|
||||
}
|
||||
|
||||
async schedulePost(post: Post, page: SocialPage, scheduledTime: Date, media?: Media): Promise<string> {
|
||||
if (page.platform !== 'facebook') {
|
||||
throw new Error('This service only supports Facebook pages');
|
||||
|
||||
@@ -67,13 +67,19 @@ export class SchedulerService {
|
||||
|
||||
console.log(`Publishing post ${post.id} to ${page.platform} page ${page.pageName}`);
|
||||
|
||||
// Get media if post has media
|
||||
// Get all media for the post (for multi-photo carousels)
|
||||
const postMediaList = await storage.getPostMedia(post.id);
|
||||
const media = postMediaList.length > 0 ? await storage.getMediaById(postMediaList[0].mediaId) : undefined;
|
||||
const mediaList: any[] = [];
|
||||
for (const pm of postMediaList) {
|
||||
const media = await storage.getMediaById(pm.mediaId);
|
||||
if (media) {
|
||||
mediaList.push(media);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that story posts have media
|
||||
// Note: "both" should never reach here since they're split at creation
|
||||
if (scheduledPost.postType === 'story' && !media) {
|
||||
if (scheduledPost.postType === 'story' && mediaList.length === 0) {
|
||||
const errorMsg = 'Les stories nécessitent un média. Ce post ne peut pas être publié.';
|
||||
console.error(`Deleting invalid story post ${scheduledPost.id} without media: ${errorMsg}`);
|
||||
|
||||
@@ -91,7 +97,7 @@ export class SchedulerService {
|
||||
let externalPostId: string;
|
||||
|
||||
if (page.platform === 'facebook') {
|
||||
externalPostId = await facebookService.publishPost(post, page, scheduledPost.postType, media || undefined);
|
||||
externalPostId = await facebookService.publishPost(post, page, scheduledPost.postType, mediaList);
|
||||
} else if (page.platform === 'instagram') {
|
||||
// Instagram publishing not yet implemented
|
||||
console.warn(`Instagram publishing not yet implemented for post ${post.id}`);
|
||||
|
||||
Reference in new issue
Block a user