HackmanCMS/web/api/social_upload.php
Bashy ac5c262bfb Add Social Scheduler — global social media post scheduling
- New /social page with left/right layout (form + post list)
- Social topbar link
- Tables: social_posts, social_targets (project-agnostic posts, per-target project refs)
- CRUD: create, edit, duplicate, delete, retry
- Image upload (JPEG/PNG/GIF/WebP, 10MB max) stored in data/social-uploads/
- Per-platform image toggle (Discord multipart, Mastodon /api/v2/media, LinkedIn Assets API)
- bin/dispatch-social.php cron dispatcher (every minute via /etc/cron.d)
- Settings: eligible projects section to enable bot projects as social account sources
- Platforms: Discord (webhook), Mastodon, LinkedIn personal profile

Cron setup:
  * * * * * www-data /usr/bin/php /opt/hackmancms/bin/dispatch-social.php >> /var/log/hackman_social.log 2>&1

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 17:01:03 +03:00

73 lines
2.5 KiB
PHP

<?php
define('UPLOAD_DIR', ROOT . '/data/social-uploads');
if (!is_dir(UPLOAD_DIR)) mkdir(UPLOAD_DIR, 0750, true);
// ── SERVE ─────────────────────────────────────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] === 'GET' && ($_GET['action'] ?? '') === 'serve') {
$filename = basename($_GET['path'] ?? '');
if (!$filename) { http_response_code(400); exit; }
$full = UPLOAD_DIR . '/' . $filename;
$real = realpath($full);
if (!$real || !str_starts_with($real, realpath(UPLOAD_DIR) . '/') || !file_exists($real)) {
http_response_code(404); exit;
}
$mime = mime_content_type($real) ?: 'application/octet-stream';
header('Content-Type: ' . $mime);
header('Content-Length: ' . filesize($real));
header('Cache-Control: private, max-age=86400');
readfile($real);
exit;
}
// ── UPLOAD ────────────────────────────────────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
header('Content-Type: application/json');
echo json_encode(['error' => 'POST required']);
exit;
}
header('Content-Type: application/json');
$file = $_FILES['image'] ?? null;
if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
echo json_encode(['error' => 'Upload failed — error code ' . ($file['error'] ?? 'none')]);
exit;
}
$allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$mime = mime_content_type($file['tmp_name']);
if (!in_array($mime, $allowed_types, true)) {
echo json_encode(['error' => 'Only JPEG, PNG, GIF, and WebP images are allowed']);
exit;
}
if ($file['size'] > 10 * 1024 * 1024) {
echo json_encode(['error' => 'Image must be under 10 MB']);
exit;
}
$ext = match ($mime) {
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp',
default => 'jpg',
};
$filename = time() . '_' . bin2hex(random_bytes(6)) . '.' . $ext;
$dest = UPLOAD_DIR . '/' . $filename;
if (!move_uploaded_file($file['tmp_name'], $dest)) {
echo json_encode(['error' => 'Failed to save image']);
exit;
}
echo json_encode([
'ok' => true,
'path' => $filename,
'url' => '/api/social_upload?action=serve&path=' . rawurlencode($filename),
]);