string, 'call_id' => int] * On success: ['dialer_call_id' => int, 'linkedId' => string] */ public function callbackWebhook(array $return_data): bool { \Log::info('AutodialerService@callbackWebhook: start', ['data' => $return_data]); $webhookUrl = CompanySettings::get('api_webhook_url'); if (!$webhookUrl) { \Log::warning('AutodialerService@callbackWebhook: api_webhook_url is not configured, skipping.'); return false; } if (isset($return_data['error'])) { // ── Error path ──────────────────────────────────────────────── \Log::info('AutodialerService@callbackWebhook: error payload', ['error' => $return_data['error']]); $body = json_encode([ 'error' => $return_data['error'], 'call_id' => $return_data['call_id'] ?? null, ]); } else { // ── Success path: enrich with CDR data ──────────────────────── $dialer_call_id = $return_data['dialer_call_id'] ?? null; $linkedId = $return_data['linkedId'] ?? null; \Log::info('AutodialerService@callbackWebhook: success path', [ 'dialer_call_id' => $dialer_call_id, 'linkedId' => $linkedId, ]); $dialer_call = DialerCalls::find($dialer_call_id); if (!$dialer_call) { \Log::error("AutodialerService@callbackWebhook: DialerCalls#{$dialer_call_id} not found."); return false; } // Fetch CDR record (allow a brief settle time before reading) $call = \DB::table('asteriskcdrdb.cdr') ->where('linkedid', $linkedId) ->orderBy('recordingfile', 'DESC') ->first(); \Log::info('AutodialerService@callbackWebhook: CDR fetched', ['cdr' => $call ? (array)$call : null]); $callLink = null; if ($call && $call->recordingfile) { $calldate = date('Y/m/d', strtotime($call->calldate)); $callLink = url("public/monitor/{$calldate}/{$call->recordingfile}"); $dialer_call->call_record = $call->recordingfile; } if ($call) { $dialer_call->order_date = $call->calldate; $dialer_call->call_status = $call->disposition; $dialer_call->duration = $call->duration; $dialer_call->billsec = $call->billsec; $dialer_call->linkedId = $linkedId; } $dialer_call->save(); \Log::info('AutodialerService@callbackWebhook: DialerCalls record updated'); $payload = $dialer_call->toArray(); unset($payload['voices']); if ($callLink) { $payload['call_record_url'] = $callLink; } $body = json_encode($payload); } \Log::debug('AutodialerService@callbackWebhook: posting to webhook', [ 'url' => $webhookUrl, 'body' => $body, ]); try { $client = new Client(['timeout' => 10, 'verify' => false]); $headers = ['Content-Type' => 'application/json']; $request = new Request('POST', $webhookUrl, $headers, $body); $res = $client->sendAsync($request)->wait(); $responseBody = (string) $res->getBody(); \Log::info('AutodialerService@callbackWebhook: webhook responded', ['response' => $responseBody]); return true; } catch (\Throwable $e) { \Log::error('AutodialerService@callbackWebhook: webhook request failed: ' . $e->getMessage()); return false; } } /** * Download voice files from remote URLs and store them locally. * * @param array $files [['key' => 1, 'value' => 'https://...'], ...] * @param int $id DialerCalls record ID (used for unique file names) * @return array|false Absolute paths of downloaded files, or false on failure */ public function downloadFiles(array $files, int $id) { try { usort($files, fn($a, $b) => $a['key'] <=> $b['key']); $downloadedFiles = []; foreach ($files as $index => $file) { $url = $file['value']; $fileName = "dialer-{$id}-{$index}.mp3"; \Log::info("AutodialerService@downloadFiles: downloading [{$index}] {$url}"); $response = Http::timeout(30)->retry(3, 500)->get($url); if ($response->successful()) { Storage::put("public/temp/{$fileName}", $response->body()); $path = storage_path("app/public/temp/{$fileName}"); $downloadedFiles[] = $path; \Log::info("AutodialerService@downloadFiles: saved -> {$path}"); } else { \Log::error("AutodialerService@downloadFiles: HTTP {$response->status()} for {$url}"); return false; } } \Log::info('AutodialerService@downloadFiles: all files downloaded', ['count' => count($downloadedFiles)]); return $downloadedFiles; } catch (\Throwable $th) { \Log::error('AutodialerService@downloadFiles: exception: ' . $th->getMessage()); return false; } } /** * Merge multiple MP3 files into a single output file using FFmpeg. * * @param array $audioFiles Absolute paths of source files (in order) * @param string $outputFile Absolute path for the merged output * @param int $id DialerCalls ID (used for the temp file-list name) * @return bool */ public function mergeAudiosUsingFFMpeg(array $audioFiles, string $outputFile, int $id): bool { try { $fileList = storage_path("app/public/temp/file_list_{$id}.txt"); $fileContent = ''; foreach ($audioFiles as $file) { $fileContent .= "file '{$file}'\n"; } file_put_contents($fileList, $fileContent); \Log::info("AutodialerService@mergeAudiosUsingFFMpeg: merging " . count($audioFiles) . " file(s) -> {$outputFile}"); $command = [ 'ffmpeg', '-f', 'concat', '-safe', '0', '-i', $fileList, '-vol', '2048', '-ar', '8000', '-ac', '1', '-f', 'mulaw', $outputFile, ]; $process = new Process($command); $process->run(); if (!$process->isSuccessful()) { \Log::error('AutodialerService@mergeAudiosUsingFFMpeg: ffmpeg error: ' . $process->getErrorOutput()); return false; } @unlink($fileList); \Log::info("AutodialerService@mergeAudiosUsingFFMpeg: merge complete -> {$outputFile}"); return true; } catch (\Throwable $th) { \Log::error('AutodialerService@mergeAudiosUsingFFMpeg: exception: ' . $th->getMessage()); return false; } } }