From 0da4744f6fc745db90a49a6aea8b19c6f96c0200 Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Thu, 2 Oct 2025 13:39:12 +0000 Subject: [PATCH] Add ability to schedule and publish posts to Facebook Implement Facebook API integration for publishing text and photo posts, including scheduling functionality. 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/sjuGm8z --- server/services/facebook.ts | 174 ++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 server/services/facebook.ts diff --git a/server/services/facebook.ts b/server/services/facebook.ts new file mode 100644 index 0000000..b5e9ed9 --- /dev/null +++ b/server/services/facebook.ts @@ -0,0 +1,174 @@ +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 { + 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'); + } + + // If there's media, publish as photo post, otherwise as text post + if (media) { + return await this.publishPhotoPost(post, page, media); + } else { + return await this.publishTextPost(post, page); + } + } + + private async publishTextPost(post: Post, page: SocialPage): Promise { + const url = `${this.baseUrl}/${page.pageId}/feed`; + + const params = new URLSearchParams({ + access_token: page.accessToken!, + message: post.content, + }); + + const response = await fetch(url, { + method: 'POST', + body: params, + }); + + 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 { + const url = `${this.baseUrl}/${page.pageId}/photos`; + + // 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 response = await fetch(url, { + method: 'POST', + body: params, + }); + + 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 { + 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 { + const url = `${this.baseUrl}/${page.pageId}/feed`; + + const params = new URLSearchParams({ + access_token: page.accessToken!, + message: post.content, + published: 'false', + scheduled_publish_time: scheduledTimestamp.toString(), + }); + + const response = await fetch(url, { + method: 'POST', + body: params, + }); + + 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 { + const url = `${this.baseUrl}/${page.pageId}/photos`; + + const photoUrl = media.facebookFeedUrl || media.originalUrl; + + const params = new URLSearchParams({ + access_token: page.accessToken!, + message: post.content, + url: photoUrl, + published: 'false', + scheduled_publish_time: scheduledTimestamp.toString(), + }); + + const response = await fetch(url, { + method: 'POST', + body: params, + }); + + 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; + } +} + +export const facebookService = new FacebookService();