← back to Marketing Command Center

modules/quickpost/index.js

90 lines

// Quick Post — easy per-platform "post" buttons across the command center. By
// design this STAGES A DRAFT (gated): a click records a draft post; nothing goes
// live without a human confirm. Live publishing is wired to the channels layer
// but only fires once a platform's token is connected AND confirm:true is passed
// — until then every platform falls back to a staged draft. Draft-only is the
// safe default that honors the standing "social posting is Steve-gated" rule.
const fs = require('fs');
const path = require('path');

const DATA_DIR = path.join(__dirname, '..', '..', 'data');
const STORE = path.join(DATA_DIR, 'quickpost-drafts.json');

const PLATFORMS = [
  { id: 'instagram', label: 'Instagram', icon: '📷' },
  { id: 'tiktok',    label: 'TikTok',    icon: '🎵' },
  { id: 'facebook',  label: 'Facebook',  icon: '📘' },
  { id: 'linkedin',  label: 'LinkedIn',  icon: '💼' },
];

function load() { try { return JSON.parse(fs.readFileSync(STORE, 'utf8')); } catch { return []; } }
function save(list) {
  try { fs.mkdirSync(DATA_DIR, { recursive: true }); fs.writeFileSync(STORE, JSON.stringify(list, null, 2)); }
  catch (e) { /* non-fatal */ }
}
// stable-ish id without Date.now in hot path is fine here (server runtime, not a composition)
function newId() { return 'qp_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7); }

module.exports = {
  id: 'quickpost',
  title: 'Make a Post',
  icon: '✨',
  mount(router) {
    // per-platform connection state (best-effort). Tokens aren't wired yet, so
    // everything reports draft-only; the front-end shows this honestly.
    router.get('/status', (_req, res) => {
      res.json({
        platforms: PLATFORMS.map(p => ({ ...p, connected: false })),
        note: 'Draft-only: posts are staged for approval. Live posting activates per platform once its token is connected.',
        draftCount: load().length,
      });
    });

    router.get('/drafts', (_req, res) => res.json({ drafts: load().slice().reverse() }));

    // Stage a draft. This is the "easy post" action — always safe, always gated.
    router.post('/draft', (req, res) => {
      const b = req.body || {};
      const platform = String(b.platform || '').toLowerCase();
      if (!PLATFORMS.some(p => p.id === platform)) return res.status(400).json({ error: 'unknown platform' });
      const entry = {
        id: newId(),
        platform,
        handle: String(b.handle || '').replace(/^@/, '').slice(0, 100),  // which DW-owned IG account this draft targets
        caption: String(b.caption || '').slice(0, 2200),
        mediaUrl: String(b.mediaUrl || ''),
        source: String(b.source || 'quickpost'),
        status: 'draft',
        created_at: new Date().toISOString(),
      };
      const list = load(); list.push(entry); save(list);
      res.json({ ok: true, staged: true, id: entry.id, platform });
    });

    // Remove a staged draft.
    router.post('/draft/delete', (req, res) => {
      const id = String((req.body || {}).id || '');
      const list = load().filter(d => d.id !== id);
      save(list);
      res.json({ ok: true });
    });

    // Live publish — GATED. Requires an explicit confirm AND a connected platform.
    // No platform is connected yet, so this always falls back to a staged draft.
    router.post('/publish', (req, res) => {
      const b = req.body || {};
      const platform = String(b.platform || '').toLowerCase();
      // (future) if channels.status[platform].connected && b.confirm === true → real post
      const entry = {
        id: newId(), platform, handle: String(b.handle || '').replace(/^@/, '').slice(0, 100),
        caption: String(b.caption || '').slice(0, 2200),
        mediaUrl: String(b.mediaUrl || ''), source: String(b.source || 'quickpost'),
        status: 'draft', created_at: new Date().toISOString(),
      };
      const list = load(); list.push(entry); save(list);
      res.json({ ok: true, staged: true, live: false, id: entry.id,
        reason: 'platform not connected — staged as a draft for approval' });
    });
  },
};