mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-24 07:28:11 +02:00
feat: implement FacebookService to handle post publishing for text, photos, videos, and multi-photo carousels
This commit is contained in:
1 parent
3122e162cc
commit
f56a89e61c
1 file changed
+65
-44
+65
-44
@@ -3,8 +3,7 @@ import path from 'path';
|
|||||||
import { storage } from '../storage';
|
import { storage } from '../storage';
|
||||||
import type { Post, SocialPage, Media } from '@shared/schema';
|
import type { Post, SocialPage, Media } from '@shared/schema';
|
||||||
import { imageProcessor } from './imageProcessor';
|
import { imageProcessor } from './imageProcessor';
|
||||||
import { cloudinaryService } from './cloudinary';
|
import { minioService, resolvePublicUrl } from './minio';
|
||||||
import { resolvePublicUrl } from './minio';
|
|
||||||
|
|
||||||
interface FacebookPhotoResponse {
|
interface FacebookPhotoResponse {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -121,20 +120,37 @@ export class FacebookService {
|
|||||||
return data.id;
|
return data.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getMediaBuffer(originalUrl: string): Promise<Buffer> {
|
||||||
|
const localPath = originalUrl.startsWith('/')
|
||||||
|
? path.join(process.cwd(), originalUrl)
|
||||||
|
: originalUrl;
|
||||||
|
|
||||||
|
if (localPath.startsWith('http')) {
|
||||||
|
const response = await fetch(localPath);
|
||||||
|
if (!response.ok) throw new Error(`Failed to fetch media from ${localPath}`);
|
||||||
|
return Buffer.from(await response.arrayBuffer());
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs.promises.readFile(localPath);
|
||||||
|
}
|
||||||
|
|
||||||
private async publishPhotoPost(post: Post, page: SocialPage, media: Media): Promise<string> {
|
private async publishPhotoPost(post: Post, page: SocialPage, media: Media): Promise<string> {
|
||||||
// Use original URL - Facebook handles the cropping
|
const buffer = await this.getMediaBuffer(media.originalUrl);
|
||||||
const photoUrl = resolvePublicUrl(media.originalUrl);
|
const mimeType = media.originalUrl.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg';
|
||||||
|
const blob = new Blob([buffer], { type: mimeType });
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const formData = new FormData();
|
||||||
access_token: page.accessToken!,
|
formData.append('access_token', page.accessToken!);
|
||||||
message: post.content,
|
if (post.content) {
|
||||||
url: photoUrl,
|
formData.append('message', post.content);
|
||||||
});
|
}
|
||||||
|
formData.append('source', blob, path.basename(media.originalUrl));
|
||||||
|
|
||||||
const url = `${this.baseUrl}/${page.pageId}/photos?${params.toString()}`;
|
const url = `${this.baseUrl}/${page.pageId}/photos`;
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -222,24 +238,25 @@ export class FacebookService {
|
|||||||
const photoIds: string[] = [];
|
const photoIds: string[] = [];
|
||||||
|
|
||||||
for (const media of imageMedia) {
|
for (const media of imageMedia) {
|
||||||
// Use original URL - Facebook handles the cropping for carousel
|
if (!media.originalUrl) {
|
||||||
const photoUrl = resolvePublicUrl(media.originalUrl);
|
|
||||||
|
|
||||||
if (!photoUrl) {
|
|
||||||
console.warn(`Skipping media ${media.id} - no valid URL found`);
|
console.warn(`Skipping media ${media.id} - no valid URL found`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadParams = new URLSearchParams({
|
const buffer = await this.getMediaBuffer(media.originalUrl);
|
||||||
access_token: page.accessToken!,
|
const mimeType = media.originalUrl.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg';
|
||||||
url: photoUrl,
|
const blob = new Blob([buffer], { type: mimeType });
|
||||||
published: 'false',
|
|
||||||
});
|
|
||||||
|
|
||||||
const uploadUrl = `${this.baseUrl}/${page.pageId}/photos?${uploadParams.toString()}`;
|
const formData = new FormData();
|
||||||
|
formData.append('access_token', page.accessToken!);
|
||||||
|
formData.append('published', 'false');
|
||||||
|
formData.append('source', blob, path.basename(media.originalUrl));
|
||||||
|
|
||||||
|
const uploadUrl = `${this.baseUrl}/${page.pageId}/photos`;
|
||||||
|
|
||||||
const uploadResponse = await fetch(uploadUrl, {
|
const uploadResponse = await fetch(uploadUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!uploadResponse.ok) {
|
if (!uploadResponse.ok) {
|
||||||
@@ -337,21 +354,24 @@ export class FacebookService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async schedulePhotoPost(post: Post, page: SocialPage, scheduledTimestamp: number, media: Media): Promise<string> {
|
private async schedulePhotoPost(post: Post, page: SocialPage, scheduledTimestamp: number, media: Media): Promise<string> {
|
||||||
// Use original URL - Facebook handles the cropping
|
const buffer = await this.getMediaBuffer(media.originalUrl);
|
||||||
const photoUrl = resolvePublicUrl(media.originalUrl);
|
const mimeType = media.originalUrl.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg';
|
||||||
|
const blob = new Blob([buffer], { type: mimeType });
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const formData = new FormData();
|
||||||
access_token: page.accessToken!,
|
formData.append('access_token', page.accessToken!);
|
||||||
message: post.content,
|
if (post.content) {
|
||||||
url: photoUrl,
|
formData.append('message', post.content);
|
||||||
published: '0',
|
}
|
||||||
scheduled_publish_time: scheduledTimestamp.toString(),
|
formData.append('published', '0');
|
||||||
});
|
formData.append('scheduled_publish_time', scheduledTimestamp.toString());
|
||||||
|
formData.append('source', blob, path.basename(media.originalUrl));
|
||||||
|
|
||||||
const url = `${this.baseUrl}/${page.pageId}/photos?${params.toString()}`;
|
const url = `${this.baseUrl}/${page.pageId}/photos`;
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -368,33 +388,34 @@ export class FacebookService {
|
|||||||
return this.publishVideoStory(post, page, media);
|
return this.publishVideoStory(post, page, media);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stories require a 2-step process:
|
let photoBuffer = await this.getMediaBuffer(media.originalUrl);
|
||||||
// Step 1: Upload photo as unpublished to get photo_id
|
let originalFilename = path.basename(media.originalUrl);
|
||||||
// Step 2: Publish the photo as a story using the photo_id
|
|
||||||
|
|
||||||
let photoUrl = resolvePublicUrl(media.originalUrl);
|
|
||||||
|
|
||||||
if (post.content && post.content.trim().length > 0) {
|
if (post.content && post.content.trim().length > 0) {
|
||||||
try {
|
try {
|
||||||
const imageWithText = await imageProcessor.addTextToStoryImage(resolvePublicUrl(media.originalUrl), post.content);
|
const imageWithText = await imageProcessor.addTextToStoryImage(resolvePublicUrl(media.originalUrl), post.content);
|
||||||
photoUrl = await cloudinaryService.uploadStoryImageWithText(imageWithText, `story-${media.id}.png`);
|
await minioService.uploadStoryImageWithText(imageWithText, `story-${media.id}.png`);
|
||||||
console.log('Story image with text generated:', photoUrl);
|
photoBuffer = imageWithText;
|
||||||
|
originalFilename = `story-${media.id}.png`;
|
||||||
|
console.log('Story image with text generated locally');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error generating story image with text, using original:', error);
|
console.error('Error generating story image with text, using original:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 1: Upload photo as unpublished
|
const mimeType = originalFilename.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg';
|
||||||
const uploadParams = new URLSearchParams({
|
const blob = new Blob([photoBuffer], { type: mimeType });
|
||||||
access_token: page.accessToken!,
|
|
||||||
url: photoUrl,
|
|
||||||
published: 'false', // Important: must be unpublished first
|
|
||||||
});
|
|
||||||
|
|
||||||
const uploadUrl = `${this.baseUrl}/${page.pageId}/photos?${uploadParams.toString()}`;
|
const formData = new FormData();
|
||||||
|
formData.append('access_token', page.accessToken!);
|
||||||
|
formData.append('published', 'false'); // Important: must be unpublished first
|
||||||
|
formData.append('source', blob, originalFilename);
|
||||||
|
|
||||||
|
const uploadUrl = `${this.baseUrl}/${page.pageId}/photos`;
|
||||||
|
|
||||||
const uploadResponse = await fetch(uploadUrl, {
|
const uploadResponse = await fetch(uploadUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!uploadResponse.ok) {
|
if (!uploadResponse.ok) {
|
||||||
|
|||||||
Reference in new issue
Block a user