- 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>
73 lines
2.5 KiB
PHP
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),
|
|
]);
|