UnipileWebhookController.php 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. <?php
  2. namespace Controllers;
  3. use Libs\Logger;
  4. use Libs\Payload;
  5. use Models\IntegrationsModel;
  6. use Models\UnipileMessagesModel;
  7. use Psr\Http\Message\ServerRequestInterface;
  8. class UnipileWebhookController
  9. {
  10. private IntegrationsModel $integrationsModel;
  11. private UnipileMessagesModel $messagesModel;
  12. public function __construct()
  13. {
  14. $this->integrationsModel = new IntegrationsModel();
  15. $this->messagesModel = new UnipileMessagesModel();
  16. }
  17. public function __invoke(ServerRequestInterface $request)
  18. {
  19. if (!$this->isAuthorized($request)) {
  20. return Payload::fail('Unauthorized', [], 'E_VALIDATE', 401);
  21. }
  22. $payload = json_decode((string) $request->getBody(), true) ?: [];
  23. if ($payload === []) {
  24. return Payload::fail('Invalid webhook payload', [], 'E_VALIDATE', 400);
  25. }
  26. try {
  27. if (isset($payload['AccountStatus']) && is_array($payload['AccountStatus'])) {
  28. return $this->handleAccountStatus($payload);
  29. }
  30. return $this->handleMessageEvent($payload);
  31. } catch (\Throwable $e) {
  32. Logger::error('Failed to process Unipile webhook', ['error' => $e->getMessage()]);
  33. return Payload::fail('Failed to process webhook', [], 'E_GENERIC', 500);
  34. }
  35. }
  36. private function handleAccountStatus(array $payload)
  37. {
  38. $statusPayload = $payload['AccountStatus'];
  39. $accountId = (string) ($statusPayload['account_id'] ?? '');
  40. $status = (string) ($statusPayload['message'] ?? '');
  41. if ($accountId === '' || $status === '') {
  42. return Payload::fail('Invalid account status payload', [], 'E_VALIDATE', 400);
  43. }
  44. $integration = $this->integrationsModel->updateStatusByAccountId($accountId, $status, $payload);
  45. if ($integration !== null) {
  46. $this->messagesModel->storeWebhookEvent(
  47. (int) $integration['integration_id'],
  48. 'account_status',
  49. $accountId . ':' . $status,
  50. $payload,
  51. true
  52. );
  53. }
  54. return Payload::ok();
  55. }
  56. private function handleMessageEvent(array $payload)
  57. {
  58. $accountId = (string) ($payload['account_id'] ?? '');
  59. $accountType = mb_strtoupper(trim((string) ($payload['account_type'] ?? '')));
  60. $event = (string) ($payload['event'] ?? 'message_received');
  61. if ($accountId === '' || $accountType !== 'WHATSAPP') {
  62. return Payload::ok();
  63. }
  64. $integration = $this->integrationsModel->findByAccountId($accountId);
  65. if ($integration === null) {
  66. Logger::warning('Unipile webhook account not found', ['account_id' => $accountId, 'event' => $event]);
  67. return Payload::ok();
  68. }
  69. $externalId = (string) ($payload['message_id'] ?? $payload['chat_id'] ?? hash('sha256', json_encode($payload)));
  70. $processed = false;
  71. if (in_array($event, ['message_received', 'message_edited', 'message_deleted', 'message_delivered', 'message_read'], true)) {
  72. $processed = $this->messagesModel->upsertMessageFromWebhook($integration, $payload) !== null;
  73. }
  74. $this->messagesModel->storeWebhookEvent((int) $integration['integration_id'], $event, $externalId, $payload, $processed);
  75. return Payload::ok();
  76. }
  77. private function isAuthorized(ServerRequestInterface $request): bool
  78. {
  79. $expected = (string) ($_ENV['UNIPILE_WEBHOOK_SECRET'] ?? '');
  80. if ($expected === '') {
  81. return false;
  82. }
  83. return hash_equals($expected, $request->getHeaderLine('Unipile-Auth'));
  84. }
  85. }