UnipileClient.php 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. <?php
  2. namespace Services;
  3. use Libs\Logger;
  4. class UnipileClient
  5. {
  6. private string $dsn;
  7. private string $apiKey;
  8. public function __construct()
  9. {
  10. $this->dsn = trim((string) ($_ENV['UNIPILE_DSN'] ?? ''));
  11. $this->apiKey = trim((string) ($_ENV['UNIPILE_API_KEY'] ?? ''));
  12. }
  13. public function isConfigured(): bool
  14. {
  15. return $this->dsn !== '' && $this->apiKey !== '';
  16. }
  17. public function createHostedAuthLink(array $payload): array
  18. {
  19. return $this->requestJson('POST', '/hosted/accounts/link', $payload);
  20. }
  21. public function sendTextMessage(string $chatId, string $text): array
  22. {
  23. return $this->requestMultipart('POST', '/chats/' . rawurlencode($chatId) . '/messages', ['text' => $text]);
  24. }
  25. public function apiUrl(): string
  26. {
  27. return $this->baseEndpoint();
  28. }
  29. private function requestJson(string $method, string $path, array $payload = []): array
  30. {
  31. return $this->request($method, $path, [
  32. 'Content-Type: application/json',
  33. ], $payload === [] ? '' : json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
  34. }
  35. private function requestMultipart(string $method, string $path, array $payload): array
  36. {
  37. return $this->request($method, $path, [], $payload);
  38. }
  39. private function request(string $method, string $path, array $headers, $body): array
  40. {
  41. if (!$this->isConfigured()) {
  42. throw new \RuntimeException('Unipile is not configured.');
  43. }
  44. if (!function_exists('curl_init')) {
  45. throw new \RuntimeException('PHP cURL extension is required for Unipile requests.');
  46. }
  47. $url = $this->baseUrl() . $path;
  48. $requestHeaders = array_merge([
  49. 'X-API-KEY: ' . $this->apiKey,
  50. 'Accept: application/json',
  51. ], $headers);
  52. $ch = curl_init($url);
  53. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  54. curl_setopt($ch, CURLOPT_CUSTOMREQUEST, strtoupper($method));
  55. curl_setopt($ch, CURLOPT_HTTPHEADER, $requestHeaders);
  56. curl_setopt($ch, CURLOPT_TIMEOUT, 30);
  57. if ($body !== '' && $body !== []) {
  58. curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
  59. }
  60. $raw = curl_exec($ch);
  61. $error = curl_error($ch);
  62. $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
  63. curl_close($ch);
  64. if ($raw === false) {
  65. Logger::error('Unipile request failed', ['path' => $path, 'error' => $error]);
  66. throw new \RuntimeException('Unipile request failed.');
  67. }
  68. $decoded = json_decode((string) $raw, true);
  69. if (!is_array($decoded)) {
  70. $decoded = ['raw' => (string) $raw];
  71. }
  72. if ($status < 200 || $status >= 300) {
  73. Logger::warning('Unipile returned an error', ['path' => $path, 'status' => $status, 'response' => $decoded]);
  74. throw new \RuntimeException('Unipile returned HTTP ' . $status . '.');
  75. }
  76. return $decoded;
  77. }
  78. private function baseUrl(): string
  79. {
  80. return $this->baseEndpoint() . '/api/v1';
  81. }
  82. private function baseEndpoint(): string
  83. {
  84. $dsn = preg_replace('#^https?://#', '', $this->dsn) ?? $this->dsn;
  85. $dsn = rtrim($dsn, '/');
  86. $dsn = preg_replace('#/api/v1$#', '', $dsn) ?? $dsn;
  87. return 'https://' . $dsn;
  88. }
  89. }