Merge pull request #72 from R0m1k3/dev

Dev
This commit is contained in:
LogiFlow authored and GitHub committed 2025-10-13 14:55:16 +02:00
commit e8d8afd047
5 files changed
+165 -24

No files matched your search

@@ -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

+3 -3
View File
@@ -250,15 +250,15 @@ export default function MediaUpload() {
</button>
</div>
</div>
) as any)}
))}
</div>
{/* É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">
<Loader2 className="w-5 h-5 text-primary animate-spin" />
</div>
)}
) : null}
</div>
</div>
</div>
+27 -15
View File
@@ -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 = () => (
<div className="bg-white rounded-lg overflow-hidden border border-gray-200 max-w-lg mx-auto">
{/* Header */}
@@ -87,11 +111,7 @@ export function PreviewModal({
{/* Media Carousel */}
{orderedMedia.length > 0 && (
<div className="relative bg-black">
<img
src={getMediaUrl(orderedMedia[currentPhotoIndex])}
alt={`Photo ${currentPhotoIndex + 1}`}
className="w-full h-auto"
/>
{renderMedia(orderedMedia[currentPhotoIndex], "w-full h-auto")}
{orderedMedia.length > 1 && (
<>
<button
@@ -149,11 +169,7 @@ export function PreviewModal({
{/* Media Carousel */}
{orderedMedia.length > 0 && (
<div className="relative bg-black aspect-square">
<img
src={getMediaUrl(orderedMedia[currentPhotoIndex])}
alt={`Photo ${currentPhotoIndex + 1}`}
className="w-full h-full object-cover"
/>
{renderMedia(orderedMedia[currentPhotoIndex], "w-full h-full object-cover")}
{orderedMedia.length > 1 && (
<>
<button
@@ -221,11 +237,7 @@ export function PreviewModal({
{/* Media */}
{orderedMedia.length > 0 && (
<div className="relative h-full">
<img
src={getMediaUrl(orderedMedia[currentPhotoIndex])}
alt={`Story ${currentPhotoIndex + 1}`}
className="w-full h-full object-cover"
/>
{renderMedia(orderedMedia[currentPhotoIndex], "w-full h-full object-cover")}
{orderedMedia.length > 1 && (
<>
<div className="absolute top-3 left-3 right-3 flex gap-1">
+63 -6
View File
@@ -10,6 +10,11 @@ interface FacebookFeedResponse {
id: string;
}
interface FacebookVideoResponse {
id: string;
post_id?: string;
}
interface FacebookError {
error: {
message: string;
@@ -54,8 +59,8 @@ export class FacebookService {
return storyIds.join(',');
} else {
// Default to feed
// Count images only (videos not supported in multi-photo carousel)
const imageMedia = mediaList.filter(m => m.type === 'image');
const videoMedia = mediaList.filter(m => m.type === 'video');
if (imageMedia.length > 1) {
// Multi-photo carousel (images only)
@@ -63,11 +68,9 @@ export class FacebookService {
} 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 if (videoMedia.length >= 1) {
// Video post - use video endpoint
return await this.publishVideoPost(post, page, videoMedia[0]);
} else {
// Text only
return await this.publishTextPost(post, page);
@@ -122,6 +125,32 @@ export class FacebookService {
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> {
// Validate that all media are images
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> {
if (media.type === 'video') {
return this.publishVideoStory(post, page, media);
}
// Stories require a 2-step process:
// Step 1: Upload photo as unpublished to get 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 };
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();