dsn = trim((string) ($_ENV['UNIPILE_DSN'] ?? '')); $this->apiKey = trim((string) ($_ENV['UNIPILE_API_KEY'] ?? '')); } public function isConfigured(): bool { return $this->dsn !== '' && $this->apiKey !== ''; } public function createHostedAuthLink(array $payload): array { return $this->requestJson('POST', '/hosted/accounts/link', $payload); } public function sendTextMessage(string $chatId, string $text): array { return $this->requestMultipart('POST', '/chats/' . rawurlencode($chatId) . '/messages', ['text' => $text]); } public function apiUrl(): string { return $this->baseEndpoint(); } private function requestJson(string $method, string $path, array $payload = []): array { return $this->request($method, $path, [ 'Content-Type: application/json', ], $payload === [] ? '' : json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); } private function requestMultipart(string $method, string $path, array $payload): array { return $this->request($method, $path, [], $payload); } private function request(string $method, string $path, array $headers, $body): array { if (!$this->isConfigured()) { throw new \RuntimeException('Unipile is not configured.'); } if (!function_exists('curl_init')) { throw new \RuntimeException('PHP cURL extension is required for Unipile requests.'); } $url = $this->baseUrl() . $path; $requestHeaders = array_merge([ 'X-API-KEY: ' . $this->apiKey, 'Accept: application/json', ], $headers); $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, strtoupper($method)); curl_setopt($ch, CURLOPT_HTTPHEADER, $requestHeaders); curl_setopt($ch, CURLOPT_TIMEOUT, 30); if ($body !== '' && $body !== []) { curl_setopt($ch, CURLOPT_POSTFIELDS, $body); } $raw = curl_exec($ch); $error = curl_error($ch); $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($raw === false) { Logger::error('Unipile request failed', ['path' => $path, 'error' => $error]); throw new \RuntimeException('Unipile request failed.'); } $decoded = json_decode((string) $raw, true); if (!is_array($decoded)) { $decoded = ['raw' => (string) $raw]; } if ($status < 200 || $status >= 300) { Logger::warning('Unipile returned an error', ['path' => $path, 'status' => $status, 'response' => $decoded]); throw new \RuntimeException('Unipile returned HTTP ' . $status . '.'); } return $decoded; } private function baseUrl(): string { return $this->baseEndpoint() . '/api/v1'; } private function baseEndpoint(): string { $dsn = preg_replace('#^https?://#', '', $this->dsn) ?? $this->dsn; $dsn = rtrim($dsn, '/'); $dsn = preg_replace('#/api/v1$#', '', $dsn) ?? $dsn; return 'https://' . $dsn; } }