- View cards now show "2026-06-04 14:30 (in 2h)" next to the scheduled time - New "Dispatch now" button runs dispatch-social.php immediately and shows output inline (useful for diagnosing why cron isn't sending posts) - dispatch action added to /api/social Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
206 lines
10 KiB
PHP
206 lines
10 KiB
PHP
<?php
|
|
header('Content-Type: application/json');
|
|
|
|
$method = $_SERVER['REQUEST_METHOD'];
|
|
$input = $method === 'POST' ? (json_decode(file_get_contents('php://input'), true) ?? []) : [];
|
|
|
|
function social_enabled_projects(PDO $db): array {
|
|
$row = $db->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']);
|