mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-21 22:18:12 +02:00
5 files changed
+213
-4
No files matched your search
@@ -18,6 +18,10 @@ externalPort = 80
|
|||||||
localPort = 5555
|
localPort = 5555
|
||||||
externalPort = 3002
|
externalPort = 3002
|
||||||
|
|
||||||
|
[[ports]]
|
||||||
|
localPort = 37219
|
||||||
|
externalPort = 3003
|
||||||
|
|
||||||
[[ports]]
|
[[ports]]
|
||||||
localPort = 40537
|
localPort = 40537
|
||||||
externalPort = 3000
|
externalPort = 3000
|
||||||
|
|||||||
@@ -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<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');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<string> {
|
||||||
|
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<string> {
|
||||||
|
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<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 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<string> {
|
||||||
|
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();
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import cron, { ScheduledTask } from "node-cron";
|
import cron, { ScheduledTask } from "node-cron";
|
||||||
import { storage } from "../storage";
|
import { storage } from "../storage";
|
||||||
|
import { facebookService } from "./facebook";
|
||||||
|
|
||||||
export class SchedulerService {
|
export class SchedulerService {
|
||||||
private task: ScheduledTask | null = null;
|
private task: ScheduledTask | null = null;
|
||||||
@@ -47,14 +48,35 @@ export class SchedulerService {
|
|||||||
throw new Error("Post or page not found");
|
throw new Error("Post or page not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// In a real implementation, this would call Facebook/Instagram Graph API
|
|
||||||
// For now, we'll simulate the publishing
|
|
||||||
console.log(`Publishing post ${post.id} to ${page.platform} page ${page.pageName}`);
|
console.log(`Publishing post ${post.id} to ${page.platform} page ${page.pageName}`);
|
||||||
|
|
||||||
|
// Get media if post has media
|
||||||
|
const postMediaList = await storage.getPostMedia(post.id);
|
||||||
|
const media = postMediaList.length > 0 ? await storage.getMediaById(postMediaList[0].mediaId) : undefined;
|
||||||
|
|
||||||
|
// Publish to Facebook/Instagram
|
||||||
|
let externalPostId: string;
|
||||||
|
|
||||||
|
if (page.platform === 'facebook') {
|
||||||
|
externalPostId = await facebookService.publishPost(post, page, scheduledPost.postType, media || undefined);
|
||||||
|
} else if (page.platform === 'instagram') {
|
||||||
|
// Instagram publishing not yet implemented
|
||||||
|
console.warn(`Instagram publishing not yet implemented for post ${post.id}`);
|
||||||
|
await storage.updateScheduledPost(scheduledPost.id, {
|
||||||
|
error: 'Instagram publishing not yet implemented',
|
||||||
|
});
|
||||||
|
await storage.updatePost(post.id, {
|
||||||
|
status: "failed",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unsupported platform: ${page.platform}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Update the scheduled post as published
|
// Update the scheduled post as published
|
||||||
await storage.updateScheduledPost(scheduledPost.id, {
|
await storage.updateScheduledPost(scheduledPost.id, {
|
||||||
publishedAt: new Date(),
|
publishedAt: new Date(),
|
||||||
externalPostId: `simulated-${Date.now()}`, // In real app, this would be the ID from FB/IG
|
externalPostId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update post status
|
// Update post status
|
||||||
@@ -62,7 +84,7 @@ export class SchedulerService {
|
|||||||
status: "published",
|
status: "published",
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Successfully published post ${post.id}`);
|
console.log(`Successfully published post ${post.id} with external ID: ${externalPostId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
socialPages,
|
socialPages,
|
||||||
media,
|
media,
|
||||||
posts,
|
posts,
|
||||||
|
postMedia,
|
||||||
scheduledPosts,
|
scheduledPosts,
|
||||||
aiGenerations,
|
aiGenerations,
|
||||||
cloudinaryConfig,
|
cloudinaryConfig,
|
||||||
@@ -15,6 +16,7 @@ import {
|
|||||||
type InsertMedia,
|
type InsertMedia,
|
||||||
type Post,
|
type Post,
|
||||||
type InsertPost,
|
type InsertPost,
|
||||||
|
type PostMedia,
|
||||||
type ScheduledPost,
|
type ScheduledPost,
|
||||||
type InsertScheduledPost,
|
type InsertScheduledPost,
|
||||||
type AiGeneration,
|
type AiGeneration,
|
||||||
@@ -48,6 +50,7 @@ export interface IStorage {
|
|||||||
getMediaById(id: string): Promise<Media | undefined>;
|
getMediaById(id: string): Promise<Media | undefined>;
|
||||||
createMedia(media: InsertMedia): Promise<Media>;
|
createMedia(media: InsertMedia): Promise<Media>;
|
||||||
deleteMedia(id: string): Promise<void>;
|
deleteMedia(id: string): Promise<void>;
|
||||||
|
getPostMedia(postId: string): Promise<PostMedia[]>;
|
||||||
|
|
||||||
// Posts
|
// Posts
|
||||||
getPosts(userId: string): Promise<Post[]>;
|
getPosts(userId: string): Promise<Post[]>;
|
||||||
@@ -152,6 +155,10 @@ export class DatabaseStorage implements IStorage {
|
|||||||
await db.delete(media).where(eq(media.id, id));
|
await db.delete(media).where(eq(media.id, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getPostMedia(postId: string): Promise<PostMedia[]> {
|
||||||
|
return await db.select().from(postMedia).where(eq(postMedia.postId, postId));
|
||||||
|
}
|
||||||
|
|
||||||
// Posts
|
// Posts
|
||||||
async getPosts(userId: string): Promise<Post[]> {
|
async getPosts(userId: string): Promise<Post[]> {
|
||||||
return await db.select().from(posts).where(eq(posts.userId, userId)).orderBy(desc(posts.createdAt));
|
return await db.select().from(posts).where(eq(posts.userId, userId)).orderBy(desc(posts.createdAt));
|
||||||
|
|||||||
@@ -254,3 +254,5 @@ export type InsertCloudinaryConfig = z.infer<typeof insertCloudinaryConfigSchema
|
|||||||
|
|
||||||
export type OpenrouterConfig = typeof openrouterConfig.$inferSelect;
|
export type OpenrouterConfig = typeof openrouterConfig.$inferSelect;
|
||||||
export type InsertOpenrouterConfig = z.infer<typeof insertOpenrouterConfigSchema>;
|
export type InsertOpenrouterConfig = z.infer<typeof insertOpenrouterConfigSchema>;
|
||||||
|
|
||||||
|
export type PostMedia = typeof postMedia.$inferSelect;
|
||||||
Reference in new issue
Block a user