prepare("SELECT value FROM settings WHERE key = 'social_enabled_projects'"); $row->execute(); $val = $row->fetchColumn(); return $val ? (json_decode($val, true) ?? []) : []; } function social_project_targets(PDO $db): array { $enabled = social_enabled_projects($db); if (!$enabled) return []; $placeholders = implode(',', array_fill(0, count($enabled), '?')); $stmt = $db->prepare("SELECT id, name, path FROM projects WHERE id IN ($placeholders) AND is_active = 1"); $stmt->execute($enabled); $projects = $stmt->fetchAll(); $result = []; foreach ($projects as $proj) { $config_path = realpath($proj['path']) . '/data/config.json'; if (!file_exists($config_path)) continue; $config = json_decode(file_get_contents($config_path), true); if (!is_array($config)) continue; $targets = ['discord' => [], 'mastodon' => [], 'linkedin' => []]; foreach ($config['discord_channels'] ?? [] as $key => $ch) { $targets['discord'][] = ['key' => $key, 'label' => $ch['label'] ?? $key]; } foreach ($config['mastodon'] ?? [] as $key => $acc) { $targets['mastodon'][] = ['key' => $key, 'label' => $acc['label'] ?? $key]; } foreach ($config['linkedin']['pages'] ?? [] as $key => $page) { if (($page['type'] ?? 'organization') === 'personal') { $targets['linkedin'][] = ['key' => $key, 'label' => $page['label'] ?? $key]; } } if ($targets['discord'] || $targets['mastodon'] || $targets['linkedin']) { $result[] = ['project_id' => (int)$proj['id'], 'project_name' => $proj['name'], 'targets' => $targets]; } } return $result; } // ── GET ─────────────────────────────────────────────────────────────────────── if ($method === 'GET') { $action = $_GET['action'] ?? 'list'; if ($action === 'targets') { echo json_encode(['groups' => social_project_targets($db)]); exit; } if ($action === 'settings') { $enabled = social_enabled_projects($db); $stmt = $db->query("SELECT id, name, type FROM projects WHERE is_active = 1 AND type = 'discord-bot' ORDER BY name"); $projects = $stmt->fetchAll(); echo json_encode(['enabled' => $enabled, 'projects' => $projects]); exit; } // list posts $status_filter = $_GET['status'] ?? 'pending'; if ($status_filter === 'all') { $stmt = $db->prepare("SELECT * FROM social_posts ORDER BY scheduled_at DESC LIMIT 200"); $stmt->execute(); } else { $stmt = $db->prepare("SELECT * FROM social_posts WHERE status IN ('pending','partial') ORDER BY scheduled_at ASC LIMIT 200"); $stmt->execute(); } $posts = $stmt->fetchAll(); foreach ($posts as &$post) { $tgt = $db->prepare("SELECT t.*, p.name AS project_name FROM social_targets t JOIN projects p ON p.id = t.project_id WHERE t.post_id = ? ORDER BY t.id"); $tgt->execute([$post['id']]); $post['targets'] = $tgt->fetchAll(); $post['image_url'] = $post['image_path'] ? '/api/social_upload?action=serve&path=' . rawurlencode(basename($post['image_path'])) : null; } echo json_encode(['posts' => $posts]); exit; } if ($method !== 'POST') { http_response_code(405); echo json_encode(['error' => 'Method not allowed']); exit; } $action = $input['action'] ?? ''; // ── SAVE SETTINGS ───────────────────────────────────────────────────────────── if ($action === 'save_settings') { $ids = array_map('intval', $input['project_ids'] ?? []); $db->prepare("INSERT OR REPLACE INTO settings (key, value) VALUES ('social_enabled_projects', ?)") ->execute([json_encode($ids)]); Audit::log($db, 'social_settings_save', null, implode(',', $ids)); echo json_encode(['ok' => true]); exit; } // ── CREATE ──────────────────────────────────────────────────────────────────── if ($action === 'create') { $content = trim($input['content'] ?? ''); $image_path = trim($input['image_path'] ?? ''); $scheduled_at = str_replace('T', ' ', trim($input['scheduled_at'] ?? '')); $targets = $input['targets'] ?? []; if (!$content) { echo json_encode(['error' => 'Content required']); exit; } if (!$scheduled_at) { echo json_encode(['error' => 'Scheduled time required']); exit; } if (!$targets) { echo json_encode(['error' => 'Select at least one platform target']); exit; } $db->prepare("INSERT INTO social_posts (content, image_path, scheduled_at) VALUES (?, ?, ?)") ->execute([$content, $image_path ?: null, $scheduled_at]); $post_id = (int)$db->lastInsertId(); $stmt = $db->prepare("INSERT INTO social_targets (post_id, project_id, platform, target_key, include_image) VALUES (?, ?, ?, ?, ?)"); foreach ($targets as $t) { $stmt->execute([$post_id, (int)$t['project_id'], $t['platform'], $t['target_key'], (int)($t['include_image'] ?? 0)]); } Audit::log($db, 'social_post_create', null, "post_id=$post_id"); echo json_encode(['ok' => true, 'id' => $post_id]); exit; } // ── UPDATE ──────────────────────────────────────────────────────────────────── if ($action === 'update') { $id = (int)($input['id'] ?? 0); $content = trim($input['content'] ?? ''); $image_path = trim($input['image_path'] ?? ''); $scheduled_at = str_replace('T', ' ', trim($input['scheduled_at'] ?? '')); $targets = $input['targets'] ?? []; if (!$id) { echo json_encode(['error' => 'id required']); exit; } if (!$content) { echo json_encode(['error' => 'Content required']); exit; } if (!$scheduled_at) { echo json_encode(['error' => 'Scheduled time required']); exit; } if (!$targets) { echo json_encode(['error' => 'Select at least one platform target']); exit; } $db->prepare("UPDATE social_posts SET content=?, image_path=?, scheduled_at=?, status='pending' WHERE id=?") ->execute([$content, $image_path ?: null, $scheduled_at, $id]); $db->prepare("DELETE FROM social_targets WHERE post_id=?")->execute([$id]); $stmt = $db->prepare("INSERT INTO social_targets (post_id, project_id, platform, target_key, include_image) VALUES (?, ?, ?, ?, ?)"); foreach ($targets as $t) { $stmt->execute([$id, (int)$t['project_id'], $t['platform'], $t['target_key'], (int)($t['include_image'] ?? 0)]); } Audit::log($db, 'social_post_update', null, "post_id=$id"); echo json_encode(['ok' => true]); exit; } // ── DUPLICATE ───────────────────────────────────────────────────────────────── if ($action === 'duplicate') { $id = (int)($input['id'] ?? 0); if (!$id) { echo json_encode(['error' => 'id required']); exit; } $post = $db->prepare("SELECT * FROM social_posts WHERE id=?")->execute([$id]) ? null : null; $stmt = $db->prepare("SELECT * FROM social_posts WHERE id=?"); $stmt->execute([$id]); $post = $stmt->fetch(); if (!$post) { echo json_encode(['error' => 'Post not found']); exit; } $tgt = $db->prepare("SELECT * FROM social_targets WHERE post_id=?"); $tgt->execute([$id]); $targets = $tgt->fetchAll(); echo json_encode([ 'ok' => true, 'post' => ['content' => $post['content'], 'image_path' => $post['image_path'], 'image_url' => $post['image_path'] ? '/api/social_upload?action=serve&path=' . rawurlencode(basename($post['image_path'])) : null], 'targets' => $targets, ]); exit; } // ── DELETE ──────────────────────────────────────────────────────────────────── if ($action === 'delete') { $id = (int)($input['id'] ?? 0); if (!$id) { echo json_encode(['error' => 'id required']); exit; } $db->prepare("DELETE FROM social_posts WHERE id=?")->execute([$id]); Audit::log($db, 'social_post_delete', null, "post_id=$id"); echo json_encode(['ok' => true]); exit; } // ── RETRY ───────────────────────────────────────────────────────────────────── if ($action === 'retry') { $id = (int)($input['id'] ?? 0); if (!$id) { echo json_encode(['error' => 'id required']); exit; } $db->prepare("UPDATE social_targets SET status='pending', error=NULL WHERE post_id=? AND status='failed'")->execute([$id]); $db->prepare("UPDATE social_posts SET status='pending' WHERE id=?")->execute([$id]); echo json_encode(['ok' => true]); exit; } // ── DISPATCH NOW ────────────────────────────────────────────────────────────── if ($action === 'dispatch') { $script = ROOT . '/bin/dispatch-social.php'; $output = []; $code = 0; exec('/usr/bin/php ' . escapeshellarg($script) . ' 2>&1', $output, $code); echo json_encode(['ok' => true, 'output' => implode("\n", $output), 'exit' => $code]); exit; } http_response_code(400); echo json_encode(['error' => 'Unknown action']);