mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-22 22:48:09 +02:00
Add Docker support and installation guide for easy server deployment
Includes a Dockerfile for building the application image and an INSTALLATION.md file detailing the setup process, prerequisites, and helpful commands for managing the application on a private server. 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/ds3R7vP
This commit is contained in:
1 parent
bbad954935
commit
f881f668a6
89 files changed
+17095
No files matched your search
@@ -0,0 +1,15 @@
|
||||
import { Pool, neonConfig } from '@neondatabase/serverless';
|
||||
import { drizzle } from 'drizzle-orm/neon-serverless';
|
||||
import ws from "ws";
|
||||
import * as schema from "@shared/schema";
|
||||
|
||||
neonConfig.webSocketConstructor = ws;
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
throw new Error(
|
||||
"DATABASE_URL must be set. Did you forget to provision a database?",
|
||||
);
|
||||
}
|
||||
|
||||
export const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
||||
export const db = drizzle({ client: pool, schema });
|
||||
@@ -0,0 +1,85 @@
|
||||
import express, { type Request, Response, NextFunction } from "express";
|
||||
import { registerRoutes } from "./routes";
|
||||
import { setupVite, serveStatic, log } from "./vite";
|
||||
import { schedulerService } from "./services/scheduler";
|
||||
|
||||
const app = express();
|
||||
|
||||
declare module 'http' {
|
||||
interface IncomingMessage {
|
||||
rawBody: unknown
|
||||
}
|
||||
}
|
||||
app.use(express.json({
|
||||
verify: (req, _res, buf) => {
|
||||
req.rawBody = buf;
|
||||
}
|
||||
}));
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
|
||||
app.use((req, res, next) => {
|
||||
const start = Date.now();
|
||||
const path = req.path;
|
||||
let capturedJsonResponse: Record<string, any> | undefined = undefined;
|
||||
|
||||
const originalResJson = res.json;
|
||||
res.json = function (bodyJson, ...args) {
|
||||
capturedJsonResponse = bodyJson;
|
||||
return originalResJson.apply(res, [bodyJson, ...args]);
|
||||
};
|
||||
|
||||
res.on("finish", () => {
|
||||
const duration = Date.now() - start;
|
||||
if (path.startsWith("/api")) {
|
||||
let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`;
|
||||
if (capturedJsonResponse) {
|
||||
logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`;
|
||||
}
|
||||
|
||||
if (logLine.length > 80) {
|
||||
logLine = logLine.slice(0, 79) + "…";
|
||||
}
|
||||
|
||||
log(logLine);
|
||||
}
|
||||
});
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
(async () => {
|
||||
const server = await registerRoutes(app);
|
||||
|
||||
app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
|
||||
const status = err.status || err.statusCode || 500;
|
||||
const message = err.message || "Internal Server Error";
|
||||
|
||||
res.status(status).json({ message });
|
||||
throw err;
|
||||
});
|
||||
|
||||
// importantly only setup vite in development and after
|
||||
// setting up all the other routes so the catch-all route
|
||||
// doesn't interfere with the other routes
|
||||
if (app.get("env") === "development") {
|
||||
await setupVite(app, server);
|
||||
} else {
|
||||
serveStatic(app);
|
||||
}
|
||||
|
||||
// ALWAYS serve the app on the port specified in the environment variable PORT
|
||||
// Other ports are firewalled. Default to 5000 if not specified.
|
||||
// this serves both the API and the client.
|
||||
// It is the only port that is not firewalled.
|
||||
const port = parseInt(process.env.PORT || '5000', 10);
|
||||
server.listen({
|
||||
port,
|
||||
host: "0.0.0.0",
|
||||
reusePort: true,
|
||||
}, () => {
|
||||
log(`serving on port ${port}`);
|
||||
|
||||
// Démarrer le scheduler pour les publications programmées
|
||||
schedulerService.start();
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,201 @@
|
||||
import type { Express } from "express";
|
||||
import { createServer, type Server } from "http";
|
||||
import { storage } from "./storage";
|
||||
import multer from "multer";
|
||||
import { openRouterService } from "./services/openrouter";
|
||||
import { imageProcessor } from "./services/imageProcessor";
|
||||
import { insertPostSchema, insertScheduledPostSchema, insertSocialPageSchema, insertAiGenerationSchema } from "@shared/schema";
|
||||
import express from "express";
|
||||
import path from "path";
|
||||
|
||||
const upload = multer({ storage: multer.memoryStorage() });
|
||||
|
||||
export async function registerRoutes(app: Express): Promise<Server> {
|
||||
// Serve uploaded files
|
||||
app.use("/uploads", express.static(path.join(process.cwd(), "uploads")));
|
||||
|
||||
// Stats endpoint
|
||||
app.get("/api/stats", async (req, res) => {
|
||||
try {
|
||||
// For now, return mock stats. In a real app, calculate from database
|
||||
const userId = "demo-user"; // In real app, get from session/auth
|
||||
|
||||
const posts = await storage.getPosts(userId);
|
||||
const pages = await storage.getSocialPages(userId);
|
||||
const media = await storage.getMedia(userId);
|
||||
const aiGenerations = await storage.getAiGenerations(userId);
|
||||
|
||||
const scheduledPosts = posts.filter(p => p.status === "scheduled").length;
|
||||
|
||||
res.json({
|
||||
scheduledPosts,
|
||||
connectedPages: pages.length,
|
||||
aiTextsGenerated: aiGenerations.length,
|
||||
mediaStored: media.length,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching stats:", error);
|
||||
res.status(500).json({ error: "Failed to fetch stats" });
|
||||
}
|
||||
});
|
||||
|
||||
// AI text generation
|
||||
app.post("/api/ai/generate", async (req, res) => {
|
||||
try {
|
||||
const productInfo = req.body;
|
||||
const userId = "demo-user"; // In real app, get from session/auth
|
||||
|
||||
const generatedTexts = await openRouterService.generatePostText(productInfo);
|
||||
|
||||
// Save to database
|
||||
await storage.createAiGeneration({
|
||||
userId,
|
||||
productInfo,
|
||||
generatedTexts,
|
||||
});
|
||||
|
||||
res.json({ variants: generatedTexts });
|
||||
} catch (error) {
|
||||
console.error("Error generating text:", error);
|
||||
res.status(500).json({ error: "Failed to generate text" });
|
||||
}
|
||||
});
|
||||
|
||||
// Media upload
|
||||
app.post("/api/media/upload", upload.single("file"), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: "No file uploaded" });
|
||||
}
|
||||
|
||||
const userId = "demo-user"; // In real app, get from session/auth
|
||||
|
||||
const originalUrl = await imageProcessor.saveOriginalFile(req.file.buffer, req.file.originalname);
|
||||
|
||||
let processedUrls = null;
|
||||
if (req.file.mimetype.startsWith("image/")) {
|
||||
processedUrls = await imageProcessor.processImage(req.file.buffer, req.file.originalname);
|
||||
}
|
||||
|
||||
const mediaItem = await storage.createMedia({
|
||||
userId,
|
||||
type: req.file.mimetype.startsWith("video/") ? "video" : "image",
|
||||
originalUrl,
|
||||
facebookFeedUrl: processedUrls?.facebookFeed || null,
|
||||
instagramFeedUrl: processedUrls?.instagramFeed || null,
|
||||
instagramStoryUrl: processedUrls?.instagramStory || null,
|
||||
fileName: req.file.originalname,
|
||||
fileSize: req.file.size,
|
||||
});
|
||||
|
||||
res.json(mediaItem);
|
||||
} catch (error) {
|
||||
console.error("Error uploading media:", error);
|
||||
res.status(500).json({ error: "Failed to upload media" });
|
||||
}
|
||||
});
|
||||
|
||||
// Get media
|
||||
app.get("/api/media", async (req, res) => {
|
||||
try {
|
||||
const userId = "demo-user"; // In real app, get from session/auth
|
||||
const media = await storage.getMedia(userId);
|
||||
res.json(media);
|
||||
} catch (error) {
|
||||
console.error("Error fetching media:", error);
|
||||
res.status(500).json({ error: "Failed to fetch media" });
|
||||
}
|
||||
});
|
||||
|
||||
// Posts
|
||||
app.get("/api/posts", async (req, res) => {
|
||||
try {
|
||||
const userId = "demo-user"; // In real app, get from session/auth
|
||||
const posts = await storage.getPosts(userId);
|
||||
res.json(posts);
|
||||
} catch (error) {
|
||||
console.error("Error fetching posts:", error);
|
||||
res.status(500).json({ error: "Failed to fetch posts" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/posts", async (req, res) => {
|
||||
try {
|
||||
const userId = "demo-user"; // In real app, get from session/auth
|
||||
const postData = insertPostSchema.parse({ ...req.body, userId });
|
||||
const post = await storage.createPost(postData);
|
||||
res.json(post);
|
||||
} catch (error) {
|
||||
console.error("Error creating post:", error);
|
||||
res.status(500).json({ error: "Failed to create post" });
|
||||
}
|
||||
});
|
||||
|
||||
// Scheduled posts
|
||||
app.get("/api/scheduled-posts", async (req, res) => {
|
||||
try {
|
||||
const userId = "demo-user"; // In real app, get from session/auth
|
||||
const { startDate, endDate } = req.query;
|
||||
|
||||
const start = startDate ? new Date(startDate as string) : undefined;
|
||||
const end = endDate ? new Date(endDate as string) : undefined;
|
||||
|
||||
const scheduledPosts = await storage.getScheduledPosts(userId, start, end);
|
||||
res.json(scheduledPosts);
|
||||
} catch (error) {
|
||||
console.error("Error fetching scheduled posts:", error);
|
||||
res.status(500).json({ error: "Failed to fetch scheduled posts" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/scheduled-posts", async (req, res) => {
|
||||
try {
|
||||
const scheduledPostData = insertScheduledPostSchema.parse(req.body);
|
||||
const scheduledPost = await storage.createScheduledPost(scheduledPostData);
|
||||
res.json(scheduledPost);
|
||||
} catch (error) {
|
||||
console.error("Error creating scheduled post:", error);
|
||||
res.status(500).json({ error: "Failed to create scheduled post" });
|
||||
}
|
||||
});
|
||||
|
||||
// Social pages
|
||||
app.get("/api/pages", async (req, res) => {
|
||||
try {
|
||||
const userId = "demo-user"; // In real app, get from session/auth
|
||||
const pages = await storage.getSocialPages(userId);
|
||||
res.json(pages);
|
||||
} catch (error) {
|
||||
console.error("Error fetching pages:", error);
|
||||
res.status(500).json({ error: "Failed to fetch pages" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/pages", async (req, res) => {
|
||||
try {
|
||||
const userId = "demo-user"; // In real app, get from session/auth
|
||||
const pageData = insertSocialPageSchema.parse({ ...req.body, userId });
|
||||
const page = await storage.createSocialPage(pageData);
|
||||
res.json(page);
|
||||
} catch (error) {
|
||||
console.error("Error creating page:", error);
|
||||
res.status(500).json({ error: "Failed to create page" });
|
||||
}
|
||||
});
|
||||
|
||||
// AI Generations
|
||||
app.get("/api/ai/generations", async (req, res) => {
|
||||
try {
|
||||
const userId = "demo-user"; // In real app, get from session/auth
|
||||
const generations = await storage.getAiGenerations(userId);
|
||||
res.json(generations);
|
||||
} catch (error) {
|
||||
console.error("Error fetching AI generations:", error);
|
||||
res.status(500).json({ error: "Failed to fetch AI generations" });
|
||||
}
|
||||
});
|
||||
|
||||
const httpServer = createServer(app);
|
||||
|
||||
return httpServer;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import sharp from "sharp";
|
||||
import { randomUUID } from "crypto";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
|
||||
interface CropDimensions {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface ProcessedImages {
|
||||
facebookFeed: string;
|
||||
instagramFeed: string;
|
||||
instagramStory: string;
|
||||
}
|
||||
|
||||
const CROP_FORMATS = {
|
||||
facebookFeed: { width: 1200, height: 630 } as CropDimensions,
|
||||
instagramFeed: { width: 1080, height: 1080 } as CropDimensions,
|
||||
instagramStory: { width: 1080, height: 1920 } as CropDimensions,
|
||||
};
|
||||
|
||||
export class ImageProcessor {
|
||||
private uploadsDir: string;
|
||||
|
||||
constructor() {
|
||||
this.uploadsDir = path.join(process.cwd(), "uploads");
|
||||
this.ensureUploadsDirExists();
|
||||
}
|
||||
|
||||
private async ensureUploadsDirExists() {
|
||||
try {
|
||||
await fs.access(this.uploadsDir);
|
||||
} catch {
|
||||
await fs.mkdir(this.uploadsDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
async processImage(buffer: Buffer, originalFilename: string): Promise<ProcessedImages> {
|
||||
const fileExt = path.extname(originalFilename);
|
||||
const baseId = randomUUID();
|
||||
|
||||
const processedImages: ProcessedImages = {
|
||||
facebookFeed: "",
|
||||
instagramFeed: "",
|
||||
instagramStory: "",
|
||||
};
|
||||
|
||||
// Process Facebook Feed (1200x630)
|
||||
const fbFeedFilename = `${baseId}-fb-feed${fileExt}`;
|
||||
const fbFeedPath = path.join(this.uploadsDir, fbFeedFilename);
|
||||
await sharp(buffer)
|
||||
.resize(CROP_FORMATS.facebookFeed.width, CROP_FORMATS.facebookFeed.height, {
|
||||
fit: "cover",
|
||||
position: "center",
|
||||
})
|
||||
.toFile(fbFeedPath);
|
||||
processedImages.facebookFeed = `/uploads/${fbFeedFilename}`;
|
||||
|
||||
// Process Instagram Feed (1080x1080)
|
||||
const igFeedFilename = `${baseId}-ig-feed${fileExt}`;
|
||||
const igFeedPath = path.join(this.uploadsDir, igFeedFilename);
|
||||
await sharp(buffer)
|
||||
.resize(CROP_FORMATS.instagramFeed.width, CROP_FORMATS.instagramFeed.height, {
|
||||
fit: "cover",
|
||||
position: "center",
|
||||
})
|
||||
.toFile(igFeedPath);
|
||||
processedImages.instagramFeed = `/uploads/${igFeedFilename}`;
|
||||
|
||||
// Process Instagram Story (1080x1920)
|
||||
const igStoryFilename = `${baseId}-ig-story${fileExt}`;
|
||||
const igStoryPath = path.join(this.uploadsDir, igStoryFilename);
|
||||
await sharp(buffer)
|
||||
.resize(CROP_FORMATS.instagramStory.width, CROP_FORMATS.instagramStory.height, {
|
||||
fit: "cover",
|
||||
position: "center",
|
||||
})
|
||||
.toFile(igStoryPath);
|
||||
processedImages.instagramStory = `/uploads/${igStoryFilename}`;
|
||||
|
||||
return processedImages;
|
||||
}
|
||||
|
||||
async saveOriginalFile(buffer: Buffer, filename: string): Promise<string> {
|
||||
const uniqueFilename = `${randomUUID()}-${filename}`;
|
||||
const filePath = path.join(this.uploadsDir, uniqueFilename);
|
||||
await fs.writeFile(filePath, buffer);
|
||||
return `/uploads/${uniqueFilename}`;
|
||||
}
|
||||
|
||||
async deleteFile(filePath: string): Promise<void> {
|
||||
try {
|
||||
const fullPath = path.join(process.cwd(), filePath);
|
||||
await fs.unlink(fullPath);
|
||||
} catch (error) {
|
||||
console.error("Error deleting file:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const imageProcessor = new ImageProcessor();
|
||||
@@ -0,0 +1,148 @@
|
||||
interface ProductInfo {
|
||||
name: string;
|
||||
description?: string;
|
||||
price?: string;
|
||||
features?: string[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface GeneratedText {
|
||||
variant: string;
|
||||
text: string;
|
||||
characterCount: number;
|
||||
}
|
||||
|
||||
export class OpenRouterService {
|
||||
private apiKey: string;
|
||||
private baseUrl = "https://openrouter.ai/api/v1/chat/completions";
|
||||
|
||||
constructor() {
|
||||
this.apiKey = process.env.OPENROUTER_API_KEY || "";
|
||||
if (!this.apiKey) {
|
||||
console.warn("OPENROUTER_API_KEY not set. AI text generation will not work.");
|
||||
}
|
||||
}
|
||||
|
||||
async generatePostText(productInfo: ProductInfo): Promise<GeneratedText[]> {
|
||||
if (!this.apiKey) {
|
||||
throw new Error("OpenRouter API key not configured");
|
||||
}
|
||||
|
||||
const prompt = this.buildPrompt(productInfo);
|
||||
|
||||
try {
|
||||
const response = await fetch(this.baseUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${this.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
"HTTP-Referer": process.env.APP_URL || "http://localhost:5000",
|
||||
"X-Title": "Social Flow"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: "anthropic/claude-3.5-sonnet",
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: prompt
|
||||
}
|
||||
],
|
||||
temperature: 0.8,
|
||||
max_tokens: 1000
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`OpenRouter API error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const content = data.choices[0]?.message?.content || "";
|
||||
|
||||
return this.parseGeneratedText(content);
|
||||
} catch (error) {
|
||||
console.error("Error generating text with OpenRouter:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private buildPrompt(productInfo: ProductInfo): string {
|
||||
const features = productInfo.features?.join(", ") || "";
|
||||
|
||||
return `Tu es un expert en marketing sur les réseaux sociaux. Génère 3 variantes de texte pour une publication Facebook/Instagram pour ce produit:
|
||||
|
||||
Nom: ${productInfo.name}
|
||||
${productInfo.description ? `Description: ${productInfo.description}` : ''}
|
||||
${productInfo.price ? `Prix: ${productInfo.price}` : ''}
|
||||
${features ? `Caractéristiques: ${features}` : ''}
|
||||
|
||||
Crée 3 versions différentes:
|
||||
1. Version Dynamique - Énergique et engageante avec des emojis
|
||||
2. Version Informative - Factuelle et professionnelle
|
||||
3. Version Émotionnelle - Axée sur les bénéfices émotionnels
|
||||
|
||||
Chaque version doit:
|
||||
- Être en français
|
||||
- Faire entre 150-250 caractères
|
||||
- Être optimisée pour les réseaux sociaux
|
||||
- Inclure des emojis pertinents (sauf la version informative)
|
||||
|
||||
Format de réponse:
|
||||
VERSION 1 - DYNAMIQUE:
|
||||
[texte]
|
||||
|
||||
VERSION 2 - INFORMATIVE:
|
||||
[texte]
|
||||
|
||||
VERSION 3 - ÉMOTIONNELLE:
|
||||
[texte]`;
|
||||
}
|
||||
|
||||
private parseGeneratedText(content: string): GeneratedText[] {
|
||||
const variants: GeneratedText[] = [];
|
||||
|
||||
const version1Match = content.match(/VERSION 1 - DYNAMIQUE:\s*\n([\s\S]*?)(?=VERSION 2|$)/i);
|
||||
const version2Match = content.match(/VERSION 2 - INFORMATIVE:\s*\n([\s\S]*?)(?=VERSION 3|$)/i);
|
||||
const version3Match = content.match(/VERSION 3 - ÉMOTIONNELLE:\s*\n([\s\S]*?)$/i);
|
||||
|
||||
if (version1Match) {
|
||||
const text = version1Match[1].trim();
|
||||
variants.push({
|
||||
variant: "Version 1 - Dynamique",
|
||||
text,
|
||||
characterCount: text.length
|
||||
});
|
||||
}
|
||||
|
||||
if (version2Match) {
|
||||
const text = version2Match[1].trim();
|
||||
variants.push({
|
||||
variant: "Version 2 - Informative",
|
||||
text,
|
||||
characterCount: text.length
|
||||
});
|
||||
}
|
||||
|
||||
if (version3Match) {
|
||||
const text = version3Match[1].trim();
|
||||
variants.push({
|
||||
variant: "Version 3 - Émotionnelle",
|
||||
text,
|
||||
characterCount: text.length
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback if parsing fails
|
||||
if (variants.length === 0) {
|
||||
variants.push({
|
||||
variant: "Généré par IA",
|
||||
text: content.trim(),
|
||||
characterCount: content.trim().length
|
||||
});
|
||||
}
|
||||
|
||||
return variants;
|
||||
}
|
||||
}
|
||||
|
||||
export const openRouterService = new OpenRouterService();
|
||||
@@ -0,0 +1,69 @@
|
||||
import cron, { ScheduledTask } from "node-cron";
|
||||
import { storage } from "../storage";
|
||||
|
||||
export class SchedulerService {
|
||||
private task: ScheduledTask | null = null;
|
||||
|
||||
start() {
|
||||
// Run every minute to check for pending posts
|
||||
this.task = cron.schedule("* * * * *", async () => {
|
||||
await this.processPendingPosts();
|
||||
});
|
||||
|
||||
console.log("Scheduler service started");
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.task) {
|
||||
this.task.stop();
|
||||
console.log("Scheduler service stopped");
|
||||
}
|
||||
}
|
||||
|
||||
private async processPendingPosts() {
|
||||
try {
|
||||
const pendingPosts = await storage.getPendingScheduledPosts();
|
||||
|
||||
for (const scheduledPost of pendingPosts) {
|
||||
try {
|
||||
await this.publishPost(scheduledPost);
|
||||
} catch (error) {
|
||||
console.error(`Error publishing post ${scheduledPost.id}:`, error);
|
||||
await storage.updateScheduledPost(scheduledPost.id, {
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error processing pending posts:", error);
|
||||
}
|
||||
}
|
||||
|
||||
private async publishPost(scheduledPost: any) {
|
||||
const post = await storage.getPost(scheduledPost.postId);
|
||||
const page = await storage.getSocialPage(scheduledPost.pageId);
|
||||
|
||||
if (!post || !page) {
|
||||
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}`);
|
||||
|
||||
// Update the scheduled post as published
|
||||
await storage.updateScheduledPost(scheduledPost.id, {
|
||||
publishedAt: new Date(),
|
||||
externalPostId: `simulated-${Date.now()}`, // In real app, this would be the ID from FB/IG
|
||||
});
|
||||
|
||||
// Update post status
|
||||
await storage.updatePost(post.id, {
|
||||
status: "published",
|
||||
});
|
||||
|
||||
console.log(`Successfully published post ${post.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const schedulerService = new SchedulerService();
|
||||
@@ -0,0 +1,213 @@
|
||||
import {
|
||||
users,
|
||||
socialPages,
|
||||
media,
|
||||
posts,
|
||||
scheduledPosts,
|
||||
aiGenerations,
|
||||
type User,
|
||||
type InsertUser,
|
||||
type SocialPage,
|
||||
type InsertSocialPage,
|
||||
type Media,
|
||||
type InsertMedia,
|
||||
type Post,
|
||||
type InsertPost,
|
||||
type ScheduledPost,
|
||||
type InsertScheduledPost,
|
||||
type AiGeneration,
|
||||
type InsertAiGeneration,
|
||||
} from "@shared/schema";
|
||||
import { db } from "./db";
|
||||
import { eq, and, gte, lte, desc, isNull } from "drizzle-orm";
|
||||
|
||||
export interface IStorage {
|
||||
// Users
|
||||
getUser(id: string): Promise<User | undefined>;
|
||||
getUserByUsername(username: string): Promise<User | undefined>;
|
||||
createUser(user: InsertUser): Promise<User>;
|
||||
|
||||
// Social Pages
|
||||
getSocialPages(userId: string): Promise<SocialPage[]>;
|
||||
getSocialPage(id: string): Promise<SocialPage | undefined>;
|
||||
createSocialPage(page: InsertSocialPage): Promise<SocialPage>;
|
||||
updateSocialPage(id: string, page: Partial<InsertSocialPage>): Promise<SocialPage>;
|
||||
deleteSocialPage(id: string): Promise<void>;
|
||||
|
||||
// Media
|
||||
getMedia(userId: string): Promise<Media[]>;
|
||||
getMediaById(id: string): Promise<Media | undefined>;
|
||||
createMedia(media: InsertMedia): Promise<Media>;
|
||||
deleteMedia(id: string): Promise<void>;
|
||||
|
||||
// Posts
|
||||
getPosts(userId: string): Promise<Post[]>;
|
||||
getPost(id: string): Promise<Post | undefined>;
|
||||
createPost(post: InsertPost): Promise<Post>;
|
||||
updatePost(id: string, post: Partial<InsertPost>): Promise<Post>;
|
||||
deletePost(id: string): Promise<void>;
|
||||
|
||||
// Scheduled Posts
|
||||
getScheduledPosts(userId: string, startDate?: Date, endDate?: Date): Promise<ScheduledPost[]>;
|
||||
getScheduledPost(id: string): Promise<ScheduledPost | undefined>;
|
||||
createScheduledPost(scheduledPost: InsertScheduledPost): Promise<ScheduledPost>;
|
||||
updateScheduledPost(id: string, scheduledPost: Partial<ScheduledPost>): Promise<ScheduledPost>;
|
||||
deleteScheduledPost(id: string): Promise<void>;
|
||||
getPendingScheduledPosts(): Promise<ScheduledPost[]>;
|
||||
|
||||
// AI Generations
|
||||
getAiGenerations(userId: string): Promise<AiGeneration[]>;
|
||||
createAiGeneration(generation: InsertAiGeneration): Promise<AiGeneration>;
|
||||
}
|
||||
|
||||
export class DatabaseStorage implements IStorage {
|
||||
// Users
|
||||
async getUser(id: string): Promise<User | undefined> {
|
||||
const [user] = await db.select().from(users).where(eq(users.id, id));
|
||||
return user || undefined;
|
||||
}
|
||||
|
||||
async getUserByUsername(username: string): Promise<User | undefined> {
|
||||
const [user] = await db.select().from(users).where(eq(users.username, username));
|
||||
return user || undefined;
|
||||
}
|
||||
|
||||
async createUser(insertUser: InsertUser): Promise<User> {
|
||||
const [user] = await db.insert(users).values(insertUser).returning();
|
||||
return user;
|
||||
}
|
||||
|
||||
// Social Pages
|
||||
async getSocialPages(userId: string): Promise<SocialPage[]> {
|
||||
return await db.select().from(socialPages).where(eq(socialPages.userId, userId));
|
||||
}
|
||||
|
||||
async getSocialPage(id: string): Promise<SocialPage | undefined> {
|
||||
const [page] = await db.select().from(socialPages).where(eq(socialPages.id, id));
|
||||
return page || undefined;
|
||||
}
|
||||
|
||||
async createSocialPage(page: InsertSocialPage): Promise<SocialPage> {
|
||||
const [newPage] = await db.insert(socialPages).values(page).returning();
|
||||
return newPage;
|
||||
}
|
||||
|
||||
async updateSocialPage(id: string, page: Partial<InsertSocialPage>): Promise<SocialPage> {
|
||||
const [updated] = await db.update(socialPages).set(page).where(eq(socialPages.id, id)).returning();
|
||||
return updated;
|
||||
}
|
||||
|
||||
async deleteSocialPage(id: string): Promise<void> {
|
||||
await db.delete(socialPages).where(eq(socialPages.id, id));
|
||||
}
|
||||
|
||||
// Media
|
||||
async getMedia(userId: string): Promise<Media[]> {
|
||||
return await db.select().from(media).where(eq(media.userId, userId)).orderBy(desc(media.createdAt));
|
||||
}
|
||||
|
||||
async getMediaById(id: string): Promise<Media | undefined> {
|
||||
const [mediaItem] = await db.select().from(media).where(eq(media.id, id));
|
||||
return mediaItem || undefined;
|
||||
}
|
||||
|
||||
async createMedia(mediaItem: InsertMedia): Promise<Media> {
|
||||
const [newMedia] = await db.insert(media).values(mediaItem).returning();
|
||||
return newMedia;
|
||||
}
|
||||
|
||||
async deleteMedia(id: string): Promise<void> {
|
||||
await db.delete(media).where(eq(media.id, id));
|
||||
}
|
||||
|
||||
// Posts
|
||||
async getPosts(userId: string): Promise<Post[]> {
|
||||
return await db.select().from(posts).where(eq(posts.userId, userId)).orderBy(desc(posts.createdAt));
|
||||
}
|
||||
|
||||
async getPost(id: string): Promise<Post | undefined> {
|
||||
const [post] = await db.select().from(posts).where(eq(posts.id, id));
|
||||
return post || undefined;
|
||||
}
|
||||
|
||||
async createPost(post: InsertPost): Promise<Post> {
|
||||
const [newPost] = await db.insert(posts).values(post).returning();
|
||||
return newPost;
|
||||
}
|
||||
|
||||
async updatePost(id: string, post: Partial<InsertPost>): Promise<Post> {
|
||||
const [updated] = await db.update(posts).set({ ...post, updatedAt: new Date() }).where(eq(posts.id, id)).returning();
|
||||
return updated;
|
||||
}
|
||||
|
||||
async deletePost(id: string): Promise<void> {
|
||||
await db.delete(posts).where(eq(posts.id, id));
|
||||
}
|
||||
|
||||
// Scheduled Posts
|
||||
async getScheduledPosts(userId: string, startDate?: Date, endDate?: Date): Promise<ScheduledPost[]> {
|
||||
let query = db
|
||||
.select()
|
||||
.from(scheduledPosts)
|
||||
.innerJoin(posts, eq(scheduledPosts.postId, posts.id))
|
||||
.where(eq(posts.userId, userId));
|
||||
|
||||
if (startDate && endDate) {
|
||||
const results = await query;
|
||||
return results
|
||||
.filter(r => {
|
||||
const scheduledAt = new Date(r.scheduled_posts.scheduledAt);
|
||||
return scheduledAt >= startDate && scheduledAt <= endDate;
|
||||
})
|
||||
.map(r => r.scheduled_posts);
|
||||
}
|
||||
|
||||
const results = await query;
|
||||
return results.map(r => r.scheduled_posts);
|
||||
}
|
||||
|
||||
async getScheduledPost(id: string): Promise<ScheduledPost | undefined> {
|
||||
const [scheduledPost] = await db.select().from(scheduledPosts).where(eq(scheduledPosts.id, id));
|
||||
return scheduledPost || undefined;
|
||||
}
|
||||
|
||||
async createScheduledPost(scheduledPost: InsertScheduledPost): Promise<ScheduledPost> {
|
||||
const [newScheduledPost] = await db.insert(scheduledPosts).values(scheduledPost).returning();
|
||||
return newScheduledPost;
|
||||
}
|
||||
|
||||
async updateScheduledPost(id: string, scheduledPost: Partial<ScheduledPost>): Promise<ScheduledPost> {
|
||||
const [updated] = await db.update(scheduledPosts).set(scheduledPost).where(eq(scheduledPosts.id, id)).returning();
|
||||
return updated;
|
||||
}
|
||||
|
||||
async deleteScheduledPost(id: string): Promise<void> {
|
||||
await db.delete(scheduledPosts).where(eq(scheduledPosts.id, id));
|
||||
}
|
||||
|
||||
async getPendingScheduledPosts(): Promise<ScheduledPost[]> {
|
||||
const now = new Date();
|
||||
const results = await db
|
||||
.select()
|
||||
.from(scheduledPosts)
|
||||
.where(
|
||||
and(
|
||||
isNull(scheduledPosts.publishedAt),
|
||||
lte(scheduledPosts.scheduledAt, now)
|
||||
)
|
||||
);
|
||||
return results;
|
||||
}
|
||||
|
||||
// AI Generations
|
||||
async getAiGenerations(userId: string): Promise<AiGeneration[]> {
|
||||
return await db.select().from(aiGenerations).where(eq(aiGenerations.userId, userId)).orderBy(desc(aiGenerations.createdAt));
|
||||
}
|
||||
|
||||
async createAiGeneration(generation: InsertAiGeneration): Promise<AiGeneration> {
|
||||
const [newGeneration] = await db.insert(aiGenerations).values(generation).returning();
|
||||
return newGeneration;
|
||||
}
|
||||
}
|
||||
|
||||
export const storage = new DatabaseStorage();
|
||||
@@ -0,0 +1,85 @@
|
||||
import express, { type Express } from "express";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { createServer as createViteServer, createLogger } from "vite";
|
||||
import { type Server } from "http";
|
||||
import viteConfig from "../vite.config";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
const viteLogger = createLogger();
|
||||
|
||||
export function log(message: string, source = "express") {
|
||||
const formattedTime = new Date().toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: true,
|
||||
});
|
||||
|
||||
console.log(`${formattedTime} [${source}] ${message}`);
|
||||
}
|
||||
|
||||
export async function setupVite(app: Express, server: Server) {
|
||||
const serverOptions = {
|
||||
middlewareMode: true,
|
||||
hmr: { server },
|
||||
allowedHosts: true as const,
|
||||
};
|
||||
|
||||
const vite = await createViteServer({
|
||||
...viteConfig,
|
||||
configFile: false,
|
||||
customLogger: {
|
||||
...viteLogger,
|
||||
error: (msg, options) => {
|
||||
viteLogger.error(msg, options);
|
||||
process.exit(1);
|
||||
},
|
||||
},
|
||||
server: serverOptions,
|
||||
appType: "custom",
|
||||
});
|
||||
|
||||
app.use(vite.middlewares);
|
||||
app.use("*", async (req, res, next) => {
|
||||
const url = req.originalUrl;
|
||||
|
||||
try {
|
||||
const clientTemplate = path.resolve(
|
||||
import.meta.dirname,
|
||||
"..",
|
||||
"client",
|
||||
"index.html",
|
||||
);
|
||||
|
||||
// always reload the index.html file from disk incase it changes
|
||||
let template = await fs.promises.readFile(clientTemplate, "utf-8");
|
||||
template = template.replace(
|
||||
`src="/src/main.tsx"`,
|
||||
`src="/src/main.tsx?v=${nanoid()}"`,
|
||||
);
|
||||
const page = await vite.transformIndexHtml(url, template);
|
||||
res.status(200).set({ "Content-Type": "text/html" }).end(page);
|
||||
} catch (e) {
|
||||
vite.ssrFixStacktrace(e as Error);
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function serveStatic(app: Express) {
|
||||
const distPath = path.resolve(import.meta.dirname, "public");
|
||||
|
||||
if (!fs.existsSync(distPath)) {
|
||||
throw new Error(
|
||||
`Could not find the build directory: ${distPath}, make sure to build the client first`,
|
||||
);
|
||||
}
|
||||
|
||||
app.use(express.static(distPath));
|
||||
|
||||
// fall through to index.html if the file doesn't exist
|
||||
app.use("*", (_req, res) => {
|
||||
res.sendFile(path.resolve(distPath, "index.html"));
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user