hmacUsername = $config->hmacUsername; $this->hmacSecret = $config->hmacSecret; $this->channelIntegrationId = $config->channelIntegrationId; $this->messageTemplateId = $config->messageTemplateId; $this->messageTemplateIdPelanggaranID = $config->messageTemplateIdPelanggaranID; $this->messageTemplateIdPelanggaranEN = $config->messageTemplateIdPelanggaranEN; $this->apiUrl = 'https://api.mekari.com/qontak/chat/v1/broadcasts/whatsapp/direct'; $this->uploadApiUrl = 'https://api.mekari.com/qontak/chat/v1/file_uploader'; } /** * Upload foto ke Mekari untuk digunakan di WhatsApp template * Jika file WebP, akan dikonversi ke JPEG dulu */ public function uploadImage($filePath) { $tempJpegPath = null; try { if (!file_exists($filePath)) { throw new \Exception("File tidak ditemukan: {$filePath}"); } $fileExtension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); $uploadFilePath = $filePath; $uploadFileName = basename($filePath); // Konversi WebP ke JPEG if ($fileExtension === 'webp') { log_message('debug', "Converting WebP to JPEG: {$filePath}"); $tempJpegPath = sys_get_temp_dir() . '/' . uniqid('wa_upload_') . '.jpg'; $image = imagecreatefromwebp($filePath); if (!$image) { throw new \Exception("Gagal membaca file WebP"); } $converted = imagejpeg($image, $tempJpegPath, 90); imagedestroy($image); if (!$converted) { throw new \Exception("Gagal konversi WebP ke JPEG"); } $uploadFilePath = $tempJpegPath; $uploadFileName = pathinfo($filePath, PATHINFO_FILENAME) . '.jpg'; log_message('debug', "WebP converted to JPEG: {$tempJpegPath}"); } $urlParts = parse_url($this->uploadApiUrl); $path = $urlParts['path']; $dateString = gmdate('D, d M Y H:i:s') . ' GMT'; $hmacAuth = $this->generateHmacAuth('POST', $path, $dateString); $cFile = new \CURLFile($uploadFilePath, 'image/jpeg', $uploadFileName); $ch = curl_init($this->uploadApiUrl); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: ' . $hmacAuth, 'Date: ' . $dateString ], CURLOPT_POSTFIELDS => ['file' => $cFile], CURLOPT_TIMEOUT => 60, CURLOPT_SSL_VERIFYPEER => true ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); // Cleanup temporary file if ($tempJpegPath && file_exists($tempJpegPath)) { unlink($tempJpegPath); log_message('debug', "Temporary JPEG file deleted: {$tempJpegPath}"); } if ($error) { log_message('error', "Mekari Upload cURL Error: {$error}"); return [ 'success' => false, 'message' => 'Gagal upload gambar ke server' ]; } log_message('debug', "Mekari Upload Response [{$httpCode}]: {$response}"); $result = json_decode($response, true); if ($httpCode >= 200 && $httpCode < 300 && isset($result['data']['url'])) { return [ 'success' => true, 'message' => 'Gambar berhasil diupload', 'url' => $result['data']['url'], 'filename' => $result['data']['filename'] ]; } $errorMessage = 'Gagal upload gambar'; if (isset($result['error']['message'])) { $errorMessage = $result['error']['message']; } elseif (isset($result['message'])) { $errorMessage = $result['message']; } return [ 'success' => false, 'message' => $errorMessage ]; } catch (\Exception $e) { if ($tempJpegPath && file_exists($tempJpegPath)) { unlink($tempJpegPath); } log_message('error', "Mekari Upload Exception: " . $e->getMessage()); return [ 'success' => false, 'message' => 'Terjadi kesalahan saat upload gambar: ' . $e->getMessage() ]; } } public function sendOtp($phoneNumber, $otpCode, $userName = '', $subject = 'VERIFIKASI AKUN') { try { $phoneNumber = $this->formatPhoneNumber($phoneNumber); $urlParts = parse_url($this->apiUrl); $path = $urlParts['path']; $dateString = gmdate('D, d M Y H:i:s') . ' GMT'; $hmacAuth = $this->generateHmacAuth('POST', $path, $dateString); $payload = [ 'to_number' => $phoneNumber, 'to_name' => $userName ?: 'User', 'message_template_id' => $this->messageTemplateId, 'channel_integration_id' => $this->channelIntegrationId, 'language' => [ 'code' => 'id' ], 'parameters' => [ 'body' => [ [ 'key' => '1', 'value' => 'code', 'value_text' => $otpCode ], ], "buttons" => [ [ "index" => "0", "type" => "url", "value" => "$otpCode" ] ] ] ]; log_message('debug', "Mekari WA OTP Request - Date: {$dateString}"); log_message('debug', "Mekari WA OTP Payload: " . json_encode($payload)); return $this->sendRequest($payload, $hmacAuth, $dateString); } catch (\Exception $e) { log_message('error', "Mekari WA OTP Exception: " . $e->getMessage()); return [ 'success' => false, 'message' => 'Terjadi kesalahan saat mengirim OTP' ]; } } /** * Kirim notifikasi pelanggaran via WhatsApp dengan header image * * @param string $phoneNumber Nomor WhatsApp tujuan * @param array $data Data pelanggaran dengan keys: * - nama, jenis_pelanggaran, tanggal_formatted, waktu_formatted * - lokasi_kejadian, dasar_hukum, pasal, hukuman, urutan_pelanggaran * - pelanggaran_id, is_luar_negeri, image_url (optional), image_filename (optional) */ public function sendNotifikasiPelanggaran($phoneNumber, $data) { try { $phoneNumber = $this->formatPhoneNumber($phoneNumber); $urlParts = parse_url($this->apiUrl); $path = $urlParts['path']; $dateString = gmdate('D, d M Y H:i:s') . ' GMT'; $hmacAuth = $this->generateHmacAuth('POST', $path, $dateString); $isLuarNegeri = $data['is_luar_negeri'] ?? false; // $templateId = $isLuarNegeri ? // $this->messageTemplateIdPelanggaranEN : // $this->messageTemplateIdPelanggaranID; $templateId = $this->messageTemplateIdPelanggaranID; // Validasi required fields $requiredFields = [ 'nama', 'jenis_pelanggaran', 'tanggal_formatted', 'waktu_formatted', 'lokasi_kejadian', 'dasar_hukum', 'pasal', 'hukuman', 'urutan_pelanggaran', 'pelanggaran_id' ]; foreach ($requiredFields as $field) { if (empty($data[$field])) { throw new \Exception("Field '{$field}' is required"); } } // Kalau template inggris sudah ada bisa di uncomment di bawah ini // Build body parameters // if ($isLuarNegeri) { // $bodyParams = [ // ['key' => '1', 'value' => 'p1', 'value_text' => $data['urutan_pelanggaran']], // ['key' => '2', 'value' => 'p2', 'value_text' => $data['title'] ?? 'Mr./Ms.'], // ['key' => '3', 'value' => 'p3', 'value_text' => $data['nama']], // ['key' => '4', 'value' => 'p4', 'value_text' => $data['jenis_pelanggaran']], // ['key' => '5', 'value' => 'p5', 'value_text' => $data['tanggal_formatted']], // ['key' => '6', 'value' => 'p6', 'value_text' => $data['waktu_formatted']], // ['key' => '7', 'value' => 'p7', 'value_text' => $data['lokasi_kejadian']], // ['key' => '8', 'value' => 'p8', 'value_text' => $data['dasar_hukum']], // ['key' => '9', 'value' => 'p9', 'value_text' => $data['pasal']], // ['key' => '10', 'value' => 'p10', 'value_text' => $data['hukuman']] // ]; // } else { // $bodyParams = [ // ['key' => '1', 'value' => 'p1', 'value_text' => $data['urutan_pelanggaran']], // ['key' => '2', 'value' => 'p2', 'value_text' => $data['nama']], // ['key' => '3', 'value' => 'p3', 'value_text' => $data['jenis_pelanggaran']], // ['key' => '4', 'value' => 'p4', 'value_text' => $data['tanggal_formatted']], // ['key' => '5', 'value' => 'p5', 'value_text' => $data['waktu_formatted']], // ['key' => '6', 'value' => 'p6', 'value_text' => $data['lokasi_kejadian']], // ['key' => '7', 'value' => 'p7', 'value_text' => $data['dasar_hukum']], // ['key' => '8', 'value' => 'p8', 'value_text' => $data['pasal']], // ['key' => '9', 'value' => 'p9', 'value_text' => $data['hukuman']] // ]; // } // Selalu pakai format Indonesia dulu (9 parameter) $bodyParams = [ ['key' => '1', 'value' => 'p1', 'value_text' => $data['urutan_pelanggaran']], ['key' => '2', 'value' => 'p2', 'value_text' => $data['nama']], ['key' => '3', 'value' => 'p3', 'value_text' => $data['jenis_pelanggaran']], ['key' => '4', 'value' => 'p4', 'value_text' => $data['tanggal_formatted']], ['key' => '5', 'value' => 'p5', 'value_text' => $data['waktu_formatted']], ['key' => '6', 'value' => 'p6', 'value_text' => $data['lokasi_kejadian']], ['key' => '7', 'value' => 'p7', 'value_text' => $data['dasar_hukum']], ['key' => '8', 'value' => 'p8', 'value_text' => $data['pasal']], ['key' => '9', 'value' => 'p9', 'value_text' => $data['hukuman']] ]; // Build payload parameters $parameters = [ 'body' => $bodyParams, 'buttons' => [ [ 'index' => '0', 'type' => 'url', 'value' => $data['pelanggaran_id'] ] ] ]; // Tambahkan header if (!empty($data['image_url']) && !empty($data['image_filename'])) { $parameters['header'] = [ 'format' => 'IMAGE', 'params' => [ [ 'key' => 'url', 'value' => $data['image_url'] ], [ 'key' => 'filename', 'value' => $data['image_filename'] ] ] ]; } else { $parameters['header'] = [ 'format' => 'IMAGE', 'params' => [ [ 'key' => 'url', 'value' => "https://cdn.qontak.com/uploads/direct/images/1ed10e3c-461f-477f-a74a-ee6553f23f4d/photo_2025-11-10_15-32-37.jpg" ], [ 'key' => 'filename', 'value' => "photo_2025-11-10_15-32-37.jpg" ] ] ]; } $payload = [ 'to_number' => $phoneNumber, 'to_name' => $data['nama'], 'message_template_id' => $templateId, 'channel_integration_id' => $this->channelIntegrationId, 'language' => [ 'code' => $isLuarNegeri ? 'en' : 'id' ], 'parameters' => $parameters ]; log_message('debug', "Mekari WA Pelanggaran Request - Date: {$dateString}"); log_message('debug', "Mekari WA Pelanggaran Payload: " . json_encode($payload)); $result = $this->sendRequest($payload, $hmacAuth, $dateString); if ($result['success']) { log_message('info', "WhatsApp notification sent to {$phoneNumber} for pelanggaran {$data['pelanggaran_id']}"); } return $result; } catch (\Exception $e) { log_message('error', "Mekari WA Pelanggaran Exception: " . $e->getMessage()); return [ 'success' => false, 'message' => 'Terjadi kesalahan saat mengirim notifikasi pelanggaran: ' . $e->getMessage() ]; } } private function sendRequest($payload, $hmacAuth, $dateString) { $ch = curl_init($this->apiUrl); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: ' . $hmacAuth, 'Date: ' . $dateString, 'Content-Type: application/json' ], CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_TIMEOUT => 30, CURLOPT_SSL_VERIFYPEER => true ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); if ($error) { log_message('error', "Mekari WA cURL Error: {$error}"); return [ 'success' => false, 'message' => 'Gagal terhubung ke server WhatsApp' ]; } log_message('debug', "Mekari WA Response [{$httpCode}]: {$response}"); $result = json_decode($response, true); if ($httpCode >= 200 && $httpCode < 300) { return [ 'success' => true, 'message' => 'Pesan berhasil dikirim', 'data' => $result ]; } $errorMessage = 'Gagal mengirim pesan'; if (isset($result['error']['message'])) { $errorMessage = $result['error']['message']; } elseif (isset($result['message'])) { $errorMessage = $result['message']; } log_message('error', "Mekari WA Error [{$httpCode}]: " . $response); return [ 'success' => false, 'message' => $errorMessage, 'error_code' => $result['error']['code'] ?? $httpCode, // 'raw_response' => $result // Untuk debugging ]; } private function generateHmacAuth($method, $path, $dateString) { $requestLine = strtoupper($method) . ' ' . $path . ' HTTP/1.1'; $stringToSign = 'date: ' . $dateString . "\n" . $requestLine; $signature = base64_encode(hash_hmac('sha256', $stringToSign, $this->hmacSecret, true)); $hmacHeader = sprintf( 'hmac username="%s", algorithm="hmac-sha256", headers="date request-line", signature="%s"', $this->hmacUsername, $signature ); return $hmacHeader; } private function formatPhoneNumber($phone) { $phone = preg_replace('/[^0-9]/', '', $phone); if (substr($phone, 0, 2) === '62') { return $phone; } if (substr($phone, 0, 1) === '0') { return '62' . substr($phone, 1); } return '62' . $phone; } public function verifyWebhookSignature($authHeader, $dateHeader, $method, $path) { try { preg_match('/signature="([^"]+)"/', $authHeader, $matches); $receivedSignature = $matches[1] ?? ''; if (empty($receivedSignature)) { return false; } $requestLine = strtoupper($method) . ' ' . $path . ' HTTP/1.1'; $stringToSign = 'date: ' . $dateHeader . "\n" . $requestLine; $expectedSignature = base64_encode(hash_hmac('sha256', $stringToSign, $this->hmacSecret, true)); return hash_equals($expectedSignature, $receivedSignature); } catch (\Exception $e) { log_message('error', "HMAC Verification Error: " . $e->getMessage()); return false; } } }