import fs from "fs"; import path from "path"; import { getSetting } from "./settings"; const PIPELINE_ROOT = process.env.PIPELINE_ROOT || path.join(process.cwd(), "pipeline"); async function getPostizConfig() { const url = (await getSetting("POSTIZ_URL")) || "http://localhost:5000"; const apiKey = await getSetting("POSTIZ_API_KEY"); return { url, apiKey }; } async function postizFetch(endpoint: string, options: RequestInit = {}) { const { url, apiKey } = await getPostizConfig(); const res = await fetch(`${url}/public/v1${endpoint}`, { ...options, headers: { Authorization: apiKey, ...options.headers, }, }); if (!res.ok) { const text = await res.text(); throw new Error(`Postiz API error ${res.status}: ${text}`); } return res.json(); } /** * Resolve an asset's relative filePath to an absolute path. */ function resolveAssetPath(filePath: string): string { if (path.isAbsolute(filePath)) return filePath; return path.join(PIPELINE_ROOT, filePath); } export async function uploadToPostiz(filePath: string) { const absolutePath = resolveAssetPath(filePath); const fileBuffer = fs.readFileSync(absolutePath); const fileName = path.basename(absolutePath); const formData = new FormData(); formData.append("file", new Blob([fileBuffer]), fileName); const { url, apiKey } = await getPostizConfig(); const res = await fetch(`${url}/public/v1/upload`, { method: "POST", headers: { Authorization: apiKey }, body: formData, }); if (!res.ok) { const text = await res.text(); throw new Error(`Postiz upload error ${res.status}: ${text}`); } const data = await res.json(); if (!data?.id || !data?.path) { throw new Error(`Postiz upload returned unexpected shape: ${JSON.stringify(data)}`); } return { mediaId: data.id, publicUrl: data.path }; } // Map our internal platform names to Postiz provider identifiers const PLATFORM_ALIASES: Record = { instagram: ["instagram", "instagram-standalone", "ig"], tiktok: ["tiktok", "tt"], nextdoor: ["nextdoor"], }; export async function pushToPostiz( asset: { filePath: string; platform?: string | null; metadata?: string | null; }, scheduledAt: string ) { const { mediaId, publicUrl } = await uploadToPostiz(asset.filePath); const integrations = await getPostizIntegrations(); const platform = (asset.platform || "").toLowerCase(); const aliases = PLATFORM_ALIASES[platform] || [platform]; const integration = integrations.find( (i: { identifier?: string; providerIdentifier?: string }) => { const id = (i.identifier || i.providerIdentifier || "").toLowerCase(); return aliases.includes(id); } ); if (!integration) { const available = integrations .map((i: { identifier?: string }) => i.identifier) .join(", "); throw new Error( `No Postiz channel for "${platform}". Available: ${available || "none"}` ); } const metadata = JSON.parse(asset.metadata || "{}"); // Postiz v1 API post structure const platformSettings: Record = { __type: platform }; if (platform === "instagram") { platformSettings.post_type = "post"; } else if (platform === "tiktok") { platformSettings.privacy_level = "PUBLIC_TO_EVERYONE"; platformSettings.comment = true; platformSettings.duet = false; platformSettings.stitch = false; platformSettings.content_posting_method = "DIRECT_POST"; platformSettings.autoAddMusic = "no"; platformSettings.brand_content_toggle = false; platformSettings.brand_organic_toggle = false; } const post = await postizFetch("/posts", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ type: "schedule", date: scheduledAt, shortLink: false, tags: [], posts: [ { integration: { id: integration.id }, value: [ { content: metadata.caption || "", image: [{ id: mediaId, path: publicUrl }], }, ], settings: platformSettings, }, ], }), }); return post; } export async function getPostizIntegrations() { return postizFetch("/integrations"); }