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)); } }