Initial commit - NexusPBX IP Telephony Platform

This commit is contained in:
sorcier committed 2026-11-02 02:24:47 +01:00
commit c5de8d0c71
25 files changed
+18123

No files matched your search

+227
View File
@@ -0,0 +1,227 @@
<?php
declare(strict_types=1);
namespace App;
use PDO;
use RuntimeException;
class AmiClient
{
public function __construct(
private string $host,
private int $port,
private string $username,
private string $secret,
private int $timeout = 3
) {}
public static function fromDatabase(PDO $db): self
{
$row = $db->query('SELECT ami_host, ami_port, ami_user, ami_secret FROM settings WHERE id = 1')->fetch();
return new self(
(string)($row['ami_host'] ?? '127.0.0.1'),
(int)($row['ami_port'] ?? 5038),
(string)($row['ami_user'] ?? 'pbxgui'),
(string)($row['ami_secret'] ?? 'AsteriskAmi2026!')
);
}
/**
* Teste la connexion et l'authentification AMI
*/
public function testConnection(): array
{
$fp = $this->connectAndLogin($banner);
$versionOutput = $this->sendAction($fp, [
'Action' => 'Command',
'Command' => 'core show version',
]);
$this->logoff($fp);
return [
'connected' => true,
'banner' => trim($banner),
'version' => $this->extractCommandOutput($versionOutput),
];
}
/**
* Recharge à chaud l'ensemble des modules PJSIP, force le Re-Register SIP immédiat des Trunks, et recharge le Dialplan via AMI
*/
public function reloadPjsipAndDialplan(): array
{
$fp = $this->connectAndLogin($banner);
// Recharge l'intégralité de la pile PJSIP (res_pjsip.so, res_pjsip_outbound_registration.so, res_pjsip_endpoint_identifier_ip.so, etc.)
$pjsipRes = $this->sendAction($fp, [
'Action' => 'Command',
'Command' => 'pjsip reload',
]);
// Force l'envoi immédiat d'une requête SIP REGISTER vers tous les opérateurs (reprend immédiatement la main si un autre PBX était enregistré)
$regRes = $this->sendAction($fp, [
'Action' => 'Command',
'Command' => 'pjsip send register *all',
]);
// Recharge le dialplan (extensions.conf / extensions.custom.conf)
$dialplanRes = $this->sendAction($fp, [
'Action' => 'Command',
'Command' => 'dialplan reload',
]);
// Recharge les classes de musique d'attente (musiconhold.conf)
$mohRes = $this->sendAction($fp, [
'Action' => 'Command',
'Command' => 'moh reload',
]);
// Recharge le module RTP (plages de ports rtpstart/rtpend & NAT)
$rtpRes = $this->sendAction($fp, [
'Action' => 'Command',
'Command' => 'module reload res_rtp_asterisk.so',
]);
// Recharge les touches en cours d'appel (features.conf : transferts ##, *2, etc.)
$featuresRes = $this->sendAction($fp, [
'Action' => 'Command',
'Command' => 'module reload features',
]);
// Recharge la messagerie vocale (voicemail.conf & voicemail.custom.conf)
$voicemailRes = $this->sendAction($fp, [
'Action' => 'Command',
'Command' => 'voicemail reload',
]);
$this->logoff($fp);
// Laisse 1.25s au scheduler PJSIP (1s) + registrar opérateur (OVH) pour répondre 200 OK avant le rafraîchissement de l'état UI
usleep(1250000);
return [
'banner' => trim($banner),
'pjsip' => $this->extractCommandOutput($pjsipRes),
'register' => $this->extractCommandOutput($regRes),
'dialplan' => $this->extractCommandOutput($dialplanRes),
'moh' => $this->extractCommandOutput($mohRes),
];
}
/**
* Force l'envoi immédiat d'un paquet SIP REGISTER sur un trunk précis ou tous les trunks (*all)
*/
public function forceRegister(string $regObject = '*all'): string
{
$fp = $this->connectAndLogin($banner);
$raw = $this->sendAction($fp, [
'Action' => 'Command',
'Command' => 'pjsip send register ' . $regObject,
]);
$this->logoff($fp);
usleep(1250000);
return $this->extractCommandOutput($raw);
}
/**
* Exécute une commande CLI Asterisk via AMI (ex: "pjsip show endpoints")
*/
public function runCommand(string $cliCommand): string
{
$fp = $this->connectAndLogin($banner);
$raw = $this->sendAction($fp, [
'Action' => 'Command',
'Command' => $cliCommand,
]);
$this->logoff($fp);
return $this->extractCommandOutput($raw);
}
/**
* @return resource
*/
private function connectAndLogin(?string &$banner = null)
{
$errno = 0;
$errstr = '';
$fp = @fsockopen($this->host, $this->port, $errno, $errstr, $this->timeout);
if (!$fp) {
throw new RuntimeException("Connexion socket AMI refusée sur {$this->host}:{$this->port} ({$errstr} [{$errno}]). Vérifiez qu'Asterisk et manager.conf sont actifs.");
}
stream_set_timeout($fp, $this->timeout);
$banner = (string)fgets($fp, 1024);
$loginResp = $this->sendAction($fp, [
'Action' => 'Login',
'Username' => $this->username,
'Secret' => $this->secret,
'Events' => 'off',
]);
if (!preg_match('/Response:\s*Success/i', $loginResp)) {
fclose($fp);
throw new RuntimeException("Échec d'authentification AMI pour l'utilisateur '{$this->username}' sur {$this->host}:{$this->port}.");
}
return $fp;
}
/**
* @param resource $fp
*/
private function sendAction($fp, array $headers): string
{
$packet = '';
foreach ($headers as $k => $v) {
$packet .= "{$k}: {$v}\r\n";
}
$packet .= "\r\n";
fwrite($fp, $packet);
$response = '';
while (!feof($fp)) {
$line = fgets($fp, 4096);
if ($line === false) {
break;
}
$response .= $line;
if (str_ends_with($response, "\r\n\r\n") || str_ends_with($response, "\n\n")) {
break;
}
}
return $response;
}
/**
* @param resource $fp
*/
private function logoff($fp): void
{
@fwrite($fp, "Action: Logoff\r\n\r\n");
@fclose($fp);
}
private function extractCommandOutput(string $raw): string
{
$lines = preg_split('/\r?\n/', trim($raw)) ?: [];
$clean = [];
foreach ($lines as $line) {
if (preg_match('/^(Response|Privilege|ActionID|Message):/i', $line)) {
continue;
}
if (str_starts_with($line, 'Output:')) {
$val = ltrim(substr($line, 7));
if ($val !== '') {
$clean[] = $val;
}
} elseif ($line !== '--END COMMAND--') {
$clean[] = $line;
}
}
return trim(implode("\n", $clean));
}
}