mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-11 17:26:45 +02:00
Introduce a new 'both' post type allowing simultaneous publishing to Facebook and Instagram feeds and stories. This includes UI updates for selecting post types, backend logic for splitting 'both' posts into separate feed and story entries, and the ability to delete scheduled posts. The changes also include a new 'Pages gérées' link in the sidebar and necessary database schema updates. Replit-Commit-Author: Agent Replit-Commit-Session-Id: ae4037a0-2a6f-4530-9bac-79b543286bda Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/397bca8c-984f-43ff-841a-10897aeb8140/ae4037a0-2a6f-4530-9bac-79b543286bda/WuFXYmm
210 lines
6.4 KiB
TypeScript
210 lines
6.4 KiB
TypeScript
import { storage } from '../storage';
|
|
import type { Post, SocialPage, Media } from '@shared/schema';
|
|
|
|
interface FacebookPhotoResponse {
|
|
id: string;
|
|
post_id: string;
|
|
}
|
|
|
|
interface FacebookFeedResponse {
|
|
id: string;
|
|
}
|
|
|
|
interface FacebookError {
|
|
error: {
|
|
message: string;
|
|
type: string;
|
|
code: number;
|
|
};
|
|
}
|
|
|
|
export class FacebookService {
|
|
private baseUrl = 'https://graph.facebook.com/v23.0';
|
|
|
|
async publishPost(post: Post, page: SocialPage, postType: string, media?: Media): Promise<string> {
|
|
if (page.platform !== 'facebook') {
|
|
throw new Error('This service only supports Facebook pages');
|
|
}
|
|
|
|
if (!page.accessToken) {
|
|
throw new Error('No access token found for this page');
|
|
}
|
|
|
|
// Handle different post types
|
|
// Note: "both" type should never reach here - new posts are split at creation
|
|
// If it does reach here (legacy data), throw an error
|
|
if (postType === 'both') {
|
|
throw new Error('postType "both" is not supported. Posts should be split into separate feed and story entries at creation.');
|
|
}
|
|
|
|
if (postType === 'story') {
|
|
// Publish as story only
|
|
if (!media) {
|
|
throw new Error('Stories require media (photo or video)');
|
|
}
|
|
return await this.publishStory(post, page, media);
|
|
} else {
|
|
// Default to feed
|
|
if (media) {
|
|
return await this.publishPhotoPost(post, page, media);
|
|
} else {
|
|
return await this.publishTextPost(post, page);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async publishTextPost(post: Post, page: SocialPage): Promise<string> {
|
|
const params = new URLSearchParams({
|
|
access_token: page.accessToken!,
|
|
message: post.content,
|
|
});
|
|
|
|
const url = `${this.baseUrl}/${page.pageId}/feed?${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: ${error.error.message} (code: ${error.error.code})`);
|
|
}
|
|
|
|
const data = await response.json() as FacebookFeedResponse;
|
|
return data.id;
|
|
}
|
|
|
|
private async publishPhotoPost(post: Post, page: SocialPage, media: Media): Promise<string> {
|
|
// Use the Facebook Feed optimized URL (1200x630)
|
|
const photoUrl = media.facebookFeedUrl || media.originalUrl;
|
|
|
|
const params = new URLSearchParams({
|
|
access_token: page.accessToken!,
|
|
message: post.content,
|
|
url: photoUrl,
|
|
});
|
|
|
|
const url = `${this.baseUrl}/${page.pageId}/photos?${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: ${error.error.message} (code: ${error.error.code})`);
|
|
}
|
|
|
|
const data = await response.json() as FacebookPhotoResponse;
|
|
// Return the post_id which is the full post identifier
|
|
return data.post_id || data.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');
|
|
}
|
|
|
|
if (!page.accessToken) {
|
|
throw new Error('No access token found for this page');
|
|
}
|
|
|
|
// Facebook requires Unix timestamp for scheduled posts
|
|
const scheduledTimestamp = Math.floor(scheduledTime.getTime() / 1000);
|
|
const now = Math.floor(Date.now() / 1000);
|
|
|
|
// Facebook requires scheduled time to be at least 10 minutes in the future
|
|
if (scheduledTimestamp < now + 600) {
|
|
throw new Error('Scheduled time must be at least 10 minutes in the future');
|
|
}
|
|
|
|
// Maximum 6 months in the future
|
|
const sixMonthsFromNow = now + (6 * 30 * 24 * 60 * 60);
|
|
if (scheduledTimestamp > sixMonthsFromNow) {
|
|
throw new Error('Scheduled time cannot be more than 6 months in the future');
|
|
}
|
|
|
|
// For scheduled posts with photos
|
|
if (media) {
|
|
return await this.schedulePhotoPost(post, page, scheduledTimestamp, media);
|
|
} else {
|
|
return await this.scheduleTextPost(post, page, scheduledTimestamp);
|
|
}
|
|
}
|
|
|
|
private async scheduleTextPost(post: Post, page: SocialPage, scheduledTimestamp: number): Promise<string> {
|
|
const params = new URLSearchParams({
|
|
access_token: page.accessToken!,
|
|
message: post.content,
|
|
published: '0',
|
|
scheduled_publish_time: scheduledTimestamp.toString(),
|
|
});
|
|
|
|
const url = `${this.baseUrl}/${page.pageId}/feed?${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: ${error.error.message} (code: ${error.error.code})`);
|
|
}
|
|
|
|
const data = await response.json() as FacebookFeedResponse;
|
|
return data.id;
|
|
}
|
|
|
|
private async schedulePhotoPost(post: Post, page: SocialPage, scheduledTimestamp: number, media: Media): Promise<string> {
|
|
const photoUrl = media.facebookFeedUrl || media.originalUrl;
|
|
|
|
const params = new URLSearchParams({
|
|
access_token: page.accessToken!,
|
|
message: post.content,
|
|
url: photoUrl,
|
|
published: '0',
|
|
scheduled_publish_time: scheduledTimestamp.toString(),
|
|
});
|
|
|
|
const url = `${this.baseUrl}/${page.pageId}/photos?${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: ${error.error.message} (code: ${error.error.code})`);
|
|
}
|
|
|
|
const data = await response.json() as FacebookPhotoResponse;
|
|
return data.post_id || data.id;
|
|
}
|
|
|
|
private async publishStory(post: Post, page: SocialPage, media: Media): Promise<string> {
|
|
// Use vertical format for stories (Instagram Story URL 1080x1920)
|
|
const photoUrl = media.instagramStoryUrl || media.originalUrl;
|
|
|
|
const params = new URLSearchParams({
|
|
access_token: page.accessToken!,
|
|
url: photoUrl,
|
|
});
|
|
|
|
const url = `${this.baseUrl}/${page.pageId}/photo_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: ${error.error.message} (code: ${error.error.code})`);
|
|
}
|
|
|
|
const data = await response.json() as FacebookPhotoResponse;
|
|
return data.id;
|
|
}
|
|
}
|
|
|
|
export const facebookService = new FacebookService();
|