| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193 |
- <?php
- declare(strict_types=1);
- namespace EKSRelay\Clients;
- use EKSRelay\Core\Env;
- use EKSRelay\Core\HttpClient;
- use EKSRelay\Core\HttpException;
- use EKSRelay\Core\Logger;
- final class ChatwootClient
- {
- private string $baseUrl;
- private string $token;
- private int $accountId;
- public function __construct()
- {
- $this->baseUrl = rtrim(Env::get('CHATWOOT_BASE_URL'), '/');
- $this->token = Env::get('CHATWOOT_API_TOKEN');
- $this->accountId = Env::getInt('CHATWOOT_ACCOUNT_ID', 1);
- }
- // ---------------------------------------------------------------
- // API helpers
- // ---------------------------------------------------------------
- private function apiUrl(string $path): string
- {
- // Chatwoot v2/v3 API: /api/v1/accounts/{account_id}/...
- return "{$this->baseUrl}/api/v1/accounts/{$this->accountId}{$path}";
- }
- private function authHeaders(): array
- {
- return ["api_access_token: {$this->token}"];
- }
- /**
- * @return array Decoded JSON response
- */
- private function api(string $method, string $path, ?array $body = null): array
- {
- $url = $this->apiUrl($path);
- $res = HttpClient::request($method, $url, $this->authHeaders(), $body);
- if ($res['status'] >= 400) {
- Logger::warn("Chatwoot API error", [
- 'method' => $method,
- 'path' => $path,
- 'status' => $res['status'],
- 'body' => mb_substr($res['body'], 0, 500),
- ]);
- throw new HttpException(502, 'CHATWOOT_API_ERROR', "Chatwoot returned HTTP {$res['status']}");
- }
- return $res['json'] ?? [];
- }
- // ---------------------------------------------------------------
- // Public methods
- // ---------------------------------------------------------------
- /**
- * Get full conversation details (labels, custom_attributes, etc.).
- */
- public function getConversation(int $conversationId): array
- {
- return $this->api('GET', "/conversations/{$conversationId}");
- }
- /**
- * Add a label to a conversation.
- * Chatwoot API: POST /conversations/{id}/labels (Chatwoot >= v2.14)
- * The endpoint expects { "labels": ["label1","label2"] } and REPLACES all labels,
- * so we first fetch existing labels and merge.
- *
- * NOTE: Chatwoot label API behaviour may vary across versions.
- * In v3.x the endpoint path/format is the same, but verify if you upgrade.
- */
- public function addLabel(int $conversationId, string $label): void
- {
- $conv = $this->getConversation($conversationId);
- $existing = $conv['labels'] ?? [];
- if (in_array($label, $existing, true)) {
- return; // already present
- }
- $existing[] = $label;
- // Chatwoot expects a JSON body with the complete labels array
- $this->api('POST', "/conversations/{$conversationId}/labels", [
- 'labels' => $existing,
- ]);
- Logger::info("Label added", ['conversation_id' => $conversationId, 'label' => $label]);
- }
- /**
- * Set custom attributes on a conversation.
- * POST /conversations/{id}/custom_attributes with { "custom_attributes": { ... } }
- *
- * Chatwoot merges provided keys into existing custom_attributes.
- */
- public function setCustomAttributes(int $conversationId, array $attrs): void
- {
- $result = $this->api('POST', "/conversations/{$conversationId}/custom_attributes", [
- 'custom_attributes' => $attrs,
- ]);
- Logger::info("Custom attributes set", ['conversation_id' => $conversationId, 'attrs' => $attrs]);
- Logger::debug("Custom attributes API response", [
- 'conversation_id' => $conversationId,
- 'returned_custom_attrs' => $result['custom_attributes'] ?? '(missing)',
- ]);
- }
- /**
- * Assign a specific agent to a conversation.
- * POST /conversations/{id}/assignments with { "assignee_id": agent_id }
- */
- public function assignConversation(int $conversationId, int $agentId): void
- {
- $this->api('POST', "/conversations/{$conversationId}/assignments", [
- 'assignee_id' => $agentId,
- ]);
- Logger::info("Conversation assigned", ['conversation_id' => $conversationId, 'agent_id' => $agentId]);
- }
- /**
- * Remove the assigned agent from a conversation (un-assign).
- *
- * Chatwoot API: POST /conversations/{id}/assignments
- * with { "assignee_id": null } to un-assign.
- *
- * NOTE (Chatwoot version): In v2.x/v3.x the assignments endpoint accepts
- * assignee_id=null to clear the assignment. If your version behaves
- * differently, adjust accordingly.
- */
- public function unassignConversation(int $conversationId): void
- {
- // Attempt to un-assign by setting assignee_id to null
- $url = $this->apiUrl("/conversations/{$conversationId}/assignments");
- $res = HttpClient::request('POST', $url, $this->authHeaders(), [
- 'assignee_id' => null,
- ]);
- if ($res['status'] >= 400) {
- Logger::warn("Unassign may have failed", [
- 'conversation_id' => $conversationId,
- 'status' => $res['status'],
- ]);
- } else {
- Logger::info("Conversation unassigned", ['conversation_id' => $conversationId]);
- }
- }
- /**
- * Send an outgoing message in a conversation.
- * POST /conversations/{id}/messages
- *
- * message_type: 1 = outgoing
- */
- public function sendOutgoingMessage(int $conversationId, string $text): array
- {
- return $this->api('POST', "/conversations/{$conversationId}/messages", [
- 'content' => $text,
- 'message_type' => 'outgoing',
- 'private' => false,
- ]);
- }
- /**
- * Check if conversation is in "ticket/manual" mode.
- * Returns true if label=ticket or custom_attributes.handoff=true.
- */
- public function isTicketMode(int $conversationId, ?array $convData = null): bool
- {
- $conv = $convData ?? $this->getConversation($conversationId);
- $ticketLabel = Env::get('CHATWOOT_TICKET_LABEL', 'ticket');
- $labels = $conv['labels'] ?? [];
- if (in_array($ticketLabel, $labels, true)) {
- return true;
- }
- $ca = $conv['custom_attributes'] ?? [];
- if (isset($ca['handoff']) && ($ca['handoff'] === true || $ca['handoff'] === 'true')) {
- return true;
- }
- return false;
- }
- }
|