false, 'provider_call_id' => null, 'message' => 'شماره داخلی برای این کاربر ثبت نشده است.', 'status' => 'missing_extension', ]; } $config = $this->config(); $missing = $this->missingConfig($config); if ($missing !== []) { return [ 'success' => false, 'provider_call_id' => null, 'message' => 'تنظیمات AMI کامل نیست.', 'status' => 'configuration_error', 'missing' => $missing, ]; } $connection = $this->connect($config, $error); if (! $connection) { return [ 'success' => false, 'provider_call_id' => null, 'message' => $error ?: 'اتصال به مرکز تلفن برقرار نشد.', 'status' => 'connection_failed', ]; } $login = $this->login($connection, $config); if (! $this->isSuccess($login)) { fclose($connection); return [ 'success' => false, 'provider_call_id' => null, 'message' => $login['Message'] ?? 'ورود به AMI انجام نشد.', 'status' => 'login_failed', 'raw' => $login, ]; } $providerCallId = 'ami_'.uniqid(); $channel = "{$config['technology']}/{$extension}"; $response = $this->sendAction($connection, [ 'Action' => 'Originate', 'ActionID' => $providerCallId, 'Channel' => $channel, 'Context' => $config['context'], 'Exten' => $phone, 'Priority' => '1', 'CallerID' => $this->callerId($extension), 'Timeout' => (string) ((int) $config['originate_timeout'] * 1000), 'Async' => 'true', 'Variable' => "CRM_USER_EXTENSION={$extension}", ]); $this->logout($connection); fclose($connection); $ok = $this->isSuccess($response); return [ 'success' => $ok, 'provider_call_id' => $providerCallId, 'message' => $ok ? 'درخواست تماس به مرکز تلفن ارسال شد.' : ($response['Message'] ?? 'درخواست تماس به مرکز تلفن ارسال نشد.'), 'status' => $ok ? 'initiated' : 'failed', 'raw' => $response, 'channel' => $channel, ]; } public function getCallStatus(string $providerCallId): array { return [ 'status' => 'pending', 'provider_call_id' => $providerCallId, ]; } public function getRecordingUrl(string $providerCallId): ?string { $template = Setting::where('key', 'voip_ami_recording_url')->value('value'); return $template ? str_replace('{id}', $providerCallId, $template) : null; } public function testConnection(): array { $config = $this->config(); $missing = $this->missingConfig($config); if ($missing !== []) { return [ 'ok' => false, 'message' => 'تنظیمات AMI کامل نیست.', 'missing' => $missing, ]; } $connection = $this->connect($config, $error); if (! $connection) { return [ 'ok' => false, 'message' => $error ?: 'اتصال به AMI برقرار نشد.', 'missing' => [], ]; } $login = $this->login($connection, $config); $this->logout($connection); fclose($connection); return [ 'ok' => $this->isSuccess($login), 'message' => $this->isSuccess($login) ? 'اتصال AMI موفق بود.' : ($login['Message'] ?? 'ورود به AMI انجام نشد.'), 'missing' => [], ]; } private function config(): array { return [ 'host' => trim((string) Setting::where('key', 'voip_ami_host')->value('value')), 'port' => (int) (Setting::where('key', 'voip_ami_port')->value('value') ?: 5038), 'username' => trim((string) Setting::where('key', 'voip_ami_username')->value('value')), 'secret' => (string) Setting::where('key', 'voip_ami_secret')->value('value'), 'technology' => trim((string) (Setting::where('key', 'voip_ami_channel_technology')->value('value') ?: 'SIP')), 'context' => trim((string) (Setting::where('key', 'voip_ami_context')->value('value') ?: 'from-internal')), 'timeout' => (float) (Setting::where('key', 'voip_ami_timeout')->value('value') ?: 5), 'originate_timeout' => (int) (Setting::where('key', 'voip_ami_originate_timeout')->value('value') ?: 30), ]; } private function missingConfig(array $config): array { return array_values(array_filter(['host', 'port', 'username', 'secret', 'technology', 'context'], fn (string $key) => empty($config[$key]))); } private function connect(array $config, ?string &$error): mixed { $error = null; $connection = @stream_socket_client( "tcp://{$config['host']}:{$config['port']}", $errorCode, $errorMessage, $config['timeout'] ); if (! $connection) { $error = $errorMessage ?: "خطای اتصال AMI ({$errorCode})"; return false; } stream_set_timeout($connection, (int) ceil($config['timeout'])); fgets($connection); return $connection; } private function login(mixed $connection, array $config): array { return $this->sendAction($connection, [ 'Action' => 'Login', 'Username' => $config['username'], 'Secret' => $config['secret'], 'Events' => 'off', ]); } private function logout(mixed $connection): void { $this->sendAction($connection, ['Action' => 'Logoff']); } private function sendAction(mixed $connection, array $headers): array { foreach ($headers as $key => $value) { fwrite($connection, "{$key}: {$value}\r\n"); } fwrite($connection, "\r\n"); return $this->readResponse($connection); } private function readResponse(mixed $connection): array { $response = []; while (! feof($connection)) { $line = fgets($connection); if ($line === false || trim($line) === '') { break; } if (str_contains($line, ':')) { [$key, $value] = explode(':', $line, 2); $response[trim($key)] = trim($value); } } return $response; } private function isSuccess(array $response): bool { return strtolower((string) ($response['Response'] ?? '')) === 'success'; } private function callerId(string $extension): string { $template = Setting::where('key', 'voip_ami_caller_id_template')->value('value') ?: 'CRM <{extension}>'; return str_replace('{extension}', $extension, $template); } }