223 خطوط
7.5 KiB
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);
|
|
}
|
|
}
|