mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-22 22:48:09 +02:00
5 files changed
+165
-24
No files matched your search
+72
@@ -0,0 +1,72 @@
|
|||||||
|
at async Timeout._onTimeout (/app/node_modules/node-cron/dist/esm/scheduler/runner.js:70:44)
|
||||||
|
|
||||||
|
Publishing post 6439b99f-d5eb-4774-b7ea-97c9be8bb3bf to facebook page Frouard
|
||||||
|
|
||||||
|
Publishing non-image media to Facebook feed - this may fail
|
||||||
|
|
||||||
|
Error publishing post aea0ba94-8e1a-4923-9942-6940029f6be5: Error: Facebook API error: Invalid parameter (code: 100)
|
||||||
|
|
||||||
|
at FacebookService.publishPhotoPost (file:///app/dist/index.js:2029:13)
|
||||||
|
|
||||||
|
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
|
||||||
|
|
||||||
|
at async FacebookService.publishPost (file:///app/dist/index.js:1994:16)
|
||||||
|
|
||||||
|
at async SchedulerService.publishPost (file:///app/dist/index.js:2249:24)
|
||||||
|
|
||||||
|
at async SchedulerService.processPendingPosts (file:///app/dist/index.js:2211:11)
|
||||||
|
|
||||||
|
at async file:///app/dist/index.js:2187:7
|
||||||
|
|
||||||
|
at async Timeout._onTimeout (/app/node_modules/node-cron/dist/esm/scheduler/runner.js:70:44)
|
||||||
|
|
||||||
|
12:52:05 PM [express] GET /api/stats 304 in 29ms :: {"scheduledPosts":9,"scheduledPostsChange":"+100%…
|
||||||
|
|
||||||
|
12:52:05 PM [express] GET /api/scheduled-posts 200 in 33ms :: [{"id":"9128ce3e-88b5-4afe-9834-e79e2da…
|
||||||
|
|
||||||
|
12:52:06 PM [express] GET /api/media 304 in 11ms :: [{"id":"d501254d-4892-4d63-8900-e790ab3794e8","us…
|
||||||
|
|
||||||
|
Publishing post 6439b99f-d5eb-4774-b7ea-97c9be8bb3bf to facebook page Houdemont
|
||||||
|
|
||||||
|
Publishing non-image media to Facebook feed - this may fail
|
||||||
|
|
||||||
|
Error publishing post 23173c8a-7843-48e7-876f-3353988b82a0: Error: Facebook API error: (#200) The permission(s) publish_actions are not available. It has been deprecated. If you want to provide a way for your app users to share content to Facebook, we encourage you to use our Sharing products instead. (code: 200)
|
||||||
|
|
||||||
|
at FacebookService.publishPhotoPost (file:///app/dist/index.js:2029:13)
|
||||||
|
|
||||||
|
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
|
||||||
|
|
||||||
|
at async FacebookService.publishPost (file:///app/dist/index.js:1994:16)
|
||||||
|
|
||||||
|
at async SchedulerService.publishPost (file:///app/dist/index.js:2249:24)
|
||||||
|
|
||||||
|
at async SchedulerService.processPendingPosts (file:///app/dist/index.js:2211:11)
|
||||||
|
|
||||||
|
at async file:///app/dist/index.js:2187:7
|
||||||
|
|
||||||
|
at async Timeout._onTimeout (/app/node_modules/node-cron/dist/esm/scheduler/runner.js:70:44)
|
||||||
|
|
||||||
|
Publishing post 6439b99f-d5eb-4774-b7ea-97c9be8bb3bf to facebook page Frouard
|
||||||
|
|
||||||
|
Publishing non-image media to Facebook feed - this may fail
|
||||||
|
|
||||||
|
Error publishing post aea0ba94-8e1a-4923-9942-6940029f6be5: Error: Facebook API error: Invalid parameter (code: 100)
|
||||||
|
|
||||||
|
at FacebookService.publishPhotoPost (file:///app/dist/index.js:2029:13)
|
||||||
|
|
||||||
|
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
|
||||||
|
|
||||||
|
at async FacebookService.publishPost (file:///app/dist/index.js:1994:16)
|
||||||
|
|
||||||
|
at async SchedulerService.publishPost (file:///app/dist/index.js:2249:24)
|
||||||
|
|
||||||
|
at async SchedulerService.processPendingPosts (file:///app/dist/index.js:2211:11)
|
||||||
|
|
||||||
|
at async file:///app/dist/index.js:2187:7
|
||||||
|
|
||||||
|
at async Timeout._onTimeout (/app/node_modules/node-cron/dist/esm/scheduler/runner.js:70:44)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
@@ -250,15 +250,15 @@ export default function MediaUpload() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) as any)}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Élément sentinelle pour le scroll infini */}
|
{/* Élément sentinelle pour le scroll infini */}
|
||||||
{mediaList && visibleCount < (mediaList as any[]).length && (
|
{(mediaList && visibleCount < (mediaList as any[]).length) ? (
|
||||||
<div ref={loadMoreRef} className="flex justify-center py-4 mt-3">
|
<div ref={loadMoreRef} className="flex justify-center py-4 mt-3">
|
||||||
<Loader2 className="w-5 h-5 text-primary animate-spin" />
|
<Loader2 className="w-5 h-5 text-primary animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -64,6 +64,30 @@ export function PreviewModal({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderMedia = (media: Media, className: string = "w-full h-auto") => {
|
||||||
|
const url = getMediaUrl(media);
|
||||||
|
|
||||||
|
if (media.type === 'video') {
|
||||||
|
return (
|
||||||
|
<video
|
||||||
|
src={url}
|
||||||
|
className={className}
|
||||||
|
controls
|
||||||
|
playsInline
|
||||||
|
data-testid="video-preview"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={url}
|
||||||
|
alt={`Photo ${currentPhotoIndex + 1}`}
|
||||||
|
className={className}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const renderFacebookFeed = () => (
|
const renderFacebookFeed = () => (
|
||||||
<div className="bg-white rounded-lg overflow-hidden border border-gray-200 max-w-lg mx-auto">
|
<div className="bg-white rounded-lg overflow-hidden border border-gray-200 max-w-lg mx-auto">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -87,11 +111,7 @@ export function PreviewModal({
|
|||||||
{/* Media Carousel */}
|
{/* Media Carousel */}
|
||||||
{orderedMedia.length > 0 && (
|
{orderedMedia.length > 0 && (
|
||||||
<div className="relative bg-black">
|
<div className="relative bg-black">
|
||||||
<img
|
{renderMedia(orderedMedia[currentPhotoIndex], "w-full h-auto")}
|
||||||
src={getMediaUrl(orderedMedia[currentPhotoIndex])}
|
|
||||||
alt={`Photo ${currentPhotoIndex + 1}`}
|
|
||||||
className="w-full h-auto"
|
|
||||||
/>
|
|
||||||
{orderedMedia.length > 1 && (
|
{orderedMedia.length > 1 && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
@@ -149,11 +169,7 @@ export function PreviewModal({
|
|||||||
{/* Media Carousel */}
|
{/* Media Carousel */}
|
||||||
{orderedMedia.length > 0 && (
|
{orderedMedia.length > 0 && (
|
||||||
<div className="relative bg-black aspect-square">
|
<div className="relative bg-black aspect-square">
|
||||||
<img
|
{renderMedia(orderedMedia[currentPhotoIndex], "w-full h-full object-cover")}
|
||||||
src={getMediaUrl(orderedMedia[currentPhotoIndex])}
|
|
||||||
alt={`Photo ${currentPhotoIndex + 1}`}
|
|
||||||
className="w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
{orderedMedia.length > 1 && (
|
{orderedMedia.length > 1 && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
@@ -221,11 +237,7 @@ export function PreviewModal({
|
|||||||
{/* Media */}
|
{/* Media */}
|
||||||
{orderedMedia.length > 0 && (
|
{orderedMedia.length > 0 && (
|
||||||
<div className="relative h-full">
|
<div className="relative h-full">
|
||||||
<img
|
{renderMedia(orderedMedia[currentPhotoIndex], "w-full h-full object-cover")}
|
||||||
src={getMediaUrl(orderedMedia[currentPhotoIndex])}
|
|
||||||
alt={`Story ${currentPhotoIndex + 1}`}
|
|
||||||
className="w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
{orderedMedia.length > 1 && (
|
{orderedMedia.length > 1 && (
|
||||||
<>
|
<>
|
||||||
<div className="absolute top-3 left-3 right-3 flex gap-1">
|
<div className="absolute top-3 left-3 right-3 flex gap-1">
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ interface FacebookFeedResponse {
|
|||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FacebookVideoResponse {
|
||||||
|
id: string;
|
||||||
|
post_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface FacebookError {
|
interface FacebookError {
|
||||||
error: {
|
error: {
|
||||||
message: string;
|
message: string;
|
||||||
@@ -54,8 +59,8 @@ export class FacebookService {
|
|||||||
return storyIds.join(',');
|
return storyIds.join(',');
|
||||||
} else {
|
} else {
|
||||||
// Default to feed
|
// Default to feed
|
||||||
// Count images only (videos not supported in multi-photo carousel)
|
|
||||||
const imageMedia = mediaList.filter(m => m.type === 'image');
|
const imageMedia = mediaList.filter(m => m.type === 'image');
|
||||||
|
const videoMedia = mediaList.filter(m => m.type === 'video');
|
||||||
|
|
||||||
if (imageMedia.length > 1) {
|
if (imageMedia.length > 1) {
|
||||||
// Multi-photo carousel (images only)
|
// Multi-photo carousel (images only)
|
||||||
@@ -63,11 +68,9 @@ export class FacebookService {
|
|||||||
} else if (imageMedia.length === 1) {
|
} else if (imageMedia.length === 1) {
|
||||||
// Single image (even if mixed with videos, prioritize the image)
|
// Single image (even if mixed with videos, prioritize the image)
|
||||||
return await this.publishPhotoPost(post, page, imageMedia[0]);
|
return await this.publishPhotoPost(post, page, imageMedia[0]);
|
||||||
} else if (mediaList.length >= 1) {
|
} else if (videoMedia.length >= 1) {
|
||||||
// No images, but has media (likely video) - publish first media
|
// Video post - use video endpoint
|
||||||
// Note: Facebook /photos endpoint doesn't support videos well, this may fail
|
return await this.publishVideoPost(post, page, videoMedia[0]);
|
||||||
console.warn(`Publishing non-image media to Facebook feed - this may fail`);
|
|
||||||
return await this.publishPhotoPost(post, page, mediaList[0]);
|
|
||||||
} else {
|
} else {
|
||||||
// Text only
|
// Text only
|
||||||
return await this.publishTextPost(post, page);
|
return await this.publishTextPost(post, page);
|
||||||
@@ -122,6 +125,32 @@ export class FacebookService {
|
|||||||
return data.post_id || data.id;
|
return data.post_id || data.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async publishVideoPost(post: Post, page: SocialPage, media: Media): Promise<string> {
|
||||||
|
// Use original URL for video
|
||||||
|
const videoUrl = media.originalUrl;
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
access_token: page.accessToken!,
|
||||||
|
description: post.content,
|
||||||
|
file_url: videoUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = `${this.baseUrl}/${page.pageId}/videos?${params.toString()}`;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json() as FacebookError;
|
||||||
|
throw new Error(`Facebook API error publishing video: ${error.error.message} (code: ${error.error.code})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json() as FacebookVideoResponse;
|
||||||
|
// Return the post_id or id
|
||||||
|
return data.post_id || data.id;
|
||||||
|
}
|
||||||
|
|
||||||
private async publishMultiPhotoPost(post: Post, page: SocialPage, mediaList: Media[]): Promise<string> {
|
private async publishMultiPhotoPost(post: Post, page: SocialPage, mediaList: Media[]): Promise<string> {
|
||||||
// Validate that all media are images
|
// Validate that all media are images
|
||||||
const imageMedia = mediaList.filter(m => m.type === 'image');
|
const imageMedia = mediaList.filter(m => m.type === 'image');
|
||||||
@@ -279,6 +308,10 @@ export class FacebookService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async publishStory(post: Post, page: SocialPage, media: Media): Promise<string> {
|
private async publishStory(post: Post, page: SocialPage, media: Media): Promise<string> {
|
||||||
|
if (media.type === 'video') {
|
||||||
|
return this.publishVideoStory(post, page, media);
|
||||||
|
}
|
||||||
|
|
||||||
// Stories require a 2-step process:
|
// Stories require a 2-step process:
|
||||||
// Step 1: Upload photo as unpublished to get photo_id
|
// Step 1: Upload photo as unpublished to get photo_id
|
||||||
// Step 2: Publish the photo as a story using the photo_id
|
// Step 2: Publish the photo as a story using the photo_id
|
||||||
@@ -326,6 +359,30 @@ export class FacebookService {
|
|||||||
const storyData = await storyResponse.json() as { success: boolean; post_id: string };
|
const storyData = await storyResponse.json() as { success: boolean; post_id: string };
|
||||||
return storyData.post_id;
|
return storyData.post_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async publishVideoStory(post: Post, page: SocialPage, media: Media): Promise<string> {
|
||||||
|
// Video stories can be published directly to the video_stories endpoint
|
||||||
|
const videoUrl = media.originalUrl;
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
access_token: page.accessToken!,
|
||||||
|
file_url: videoUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = `${this.baseUrl}/${page.pageId}/video_stories?${params.toString()}`;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json() as FacebookError;
|
||||||
|
throw new Error(`Facebook API error publishing video story: ${error.error.message} (code: ${error.error.code})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json() as { success: boolean; post_id: string };
|
||||||
|
return data.post_id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const facebookService = new FacebookService();
|
export const facebookService = new FacebookService();
|
||||||
Reference in new issue
Block a user