gets Graph API URL and downloads * - storeContent($bytes, $destPath) * - createThumbnail($srcPath, $thumbPath) */ class MediaService { private $token; private $apiUrl; private $uploadsDir; public function __construct() { $this->token = getConfigFromDB('whatsapp_token', ''); $this->apiUrl = rtrim(getConfigFromDB('whatsapp_api_url', 'https://graph.facebook.com/v22.0/'), '/'); $this->uploadsDir = __DIR__ . '/../uploads/media'; if (!is_dir($this->uploadsDir)) mkdir($this->uploadsDir, 0755, true); } public function fetchAndStoreFromGraph($mediaId, $subdir = '') { // Obtener metadata (url) $endpoint = "{$this->apiUrl}/{$mediaId}"; $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $endpoint, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $this->token], CURLOPT_TIMEOUT => 15 ]); $resp = curl_exec($ch); $http = curl_getinfo($ch, CURLINFO_HTTP_CODE); $err = curl_error($ch); curl_close($ch); if ($err || $http >= 400) { throw new Exception('Graph API fetch failed: ' . $err); } $decoded = json_decode($resp, true); $mediaUrl = $decoded['url'] ?? ($decoded['data'][0]['url'] ?? null); if (!$mediaUrl) throw new Exception('No media url from Graph'); // Descargar contenido (primer intento) $content = $this->downloadUrl($mediaUrl); if ($content === false) { // Reintentar con header Referer (algunos endpoints requieren referer) $content = $this->downloadUrl($mediaUrl, ['Referer: https://graph.facebook.com']); } if ($content === false) throw new Exception('Failed to download media content from ' . $mediaUrl); // Detectar extension desde mime o url $mime = $decoded['mime_type'] ?? null; $ext = $this->extensionFromMime($mime) ?: pathinfo(parse_url($mediaUrl, PHP_URL_PATH), PATHINFO_EXTENSION) ?: 'bin'; $dir = $this->uploadsDir . ($subdir ? '/' . trim($subdir, '/') : ''); if (!is_dir($dir)) mkdir($dir, 0755, true); $filename = uniqid('m_') . '.' . $ext; $fullPath = $dir . '/' . $filename; file_put_contents($fullPath, $content); $result = ['local_file' => 'uploads/media' . ($subdir ? '/' . trim($subdir, '/') : '') . '/' . $filename, 'mime_type' => $mime, 'size' => filesize($fullPath)]; // Si es imagen, generar miniatura if ($this->isImageMime($mime) || preg_match('#^jpe?g|png|gif$#i', $ext)) { $thumbPath = $dir . '/thumb_' . $filename . '.jpg'; $thumbRel = dirname($result['local_file']) . '/thumb_' . $filename . '.jpg'; if ($this->createThumbnail($fullPath, $thumbPath)) { $result['local_thumb'] = $thumbRel; } } return $result; } // Nuevo: descargar y almacenar desde una URL directa public function fetchAndStoreFromUrl($url, $subdir = '') { $content = $this->downloadUrl($url); if ($content === false) { $content = $this->downloadUrl($url, ['Referer: https://graph.facebook.com']); } if ($content === false) throw new Exception('Failed to download media content from url ' . $url); // intentar inferir mime $mime = null; $ext = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION) ?: 'bin'; $dir = $this->uploadsDir . ($subdir ? '/' . trim($subdir, '/') : ''); if (!is_dir($dir)) mkdir($dir, 0755, true); $filename = uniqid('m_') . '.' . $ext; $fullPath = $dir . '/' . $filename; file_put_contents($fullPath, $content); $result = ['local_file' => 'uploads/media' . ($subdir ? '/' . trim($subdir, '/') : '') . '/' . $filename, 'mime_type' => $mime, 'size' => filesize($fullPath)]; // Si parece imagen, intentar crear thumb if (preg_match('#jpe?g|png|gif#i', $ext)) { $thumbPath = $dir . '/thumb_' . $filename . '.jpg'; $thumbRel = dirname($result['local_file']) . '/thumb_' . $filename . '.jpg'; if ($this->createThumbnail($fullPath, $thumbPath)) { $result['local_thumb'] = $thumbRel; } } return $result; } public function downloadUrl($url, $extraHeaders = []) { // Incluir Authorization Bearer si está configurado (necesario para algunos endpoints) $authHeader = []; if (!empty($this->token)) { $authHeader[] = 'Authorization: Bearer ' . $this->token; } $headers = array_merge(['User-Agent: WhatsAppMediaFetcher/1.0', 'Accept: */*'], $authHeader, $extraHeaders); $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_TIMEOUT => 120, CURLOPT_HTTPHEADER => $headers, ]); $content = curl_exec($ch); $err = curl_error($ch); $http = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($err || $http >= 400) return false; return $content; } public function extensionFromMime($mime) { if (!$mime) return null; // Remover parámetros del mime type (ej: "audio/ogg; codecs=opus" -> "audio/ogg") $baseMime = strtolower(trim(explode(';', $mime)[0])); $map = [ 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp', 'image/svg+xml' => 'svg', 'audio/mpeg' => 'mp3', 'audio/ogg' => 'ogg', 'audio/opus' => 'ogg', 'audio/aac' => 'aac', 'audio/amr' => 'amr', 'audio/mp4' => 'm4a', 'video/mp4' => 'mp4', 'video/3gpp' => '3gp', 'video/quicktime' => 'mov', 'video/webm' => 'webm', 'text/csv' => 'csv', 'text/plain' => 'txt', 'application/pdf' => 'pdf', 'application/msword' => 'doc', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx', 'application/vnd.ms-excel' => 'xls', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx', 'application/vnd.ms-powerpoint' => 'ppt', 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx', 'application/zip' => 'zip', 'application/x-rar-compressed' => 'rar', 'application/octet-stream' => 'bin', ]; return $map[$baseMime] ?? null; } public function isImageMime($mime) { return strpos($mime, 'image/') === 0; } public function createThumbnail($src, $dest, $maxW = 400, $maxH = 400) { if (!function_exists('getimagesize')) return false; $info = @getimagesize($src); if (!$info) return false; list($w, $h) = $info; $mime = $info['mime']; switch ($mime) { case 'image/jpeg': $srcImg = imagecreatefromjpeg($src); break; case 'image/png': $srcImg = imagecreatefrompng($src); break; case 'image/gif': $srcImg = imagecreatefromgif($src); break; default: return false; } $ratio = min($maxW / $w, $maxH / $h, 1); $nw = (int)($w * $ratio); $nh = (int)($h * $ratio); $thumb = imagecreatetruecolor($nw, $nh); imagecopyresampled($thumb, $srcImg, 0,0,0,0, $nw, $nh, $w, $h); imagejpeg($thumb, $dest, 85); imagedestroy($thumb); imagedestroy($srcImg); return true; } }