CRM/backend/app/Services/VoIP/AmiProvider.php

223 خطوط
7.5 KiB
PHP

<?php
namespace App\Services\VoIP;
use App\Models\Setting;
class AmiProvider implements VoIPProviderInterface
{
public function initiateCall(string $phone, string $callerId = null): array
{
$extension = trim((string) $callerId);
if ($extension === '') {
return [
'success' => 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);
}
}