From 17f76e90122d245aace6640a1f8766fb77c29ef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Sat, 24 Dec 2022 17:28:41 +0100 Subject: [PATCH 01/20] 2.x: Split responsability of Request class --- src/Client.php | 12 +- src/Http/ClientHandler.php | 6 +- src/Http/GuzzleClientHandler.php | 4 +- src/Request.php | 81 +------- src/Request/MessageRequest.php | 54 ++++++ .../RequestAudioMessage.php | 15 +- .../RequestContactMessage.php | 19 +- .../RequestDocumentMessage.php | 15 +- .../RequestImageMessage.php | 15 +- .../RequestLocationMessage.php | 15 +- .../RequestOptionsListMessage.php | 10 +- .../RequestStickerMessage.php | 13 +- .../RequestTemplateMessage.php | 18 +- .../RequestTextMessage.php | 15 +- .../RequestVideoMessage.php | 15 +- src/WhatsAppCloudApi.php | 51 +++-- tests/Integration/ClientTest.php | 4 +- tests/Integration/WhatsAppCloudApiTest.php | 4 + tests/Unit/WhatsAppCloudApiTest.php | 177 ++++++++---------- 19 files changed, 242 insertions(+), 301 deletions(-) create mode 100644 src/Request/MessageRequest.php rename src/Request/{ => MessageRequest}/RequestAudioMessage.php (61%) rename src/Request/{ => MessageRequest}/RequestContactMessage.php (74%) rename src/Request/{ => MessageRequest}/RequestDocumentMessage.php (67%) rename src/Request/{ => MessageRequest}/RequestImageMessage.php (64%) rename src/Request/{ => MessageRequest}/RequestLocationMessage.php (68%) rename src/Request/{ => MessageRequest}/RequestOptionsListMessage.php (78%) rename src/Request/{ => MessageRequest}/RequestStickerMessage.php (63%) rename src/Request/{ => MessageRequest}/RequestTemplateMessage.php (68%) rename src/Request/{ => MessageRequest}/RequestTextMessage.php (63%) rename src/Request/{ => MessageRequest}/RequestVideoMessage.php (65%) diff --git a/src/Client.php b/src/Client.php index 29272f7..f628f5c 100644 --- a/src/Client.php +++ b/src/Client.php @@ -42,11 +42,11 @@ public function __construct(string $graph_version, ?ClientHandler $handler = nul * * @throws Netflie\WhatsAppCloudApi\Response\ResponseException */ - public function sendRequest(Request $request): Response + public function sendMessage(Request\MessageRequest $request): Response { - $raw_response = $this->handler->send( - $this->buildRequestUri($request), - $request->encodedBody(), + $raw_response = $this->handler->postJsonData( + $this->buildRequestUri($request->nodePath()), + $request->body(), $request->headers(), $request->timeout() ); @@ -75,8 +75,8 @@ private function buildBaseUri(): string return self::BASE_GRAPH_URL . '/' . $this->graph_version; } - private function buildRequestUri(Request $request): string + private function buildRequestUri(string $node_path): string { - return $this->buildBaseUri() . '/' . $request->fromPhoneNumberId() . '/messages'; + return $this->buildBaseUri() . '/' . $node_path; } } diff --git a/src/Http/ClientHandler.php b/src/Http/ClientHandler.php index ca7c1a2..6ac4033 100644 --- a/src/Http/ClientHandler.php +++ b/src/Http/ClientHandler.php @@ -5,10 +5,10 @@ interface ClientHandler { /** - * Sends a request to the server and returns the raw response. + * Sends a POST request to the server and returns the raw response. * * @param string $url The endpoint to send the request to. - * @param string $body The body of the request. + * @param array $body The body of the request. * @param array $headers The request headers. * @param int $timeout The timeout in seconds for the request. * @@ -16,5 +16,5 @@ interface ClientHandler * * @throws Netflie\WhatsAppCloudApi\Response\ResponseException */ - public function send(string $url, string $body, array $headers, int $timeout): RawResponse; + public function postJsonData(string $url, array $body, array $headers, int $timeout): RawResponse; } diff --git a/src/Http/GuzzleClientHandler.php b/src/Http/GuzzleClientHandler.php index 106d8bd..9bfe4cb 100644 --- a/src/Http/GuzzleClientHandler.php +++ b/src/Http/GuzzleClientHandler.php @@ -23,10 +23,10 @@ public function __construct(?Client $guzzle_client = null) * {@inheritDoc} * */ - public function send(string $url, string $body, array $headers, int $timeout): RawResponse + public function postJsonData(string $url, array $body, array $headers, int $timeout): RawResponse { $raw_handler_response = $this->guzzle_client->post($url, [ - 'body' => $body, + 'json' => $body, 'headers' => $headers, 'timeout' => $timeout, ]); diff --git a/src/Request.php b/src/Request.php index b968d4f..c606590 100644 --- a/src/Request.php +++ b/src/Request.php @@ -2,8 +2,6 @@ namespace Netflie\WhatsAppCloudApi; -use Netflie\WhatsAppCloudApi\Message\Message; - abstract class Request { /** @@ -11,35 +9,11 @@ abstract class Request */ public const DEFAULT_REQUEST_TIMEOUT = 60; - /** - * @var Message WhatsApp Message to be sent. - */ - protected Message $message; - /** * @var string The access token to use for this request. */ protected string $access_token; - /** - * @var string WhatsApp Number Id from messages will sent. - */ - protected string $from_phone_number_id; - - /** - * The raw body request. - * - * @return array - */ - protected array $body; - - /** - * The raw encoded body request. - * - * @return string - */ - protected string $encoded_body; - /** * The timeout request. * @@ -53,35 +27,10 @@ abstract class Request * @param Message $message * @param string $access_token */ - public function __construct(Message $message, string $access_token, string $from_phone_number_id, ?int $timeout = null) + public function __construct(string $access_token, ?int $timeout = null) { - $this->message = $message; $this->access_token = $access_token; - $this->from_phone_number_id = $from_phone_number_id; $this->timeout = $timeout ?? static::DEFAULT_REQUEST_TIMEOUT; - - $this->makeBody(); - $this->encodeBody(); - } - - /** - * Returns the raw body of the request. - * - * @return array - */ - public function body(): array - { - return $this->body; - } - - /** - * Returns the body of the request encoded. - * - * @return string - */ - public function encodedBody(): string - { - return $this->encoded_body; } /** @@ -93,7 +42,6 @@ public function headers(): array { return [ 'Authorization' => "Bearer $this->access_token", - 'Content-Type' => 'application/json', ]; } @@ -107,16 +55,6 @@ public function accessToken(): string return $this->access_token; } - /** - * Return WhatsApp Number Id for this request. - * - * @return string - */ - public function fromPhoneNumberId(): string - { - return $this->from_phone_number_id; - } - /** * Return the timeout for this request. * @@ -126,21 +64,4 @@ public function timeout(): int { return $this->timeout; } - - /** - * Makes the raw body of the request. - * - * @return array - */ - abstract protected function makeBody(): void; - - /** - * Encodes the raw body of the request. - * - * @return array - */ - private function encodeBody(): void - { - $this->encoded_body = json_encode($this->body()); - } } diff --git a/src/Request/MessageRequest.php b/src/Request/MessageRequest.php new file mode 100644 index 0000000..fb0b647 --- /dev/null +++ b/src/Request/MessageRequest.php @@ -0,0 +1,54 @@ +message = $message; + $this->from_phone_number_id = $from_phone_number_id; + + parent::__construct($access_token, $timeout); + } + + /** + * Returns the raw body of the request. + * + * @return array + */ + abstract public function body(): array; + + /** + * Return WhatsApp Number Id for this request. + * + * @return string + */ + public function fromPhoneNumberId(): string + { + return $this->from_phone_number_id; + } + + /** + * WhatsApp node path. + * + * @return string + */ + public function nodePath(): string + { + return $this->from_phone_number_id . '/messages'; + } +} diff --git a/src/Request/RequestAudioMessage.php b/src/Request/MessageRequest/RequestAudioMessage.php similarity index 61% rename from src/Request/RequestAudioMessage.php rename to src/Request/MessageRequest/RequestAudioMessage.php index 571fe6e..7352834 100644 --- a/src/Request/RequestAudioMessage.php +++ b/src/Request/MessageRequest/RequestAudioMessage.php @@ -1,18 +1,17 @@ body = [ + return [ 'messaging_product' => $this->message->messagingProduct(), 'recipient_type' => $this->message->recipientType(), 'to' => $this->message->to(), diff --git a/src/Request/RequestContactMessage.php b/src/Request/MessageRequest/RequestContactMessage.php similarity index 74% rename from src/Request/RequestContactMessage.php rename to src/Request/MessageRequest/RequestContactMessage.php index f04c11d..d93b50f 100644 --- a/src/Request/RequestContactMessage.php +++ b/src/Request/MessageRequest/RequestContactMessage.php @@ -1,20 +1,19 @@ message->type(); - $this->body = [ + $body = [ 'messaging_product' => $this->message->messagingProduct(), 'recipient_type' => $this->message->recipientType(), 'to' => $this->message->to(), @@ -40,7 +39,9 @@ protected function makeBody(): void $phone_array['wa_id'] = $phone->waId(); } - $this->body[$message_type][0]['phones'][] = $phone_array; + $body[$message_type][0]['phones'][] = $phone_array; } + + return $body; } } diff --git a/src/Request/RequestDocumentMessage.php b/src/Request/MessageRequest/RequestDocumentMessage.php similarity index 67% rename from src/Request/RequestDocumentMessage.php rename to src/Request/MessageRequest/RequestDocumentMessage.php index 1583561..3f6c607 100644 --- a/src/Request/RequestDocumentMessage.php +++ b/src/Request/MessageRequest/RequestDocumentMessage.php @@ -1,18 +1,17 @@ body = [ + return [ 'messaging_product' => $this->message->messagingProduct(), 'recipient_type' => $this->message->recipientType(), 'to' => $this->message->to(), diff --git a/src/Request/RequestImageMessage.php b/src/Request/MessageRequest/RequestImageMessage.php similarity index 64% rename from src/Request/RequestImageMessage.php rename to src/Request/MessageRequest/RequestImageMessage.php index 08d59de..ce8f03a 100644 --- a/src/Request/RequestImageMessage.php +++ b/src/Request/MessageRequest/RequestImageMessage.php @@ -1,18 +1,17 @@ body = [ + return [ 'messaging_product' => $this->message->messagingProduct(), 'recipient_type' => $this->message->recipientType(), 'to' => $this->message->to(), diff --git a/src/Request/RequestLocationMessage.php b/src/Request/MessageRequest/RequestLocationMessage.php similarity index 68% rename from src/Request/RequestLocationMessage.php rename to src/Request/MessageRequest/RequestLocationMessage.php index 7235c34..9e7c6ae 100644 --- a/src/Request/RequestLocationMessage.php +++ b/src/Request/MessageRequest/RequestLocationMessage.php @@ -1,18 +1,17 @@ body = [ + return [ 'messaging_product' => $this->message->messagingProduct(), 'recipient_type' => $this->message->recipientType(), 'to' => $this->message->to(), diff --git a/src/Request/RequestOptionsListMessage.php b/src/Request/MessageRequest/RequestOptionsListMessage.php similarity index 78% rename from src/Request/RequestOptionsListMessage.php rename to src/Request/MessageRequest/RequestOptionsListMessage.php index 1bcd3e7..ccf2ac5 100644 --- a/src/Request/RequestOptionsListMessage.php +++ b/src/Request/MessageRequest/RequestOptionsListMessage.php @@ -1,21 +1,21 @@ body = [ + return [ 'messaging_product' => $this->message->messagingProduct(), 'recipient_type' => $this->message->recipientType(), 'to' => $this->message->to(), diff --git a/src/Request/RequestStickerMessage.php b/src/Request/MessageRequest/RequestStickerMessage.php similarity index 63% rename from src/Request/RequestStickerMessage.php rename to src/Request/MessageRequest/RequestStickerMessage.php index 2f51789..0077968 100644 --- a/src/Request/RequestStickerMessage.php +++ b/src/Request/MessageRequest/RequestStickerMessage.php @@ -1,18 +1,17 @@ body = [ + return [ 'messaging_product' => $this->message->messagingProduct(), 'recipient_type' => $this->message->recipientType(), 'to' => $this->message->to(), diff --git a/src/Request/RequestTemplateMessage.php b/src/Request/MessageRequest/RequestTemplateMessage.php similarity index 68% rename from src/Request/RequestTemplateMessage.php rename to src/Request/MessageRequest/RequestTemplateMessage.php index 92d5b9b..9b493a0 100644 --- a/src/Request/RequestTemplateMessage.php +++ b/src/Request/MessageRequest/RequestTemplateMessage.php @@ -1,17 +1,17 @@ body = [ + $body = [ 'messaging_product' => $this->message->messagingProduct(), 'recipient_type' => $this->message->recipientType(), 'to' => $this->message->to(), @@ -24,21 +24,23 @@ protected function makeBody(): void ]; if ($this->message->header()) { - $this->body['template']['components'][] = [ + $body['template']['components'][] = [ 'type' => 'header', 'parameters' => $this->message->header(), ]; } if ($this->message->body()) { - $this->body['template']['components'][] = [ + $body['template']['components'][] = [ 'type' => 'body', 'parameters' => $this->message->body(), ]; } foreach ($this->message->buttons() as $button) { - $this->body['template']['components'][] = $button; + $body['template']['components'][] = $button; } + + return $body; } } diff --git a/src/Request/RequestTextMessage.php b/src/Request/MessageRequest/RequestTextMessage.php similarity index 63% rename from src/Request/RequestTextMessage.php rename to src/Request/MessageRequest/RequestTextMessage.php index bbfe16f..286fef5 100644 --- a/src/Request/RequestTextMessage.php +++ b/src/Request/MessageRequest/RequestTextMessage.php @@ -1,18 +1,17 @@ body = [ + return [ 'messaging_product' => $this->message->messagingProduct(), 'recipient_type' => $this->message->recipientType(), 'to' => $this->message->to(), diff --git a/src/Request/RequestVideoMessage.php b/src/Request/MessageRequest/RequestVideoMessage.php similarity index 65% rename from src/Request/RequestVideoMessage.php rename to src/Request/MessageRequest/RequestVideoMessage.php index b68d84b..e0411c6 100644 --- a/src/Request/RequestVideoMessage.php +++ b/src/Request/MessageRequest/RequestVideoMessage.php @@ -1,18 +1,17 @@ body = [ + return [ 'messaging_product' => $this->message->messagingProduct(), 'recipient_type' => $this->message->recipientType(), 'to' => $this->message->to(), diff --git a/src/WhatsAppCloudApi.php b/src/WhatsAppCloudApi.php index 9273493..1cbe4e4 100644 --- a/src/WhatsAppCloudApi.php +++ b/src/WhatsAppCloudApi.php @@ -18,16 +18,7 @@ use Netflie\WhatsAppCloudApi\Message\TemplateMessage; use Netflie\WhatsAppCloudApi\Message\TextMessage; use Netflie\WhatsAppCloudApi\Message\VideoMessage; -use Netflie\WhatsAppCloudApi\Request\RequestAudioMessage; -use Netflie\WhatsAppCloudApi\Request\RequestContactMessage; -use Netflie\WhatsAppCloudApi\Request\RequestDocumentMessage; -use Netflie\WhatsAppCloudApi\Request\RequestImageMessage; -use Netflie\WhatsAppCloudApi\Request\RequestLocationMessage; -use Netflie\WhatsAppCloudApi\Request\RequestOptionsListMessage; -use Netflie\WhatsAppCloudApi\Request\RequestStickerMessage; -use Netflie\WhatsAppCloudApi\Request\RequestTemplateMessage; -use Netflie\WhatsAppCloudApi\Request\RequestTextMessage; -use Netflie\WhatsAppCloudApi\Request\RequestVideoMessage; +use Netflie\WhatsAppCloudApi\Request\MessageRequest; class WhatsAppCloudApi { @@ -84,14 +75,14 @@ public function __construct(array $config) public function sendTextMessage(string $to, string $text, bool $preview_url = false): Response { $message = new TextMessage($to, $text, $preview_url); - $request = new RequestTextMessage( + $request = new MessageRequest\RequestTextMessage( $message, $this->app->accessToken(), $this->app->fromPhoneNumberId(), $this->timeout ); - return $this->client->sendRequest($request); + return $this->client->sendMessage($request); } /** @@ -107,14 +98,14 @@ public function sendTextMessage(string $to, string $text, bool $preview_url = fa public function sendDocument(string $to, MediaID $document_id, string $name, ?string $caption): Response { $message = new DocumentMessage($to, $document_id, $name, $caption); - $request = new RequestDocumentMessage( + $request = new MessageRequest\RequestDocumentMessage( $message, $this->app->accessToken(), $this->app->fromPhoneNumberId(), $this->timeout ); - return $this->client->sendRequest($request); + return $this->client->sendMessage($request); } /** @@ -133,14 +124,14 @@ public function sendDocument(string $to, MediaID $document_id, string $name, ?st public function sendTemplate(string $to, string $template_name, string $language = 'en_US', ?Component $components = null): Response { $message = new TemplateMessage($to, $template_name, $language, $components); - $request = new RequestTemplateMessage( + $request = new MessageRequest\RequestTemplateMessage( $message, $this->app->accessToken(), $this->app->fromPhoneNumberId(), $this->timeout ); - return $this->client->sendRequest($request); + return $this->client->sendMessage($request); } /** @@ -156,14 +147,14 @@ public function sendTemplate(string $to, string $template_name, string $language public function sendAudio(string $to, MediaID $document_id): Response { $message = new AudioMessage($to, $document_id); - $request = new RequestAudioMessage( + $request = new MessageRequest\RequestAudioMessage( $message, $this->app->accessToken(), $this->app->fromPhoneNumberId(), $this->timeout ); - return $this->client->sendRequest($request); + return $this->client->sendMessage($request); } /** @@ -180,14 +171,14 @@ public function sendAudio(string $to, MediaID $document_id): Response public function sendImage(string $to, MediaID $document_id, ?string $caption = ''): Response { $message = new ImageMessage($to, $document_id, $caption); - $request = new RequestImageMessage( + $request = new MessageRequest\RequestImageMessage( $message, $this->app->accessToken(), $this->app->fromPhoneNumberId(), $this->timeout ); - return $this->client->sendRequest($request); + return $this->client->sendMessage($request); } /** @@ -203,14 +194,14 @@ public function sendImage(string $to, MediaID $document_id, ?string $caption = ' public function sendVideo(string $to, MediaID $link, string $caption = ''): Response { $message = new VideoMessage($to, $link, $caption); - $request = new RequestVideoMessage( + $request = new MessageRequest\RequestVideoMessage( $message, $this->app->accessToken(), $this->app->fromPhoneNumberId(), $this->timeout ); - return $this->client->sendRequest($request); + return $this->client->sendMessage($request); } /** @@ -226,14 +217,14 @@ public function sendVideo(string $to, MediaID $link, string $caption = ''): Resp public function sendSticker(string $to, MediaID $link): Response { $message = new StickerMessage($to, $link); - $request = new RequestStickerMessage( + $request = new MessageRequest\RequestStickerMessage( $message, $this->app->accessToken(), $this->app->fromPhoneNumberId(), $this->timeout ); - return $this->client->sendRequest($request); + return $this->client->sendMessage($request); } /** @@ -252,14 +243,14 @@ public function sendSticker(string $to, MediaID $link): Response public function sendLocation(string $to, float $longitude, float $latitude, string $name = '', string $address = ''): Response { $message = new LocationMessage($to, $longitude, $latitude, $name, $address); - $request = new RequestLocationMessage( + $request = new MessageRequest\RequestLocationMessage( $message, $this->app->accessToken(), $this->app->fromPhoneNumberId(), $this->timeout ); - return $this->client->sendRequest($request); + return $this->client->sendMessage($request); } /** @@ -276,27 +267,27 @@ public function sendLocation(string $to, float $longitude, float $latitude, stri public function sendContact(string $to, ContactName $name, Phone ...$phone): Response { $message = new ContactMessage($to, $name, ...$phone); - $request = new RequestContactMessage( + $request = new MessageRequest\RequestContactMessage( $message, $this->app->accessToken(), $this->app->fromPhoneNumberId(), $this->timeout ); - return $this->client->sendRequest($request); + return $this->client->sendMessage($request); } public function sendList(string $to, string $header, string $body, string $footer, Action $action): Response { $message = new OptionsListMessage($to, $header, $body, $footer, $action); - $request = new RequestOptionsListMessage( + $request = new MessageRequest\RequestOptionsListMessage( $message, $this->app->accessToken(), $this->app->fromPhoneNumberId(), $this->timeout ); - return $this->client->sendRequest($request); + return $this->client->sendMessage($request); } /** diff --git a/tests/Integration/ClientTest.php b/tests/Integration/ClientTest.php index 0e69188..317aa10 100644 --- a/tests/Integration/ClientTest.php +++ b/tests/Integration/ClientTest.php @@ -4,7 +4,7 @@ use Netflie\WhatsAppCloudApi\Client; use Netflie\WhatsAppCloudApi\Message\TextMessage; -use Netflie\WhatsAppCloudApi\Request\RequestTextMessage; +use Netflie\WhatsAppCloudApi\Request\MessageRequest\RequestTextMessage; use Netflie\WhatsAppCloudApi\Tests\WhatsAppCloudApiTestConfiguration; use Netflie\WhatsAppCloudApi\WhatsAppCloudApi; use PHPUnit\Framework\TestCase; @@ -34,7 +34,7 @@ public function test_send_text_message() WhatsAppCloudApiTestConfiguration::$from_phone_number_id ); - $response = $this->client->sendRequest($request); + $response = $this->client->sendMessage($request); $this->assertEquals(200, $response->httpStatusCode()); $this->assertEquals($request, $response->request()); diff --git a/tests/Integration/WhatsAppCloudApiTest.php b/tests/Integration/WhatsAppCloudApiTest.php index 74fc8b2..32d4667 100644 --- a/tests/Integration/WhatsAppCloudApiTest.php +++ b/tests/Integration/WhatsAppCloudApiTest.php @@ -44,6 +44,10 @@ public function test_send_text_message() public function test_send_document_with_id() { + $this->markTestIncomplete( + 'This test should send a real media ID.' + ); + $media_id = new MediaObjectID('341476474779872'); $response = $this->whatsapp_app_cloud_api->sendDocument( WhatsAppCloudApiTestConfiguration::$to_phone_number_id, diff --git a/tests/Unit/WhatsAppCloudApiTest.php b/tests/Unit/WhatsAppCloudApiTest.php index 0fe0d9e..219166c 100644 --- a/tests/Unit/WhatsAppCloudApiTest.php +++ b/tests/Unit/WhatsAppCloudApiTest.php @@ -67,17 +67,14 @@ public function test_send_text_message_fails() 'body' => $text_message, ], ]; - $encoded_body = json_encode($body); - $encoded_response_body = '{"error":{"message":"Invalid OAuth access token - Cannot parse access token","type":"OAuthException","code":190,"fbtrace_id":"AbJuG-rMVv36mjw-r78mKwg"}}'; $headers = [ 'Authorization' => 'Bearer ' . $this->access_token, - 'Content-Type' => 'application/json', ]; $this->client_handler - ->send($url, $encoded_body, $headers, Argument::type('int')) + ->postJsonData($url, $body, $headers, Argument::type('int')) ->shouldBeCalled() - ->willReturn(new RawResponse([], $encoded_response_body, 401)); + ->willReturn(new RawResponse([], $this->failedMessageResponse(), 401)); $this->expectException(ResponseException::class); $response = $this->whatsapp_app_cloud_api->sendTextMessage( @@ -104,16 +101,14 @@ public function test_send_text_message() 'body' => $text_message, ], ]; - $encoded_body = json_encode($body); $headers = [ 'Authorization' => 'Bearer ' . $this->access_token, - 'Content-Type' => 'application/json', ]; $this->client_handler - ->send($url, $encoded_body, $headers, Argument::type('int')) + ->postJsonData($url, $body, $headers, Argument::type('int')) ->shouldBeCalled() - ->willReturn(new RawResponse($headers, $encoded_body, 200)); + ->willReturn(new RawResponse($headers, $this->successfulMessageNodeResponse(), 200)); $response = $this->whatsapp_app_cloud_api->sendTextMessage( $to, @@ -122,8 +117,8 @@ public function test_send_text_message() ); $this->assertEquals(200, $response->httpStatusCode()); - $this->assertEquals($body, $response->decodedBody()); - $this->assertEquals($encoded_body, $response->body()); + $this->assertEquals(json_decode($this->successfulMessageNodeResponse(), true), $response->decodedBody()); + $this->assertEquals($this->successfulMessageNodeResponse(), $response->body()); $this->assertEquals(false, $response->isError()); } @@ -146,16 +141,14 @@ public function test_send_document_id() 'id' => $document_id, ], ]; - $encoded_body = json_encode($body); $headers = [ 'Authorization' => 'Bearer ' . $this->access_token, - 'Content-Type' => 'application/json', ]; $this->client_handler - ->send($url, $encoded_body, $headers, Argument::type('int')) + ->postJsonData($url, $body, $headers, Argument::type('int')) ->shouldBeCalled() - ->willReturn(new RawResponse($headers, $encoded_body, 200)); + ->willReturn(new RawResponse($headers, $this->successfulMessageNodeResponse(), 200)); $media_id = new MediaObjectID($document_id); $response = $this->whatsapp_app_cloud_api->sendDocument( @@ -166,8 +159,8 @@ public function test_send_document_id() ); $this->assertEquals(200, $response->httpStatusCode()); - $this->assertEquals($body, $response->decodedBody()); - $this->assertEquals($encoded_body, $response->body()); + $this->assertEquals(json_decode($this->successfulMessageNodeResponse(), true), $response->decodedBody()); + $this->assertEquals($this->successfulMessageNodeResponse(), $response->body()); $this->assertEquals(false, $response->isError()); } @@ -190,16 +183,14 @@ public function test_send_document_link() 'link' => $document_link, ], ]; - $encoded_body = json_encode($body); $headers = [ 'Authorization' => 'Bearer ' . $this->access_token, - 'Content-Type' => 'application/json', ]; $this->client_handler - ->send($url, $encoded_body, $headers, Argument::type('int')) + ->postJsonData($url, $body, $headers, Argument::type('int')) ->shouldBeCalled() - ->willReturn(new RawResponse($headers, $encoded_body, 200)); + ->willReturn(new RawResponse($headers, $this->successfulMessageNodeResponse(), 200)); $link_id = new LinkID($document_link); $response = $this->whatsapp_app_cloud_api->sendDocument( @@ -210,8 +201,8 @@ public function test_send_document_link() ); $this->assertEquals(200, $response->httpStatusCode()); - $this->assertEquals($body, $response->decodedBody()); - $this->assertEquals($encoded_body, $response->body()); + $this->assertEquals(json_decode($this->successfulMessageNodeResponse(), true), $response->decodedBody()); + $this->assertEquals($this->successfulMessageNodeResponse(), $response->body()); $this->assertEquals(false, $response->isError()); } @@ -233,16 +224,14 @@ public function test_send_template_without_components() 'components' => [], ], ]; - $encoded_body = json_encode($body); $headers = [ 'Authorization' => 'Bearer ' . $this->access_token, - 'Content-Type' => 'application/json', ]; $this->client_handler - ->send($url, $encoded_body, $headers, Argument::type('int')) + ->postJsonData($url, $body, $headers, Argument::type('int')) ->shouldBeCalled() - ->willReturn(new RawResponse($headers, $encoded_body, 200)); + ->willReturn(new RawResponse($headers, $this->successfulMessageNodeResponse(), 200)); $response = $this->whatsapp_app_cloud_api->sendTemplate( $to, @@ -251,8 +240,8 @@ public function test_send_template_without_components() ); $this->assertEquals(200, $response->httpStatusCode()); - $this->assertEquals($body, $response->decodedBody()); - $this->assertEquals($encoded_body, $response->body()); + $this->assertEquals(json_decode($this->successfulMessageNodeResponse(), true), $response->decodedBody()); + $this->assertEquals($this->successfulMessageNodeResponse(), $response->body()); $this->assertEquals(false, $response->isError()); } @@ -344,16 +333,14 @@ public function test_send_template_with_components() ], ], ]; - $encoded_body = json_encode($body); $headers = [ 'Authorization' => 'Bearer ' . $this->access_token, - 'Content-Type' => 'application/json', ]; $this->client_handler - ->send($url, $encoded_body, $headers, Argument::type('int')) + ->postJsonData($url, $body, $headers, Argument::type('int')) ->shouldBeCalled() - ->willReturn(new RawResponse($headers, $encoded_body, 200)); + ->willReturn(new RawResponse($headers, $this->successfulMessageNodeResponse(), 200)); $components = new Component($component_header, $component_body, $component_buttons); $response = $this->whatsapp_app_cloud_api->sendTemplate( @@ -364,8 +351,8 @@ public function test_send_template_with_components() ); $this->assertEquals(200, $response->httpStatusCode()); - $this->assertEquals($body, $response->decodedBody()); - $this->assertEquals($encoded_body, $response->body()); + $this->assertEquals(json_decode($this->successfulMessageNodeResponse(), true), $response->decodedBody()); + $this->assertEquals($this->successfulMessageNodeResponse(), $response->body()); $this->assertEquals(false, $response->isError()); } @@ -384,16 +371,14 @@ public function test_send_audio_id() 'id' => $document_id, ], ]; - $encoded_body = json_encode($body); $headers = [ 'Authorization' => 'Bearer ' . $this->access_token, - 'Content-Type' => 'application/json', ]; $this->client_handler - ->send($url, $encoded_body, $headers, Argument::type('int')) + ->postJsonData($url, $body, $headers, Argument::type('int')) ->shouldBeCalled() - ->willReturn(new RawResponse($headers, $encoded_body, 200)); + ->willReturn(new RawResponse($headers, $this->successfulMessageNodeResponse(), 200)); $media_id = new MediaObjectID($document_id); $response = $this->whatsapp_app_cloud_api->sendAudio( @@ -402,8 +387,8 @@ public function test_send_audio_id() ); $this->assertEquals(200, $response->httpStatusCode()); - $this->assertEquals($body, $response->decodedBody()); - $this->assertEquals($encoded_body, $response->body()); + $this->assertEquals(json_decode($this->successfulMessageNodeResponse(), true), $response->decodedBody()); + $this->assertEquals($this->successfulMessageNodeResponse(), $response->body()); $this->assertEquals(false, $response->isError()); } @@ -422,16 +407,14 @@ public function test_send_audio_link() 'link' => $document_link, ], ]; - $encoded_body = json_encode($body); $headers = [ 'Authorization' => 'Bearer ' . $this->access_token, - 'Content-Type' => 'application/json', ]; $this->client_handler - ->send($url, $encoded_body, $headers, Argument::type('int')) + ->postJsonData($url, $body, $headers, Argument::type('int')) ->shouldBeCalled() - ->willReturn(new RawResponse($headers, $encoded_body, 200)); + ->willReturn(new RawResponse($headers, $this->successfulMessageNodeResponse(), 200)); $link_id = new LinkID($document_link); $response = $this->whatsapp_app_cloud_api->sendAudio( @@ -440,8 +423,8 @@ public function test_send_audio_link() ); $this->assertEquals(200, $response->httpStatusCode()); - $this->assertEquals($body, $response->decodedBody()); - $this->assertEquals($encoded_body, $response->body()); + $this->assertEquals(json_decode($this->successfulMessageNodeResponse(), true), $response->decodedBody()); + $this->assertEquals($this->successfulMessageNodeResponse(), $response->body()); $this->assertEquals(false, $response->isError()); } @@ -462,16 +445,14 @@ public function test_send_image_id() 'id' => $document_id, ], ]; - $encoded_body = json_encode($body); $headers = [ 'Authorization' => 'Bearer ' . $this->access_token, - 'Content-Type' => 'application/json', ]; $this->client_handler - ->send($url, $encoded_body, $headers, Argument::type('int')) + ->postJsonData($url, $body, $headers, Argument::type('int')) ->shouldBeCalled() - ->willReturn(new RawResponse($headers, $encoded_body, 200)); + ->willReturn(new RawResponse($headers, $this->successfulMessageNodeResponse(), 200)); $media_id = new MediaObjectID($document_id); $response = $this->whatsapp_app_cloud_api->sendImage( @@ -481,8 +462,8 @@ public function test_send_image_id() ); $this->assertEquals(200, $response->httpStatusCode()); - $this->assertEquals($body, $response->decodedBody()); - $this->assertEquals($encoded_body, $response->body()); + $this->assertEquals(json_decode($this->successfulMessageNodeResponse(), true), $response->decodedBody()); + $this->assertEquals($this->successfulMessageNodeResponse(), $response->body()); $this->assertEquals(false, $response->isError()); } @@ -503,16 +484,14 @@ public function test_send_image_link() 'link' => $document_link, ], ]; - $encoded_body = json_encode($body); $headers = [ 'Authorization' => 'Bearer ' . $this->access_token, - 'Content-Type' => 'application/json', ]; $this->client_handler - ->send($url, $encoded_body, $headers, Argument::type('int')) + ->postJsonData($url, $body, $headers, Argument::type('int')) ->shouldBeCalled() - ->willReturn(new RawResponse($headers, $encoded_body, 200)); + ->willReturn(new RawResponse($headers, $this->successfulMessageNodeResponse(), 200)); $link_id = new LinkID($document_link); $response = $this->whatsapp_app_cloud_api->sendImage( @@ -522,8 +501,8 @@ public function test_send_image_link() ); $this->assertEquals(200, $response->httpStatusCode()); - $this->assertEquals($body, $response->decodedBody()); - $this->assertEquals($encoded_body, $response->body()); + $this->assertEquals(json_decode($this->successfulMessageNodeResponse(), true), $response->decodedBody()); + $this->assertEquals($this->successfulMessageNodeResponse(), $response->body()); $this->assertEquals(false, $response->isError()); } @@ -544,16 +523,14 @@ public function test_send_video_with_link() 'caption' => $caption, ], ]; - $encoded_body = json_encode($body); $headers = [ 'Authorization' => 'Bearer ' . $this->access_token, - 'Content-Type' => 'application/json', ]; $this->client_handler - ->send($url, $encoded_body, $headers, Argument::type('int')) + ->postJsonData($url, $body, $headers, Argument::type('int')) ->shouldBeCalled() - ->willReturn(new RawResponse($headers, $encoded_body, 200)); + ->willReturn(new RawResponse($headers, $this->successfulMessageNodeResponse(), 200)); $link_id = new LinkID($video_link); $response = $this->whatsapp_app_cloud_api->sendVideo( @@ -563,8 +540,8 @@ public function test_send_video_with_link() ); $this->assertEquals(200, $response->httpStatusCode()); - $this->assertEquals($body, $response->decodedBody()); - $this->assertEquals($encoded_body, $response->body()); + $this->assertEquals(json_decode($this->successfulMessageNodeResponse(), true), $response->decodedBody()); + $this->assertEquals($this->successfulMessageNodeResponse(), $response->body()); $this->assertEquals(false, $response->isError()); } @@ -585,16 +562,14 @@ public function test_send_video_with_id() 'caption' => $caption, ], ]; - $encoded_body = json_encode($body); $headers = [ 'Authorization' => 'Bearer ' . $this->access_token, - 'Content-Type' => 'application/json', ]; $this->client_handler - ->send($url, $encoded_body, $headers, Argument::type('int')) + ->postJsonData($url, $body, $headers, Argument::type('int')) ->shouldBeCalled() - ->willReturn(new RawResponse($headers, $encoded_body, 200)); + ->willReturn(new RawResponse($headers, $this->successfulMessageNodeResponse(), 200)); $media_id = new MediaObjectID($video_id); $response = $this->whatsapp_app_cloud_api->sendVideo( @@ -604,8 +579,8 @@ public function test_send_video_with_id() ); $this->assertEquals(200, $response->httpStatusCode()); - $this->assertEquals($body, $response->decodedBody()); - $this->assertEquals($encoded_body, $response->body()); + $this->assertEquals(json_decode($this->successfulMessageNodeResponse(), true), $response->decodedBody()); + $this->assertEquals($this->successfulMessageNodeResponse(), $response->body()); $this->assertEquals(false, $response->isError()); } @@ -624,16 +599,14 @@ public function test_send_sticker() 'link' => $sticker_link, ], ]; - $encoded_body = json_encode($body); $headers = [ 'Authorization' => 'Bearer ' . $this->access_token, - 'Content-Type' => 'application/json', ]; $this->client_handler - ->send($url, $encoded_body, $headers, Argument::type('int')) + ->postJsonData($url, $body, $headers, Argument::type('int')) ->shouldBeCalled() - ->willReturn(new RawResponse($headers, $encoded_body, 200)); + ->willReturn(new RawResponse($headers, $this->successfulMessageNodeResponse(), 200)); $media_id = new LinkID($sticker_link); $response = $this->whatsapp_app_cloud_api->sendSticker( @@ -642,8 +615,8 @@ public function test_send_sticker() ); $this->assertEquals(200, $response->httpStatusCode()); - $this->assertEquals($body, $response->decodedBody()); - $this->assertEquals($encoded_body, $response->body()); + $this->assertEquals(json_decode($this->successfulMessageNodeResponse(), true), $response->decodedBody()); + $this->assertEquals($this->successfulMessageNodeResponse(), $response->body()); $this->assertEquals(false, $response->isError()); } @@ -668,16 +641,14 @@ public function test_send_location() 'address' => $address, ], ]; - $encoded_body = json_encode($body); $headers = [ 'Authorization' => 'Bearer ' . $this->access_token, - 'Content-Type' => 'application/json', ]; $this->client_handler - ->send($url, $encoded_body, $headers, Argument::type('int')) + ->postJsonData($url, $body, $headers, Argument::type('int')) ->shouldBeCalled() - ->willReturn(new RawResponse($headers, $encoded_body, 200)); + ->willReturn(new RawResponse($headers, $this->successfulMessageNodeResponse(), 200)); $response = $this->whatsapp_app_cloud_api->sendLocation( $to, @@ -688,8 +659,8 @@ public function test_send_location() ); $this->assertEquals(200, $response->httpStatusCode()); - $this->assertEquals($body, $response->decodedBody()); - $this->assertEquals($encoded_body, $response->body()); + $this->assertEquals(json_decode($this->successfulMessageNodeResponse(), true), $response->decodedBody()); + $this->assertEquals($this->successfulMessageNodeResponse(), $response->body()); $this->assertEquals(false, $response->isError()); } @@ -723,16 +694,14 @@ public function test_send_contact() ], ], ]; - $encoded_body = json_encode($body); $headers = [ 'Authorization' => 'Bearer ' . $this->access_token, - 'Content-Type' => 'application/json', ]; $this->client_handler - ->send($url, $encoded_body, $headers, Argument::type('int')) + ->postJsonData($url, $body, $headers, Argument::type('int')) ->shouldBeCalled() - ->willReturn(new RawResponse($headers, $encoded_body, 200)); + ->willReturn(new RawResponse($headers, $this->successfulMessageNodeResponse(), 200)); $contact_name = new ContactName($first_name, $last_name); $response = $this->whatsapp_app_cloud_api->sendContact( @@ -742,8 +711,8 @@ public function test_send_contact() ); $this->assertEquals(200, $response->httpStatusCode()); - $this->assertEquals($body, $response->decodedBody()); - $this->assertEquals($encoded_body, $response->body()); + $this->assertEquals(json_decode($this->successfulMessageNodeResponse(), true), $response->decodedBody()); + $this->assertEquals($this->successfulMessageNodeResponse(), $response->body()); $this->assertEquals(false, $response->isError()); } @@ -778,16 +747,14 @@ public function test_send_contact_with_wa_id() ], ], ]; - $encoded_body = json_encode($body); $headers = [ 'Authorization' => 'Bearer ' . $this->access_token, - 'Content-Type' => 'application/json', ]; $this->client_handler - ->send($url, $encoded_body, $headers, Argument::type('int')) + ->postJsonData($url, $body, $headers, Argument::type('int')) ->shouldBeCalled() - ->willReturn(new RawResponse($headers, $encoded_body, 200)); + ->willReturn(new RawResponse($headers, $this->successfulMessageNodeResponse(), 200)); $contact_name = new ContactName($first_name, $last_name); $response = $this->whatsapp_app_cloud_api->sendContact( @@ -797,8 +764,8 @@ public function test_send_contact_with_wa_id() ); $this->assertEquals(200, $response->httpStatusCode()); - $this->assertEquals($body, $response->decodedBody()); - $this->assertEquals($encoded_body, $response->body()); + $this->assertEquals(json_decode($this->successfulMessageNodeResponse(), true), $response->decodedBody()); + $this->assertEquals($this->successfulMessageNodeResponse(), $response->body()); $this->assertEquals(false, $response->isError()); } @@ -831,16 +798,14 @@ public function test_send_list() 'action' => $listAction, ], ]; - $encoded_body = json_encode($body); $headers = [ 'Authorization' => 'Bearer ' . $this->access_token, - 'Content-Type' => 'application/json', ]; $this->client_handler - ->send($url, $encoded_body, $headers, Argument::type('int')) + ->postJsonData($url, $body, $headers, Argument::type('int')) ->shouldBeCalled() - ->willReturn(new RawResponse($headers, $encoded_body, 200)); + ->willReturn(new RawResponse($headers, $this->successfulMessageNodeResponse(), 200)); $actionSections = []; @@ -863,8 +828,8 @@ public function test_send_list() ); $this->assertEquals(200, $response->httpStatusCode()); - $this->assertEquals($body, $response->decodedBody()); - $this->assertEquals($encoded_body, $response->body()); + $this->assertEquals(json_decode($this->successfulMessageNodeResponse(), true), $response->decodedBody()); + $this->assertEquals($this->successfulMessageNodeResponse(), $response->body()); $this->assertEquals(false, $response->isError()); } @@ -872,4 +837,14 @@ private function buildRequestUri(): string { return Client::BASE_GRAPH_URL . '/' . static::TEST_GRAPH_VERSION . '/' . $this->from_phone_number_id . '/messages'; } + + private function successfulMessageNodeResponse(): string + { + return '{"messaging_product": "whatsapp", "contacts": [{"input": "PHONE_NUMBER", "wa_id": "WHATSAPP_ID"}], "messages": [{"id": "wamid.ID"}]}'; + } + + private function failedMessageResponse(): string + { + return '{"error":{"message":"Invalid OAuth access token - Cannot parse access token","type":"OAuthException","code":190,"fbtrace_id":"AbJuG-rMVv36mjw-r78mKwg"}}'; + } } From 7f874d4f990d987713ab0ab3a6c77d542866a812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Sat, 24 Dec 2022 17:29:05 +0100 Subject: [PATCH 02/20] 2.x: Type hint Response --- src/Response.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Response.php b/src/Response.php index 627d373..1ea8165 100644 --- a/src/Response.php +++ b/src/Response.php @@ -39,7 +39,7 @@ class Response * @param int|null $http_status_code * @param array|null $headers */ - public function __construct(Request $request, $body, $http_status_code = null, array $headers = []) + public function __construct(Request $request, string $body, ?int $http_status_code = null, array $headers = []) { $this->request = $request; $this->body = $body; @@ -54,7 +54,7 @@ public function __construct(Request $request, $body, $http_status_code = null, a * * @return Resquest */ - public function request() + public function request(): Request { return $this->request; } @@ -64,7 +64,7 @@ public function request() * * @return string */ - public function accessToken() + public function accessToken(): string { return $this->request->accessToken(); } @@ -74,7 +74,7 @@ public function accessToken() * * @return int */ - public function httpStatusCode() + public function httpStatusCode(): int { return $this->http_status_code; } @@ -84,7 +84,7 @@ public function httpStatusCode() * * @return array */ - public function headers() + public function headers(): array { return $this->headers; } @@ -94,7 +94,7 @@ public function headers() * * @return string */ - public function body() + public function body(): string { return $this->body; } @@ -104,7 +104,7 @@ public function body() * * @return array */ - public function decodedBody() + public function decodedBody(): array { return $this->decoded_body; } @@ -114,7 +114,7 @@ public function decodedBody() * * @return string|null */ - public function graphVersion() + public function graphVersion(): ?string { return $this->headers['facebook-api-version'] ?? null; } @@ -124,7 +124,7 @@ public function graphVersion() * * @return bool */ - public function isError() + public function isError(): bool { return isset($this->decoded_body['error']); } @@ -150,7 +150,7 @@ public function throwException() * a short-lived access token for a long-lived access token * - And sometimes nothing :/ but that'd be a bug. */ - public function decodeBody() + public function decodeBody(): void { $this->decoded_body = json_decode($this->body, true); From 3468f4a68b33554f5a2235f2f27ba40fac32b6dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Tue, 27 Dec 2022 19:00:03 +0100 Subject: [PATCH 03/20] 2.x: Add pre-commit configuration file --- .pre-commit-config.yaml | 7 +++++++ composer.json | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..fd7053a --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,7 @@ +repos: + - repo: https://github.com/digitalpulp/pre-commit-php.git + rev: 1.4.0 + hooks: + - id: php-cs-fixer + files: \.(php)$ + exclude: ^(vendor) \ No newline at end of file diff --git a/composer.json b/composer.json index 71f3c87..d78d8eb 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,8 @@ "phpunit/phpunit": "^9.0", "symfony/var-dumper": "^5.0", "phpspec/prophecy-phpunit": "^2.0", - "fakerphp/faker": "^1.19" + "fakerphp/faker": "^1.19", + "friendsofphp/php-cs-fixer": "^3.13" }, "autoload": { "psr-4": { From ecd489dd330c0e36db64a4510888f566a224c0b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Fri, 6 Jan 2023 13:21:49 +0100 Subject: [PATCH 04/20] 2.x: Add support to upload media files to Facebook servers --- src/Client.php | 32 ++++++- src/Http/ClientHandler.php | 16 +++- src/Http/GuzzleClientHandler.php | 32 +++++-- .../MediaRequest/UploadMediaRequest.php | 64 ++++++++++++++ src/WhatsAppCloudApi.php | 42 +++++++--- tests/Integration/ClientTest.php | 20 ++++- tests/Integration/WhatsAppCloudApiTest.php | 8 ++ tests/Support/netflie.png | Bin 0 -> 3909 bytes tests/Unit/WhatsAppCloudApiTest.php | 79 ++++++++++++++---- 9 files changed, 255 insertions(+), 38 deletions(-) create mode 100644 src/Request/MediaRequest/UploadMediaRequest.php create mode 100644 tests/Support/netflie.png diff --git a/src/Client.php b/src/Client.php index f628f5c..62470bb 100644 --- a/src/Client.php +++ b/src/Client.php @@ -36,7 +36,7 @@ public function __construct(string $graph_version, ?ClientHandler $handler = nul } /** - * Return the original request that returned this response. + * Send a message request to. * * @return Response Raw response from the server. * @@ -65,6 +65,36 @@ public function sendMessage(Request\MessageRequest $request): Response return $return_response; } + /** + * Upload a media file to Facebook servers. + * + * @return Response Raw response from the server. + * + * @throws Netflie\WhatsAppCloudApi\Response\ResponseException + */ + public function uploadMedia(Request\MediaRequest\UploadMediaRequest $request): Response + { + $raw_response = $this->handler->postFormData( + $this->buildRequestUri($request->nodePath()), + $request->form(), + $request->headers(), + $request->timeout() + ); + + $return_response = new Response( + $request, + $raw_response->body(), + $raw_response->httpResponseCode(), + $raw_response->headers() + ); + + if ($return_response->isError()) { + $return_response->throwException(); + } + + return $return_response; + } + private function defaultHandler(): ClientHandler { return new GuzzleClientHandler(); diff --git a/src/Http/ClientHandler.php b/src/Http/ClientHandler.php index 6ac4033..118cfa5 100644 --- a/src/Http/ClientHandler.php +++ b/src/Http/ClientHandler.php @@ -5,7 +5,7 @@ interface ClientHandler { /** - * Sends a POST request to the server and returns the raw response. + * Sends a JSON POST request to the server and returns the raw response. * * @param string $url The endpoint to send the request to. * @param array $body The body of the request. @@ -17,4 +17,18 @@ interface ClientHandler * @throws Netflie\WhatsAppCloudApi\Response\ResponseException */ public function postJsonData(string $url, array $body, array $headers, int $timeout): RawResponse; + + /** + * Sends a form POST request to the server and returns the raw response. + * + * @param string $url The endpoint to send the request to. + * @param array $form The form data of the request. + * @param array $headers The request headers. + * @param int $timeout The timeout in seconds for the request. + * + * @return RawResponse Response from the server. + * + * @throws Netflie\WhatsAppCloudApi\Response\ResponseException + */ + public function postFormData(string $url, array $form, array $headers, int $timeout): RawResponse; } diff --git a/src/Http/GuzzleClientHandler.php b/src/Http/GuzzleClientHandler.php index 9bfe4cb..7145fce 100644 --- a/src/Http/GuzzleClientHandler.php +++ b/src/Http/GuzzleClientHandler.php @@ -3,6 +3,7 @@ namespace Netflie\WhatsAppCloudApi\Http; use GuzzleHttp\Client; +use Psr\Http\Message\ResponseInterface; class GuzzleClientHandler implements ClientHandler { @@ -25,16 +26,37 @@ public function __construct(?Client $guzzle_client = null) */ public function postJsonData(string $url, array $body, array $headers, int $timeout): RawResponse { - $raw_handler_response = $this->guzzle_client->post($url, [ - 'json' => $body, + $raw_handler_response = $this->post($url, $body, 'json', $headers, $timeout); + + return $this->buildResponse($raw_handler_response); + } + + /** + * {@inheritDoc} + * + */ + public function postFormData(string $url, array $form, array $headers, int $timeout): RawResponse + { + $raw_handler_response = $this->post($url, $form, 'multipart', $headers, $timeout); + + return $this->buildResponse($raw_handler_response); + } + + protected function post(string $url, array $data, string $data_type, array $headers, int $timeout): ResponseInterface + { + return $this->guzzle_client->post($url, [ + $data_type => $data, 'headers' => $headers, 'timeout' => $timeout, ]); + } + protected function buildResponse(ResponseInterface $handler_response): RawResponse + { return new RawResponse( - $raw_handler_response->getHeaders(), - $raw_handler_response->getBody(), - $raw_handler_response->getStatusCode() + $handler_response->getHeaders(), + $handler_response->getBody(), + $handler_response->getStatusCode() ); } } diff --git a/src/Request/MediaRequest/UploadMediaRequest.php b/src/Request/MediaRequest/UploadMediaRequest.php new file mode 100644 index 0000000..aedf506 --- /dev/null +++ b/src/Request/MediaRequest/UploadMediaRequest.php @@ -0,0 +1,64 @@ +file_path = $file_path; + $this->phone_number_id = $phone_number_id; + + parent::__construct($access_token, $timeout); + } + + /** + * Returns the raw form of the request. + * + * @return array + */ + public function form(): array + { + return [ + [ + 'name' => 'file', + 'contents' => Psr7\Utils::tryFopen($this->file_path, 'r'), + ], + [ + 'name' => 'type', + 'contents' => mime_content_type($this->file_path), + ], + [ + 'name' => 'messaging_product', + 'contents' => 'whatsapp', + ], + ]; + } + + /** + * WhatsApp node path. + * + * @return string + */ + public function nodePath(): string + { + return $this->phone_number_id . '/media'; + } +} diff --git a/src/WhatsAppCloudApi.php b/src/WhatsAppCloudApi.php index 1cbe4e4..c606108 100644 --- a/src/WhatsAppCloudApi.php +++ b/src/WhatsAppCloudApi.php @@ -18,7 +18,6 @@ use Netflie\WhatsAppCloudApi\Message\TemplateMessage; use Netflie\WhatsAppCloudApi\Message\TextMessage; use Netflie\WhatsAppCloudApi\Message\VideoMessage; -use Netflie\WhatsAppCloudApi\Request\MessageRequest; class WhatsAppCloudApi { @@ -75,7 +74,7 @@ public function __construct(array $config) public function sendTextMessage(string $to, string $text, bool $preview_url = false): Response { $message = new TextMessage($to, $text, $preview_url); - $request = new MessageRequest\RequestTextMessage( + $request = new Request\MessageRequest\RequestTextMessage( $message, $this->app->accessToken(), $this->app->fromPhoneNumberId(), @@ -98,7 +97,7 @@ public function sendTextMessage(string $to, string $text, bool $preview_url = fa public function sendDocument(string $to, MediaID $document_id, string $name, ?string $caption): Response { $message = new DocumentMessage($to, $document_id, $name, $caption); - $request = new MessageRequest\RequestDocumentMessage( + $request = new Request\MessageRequest\RequestDocumentMessage( $message, $this->app->accessToken(), $this->app->fromPhoneNumberId(), @@ -124,7 +123,7 @@ public function sendDocument(string $to, MediaID $document_id, string $name, ?st public function sendTemplate(string $to, string $template_name, string $language = 'en_US', ?Component $components = null): Response { $message = new TemplateMessage($to, $template_name, $language, $components); - $request = new MessageRequest\RequestTemplateMessage( + $request = new Request\MessageRequest\RequestTemplateMessage( $message, $this->app->accessToken(), $this->app->fromPhoneNumberId(), @@ -147,7 +146,7 @@ public function sendTemplate(string $to, string $template_name, string $language public function sendAudio(string $to, MediaID $document_id): Response { $message = new AudioMessage($to, $document_id); - $request = new MessageRequest\RequestAudioMessage( + $request = new Request\MessageRequest\RequestAudioMessage( $message, $this->app->accessToken(), $this->app->fromPhoneNumberId(), @@ -171,7 +170,7 @@ public function sendAudio(string $to, MediaID $document_id): Response public function sendImage(string $to, MediaID $document_id, ?string $caption = ''): Response { $message = new ImageMessage($to, $document_id, $caption); - $request = new MessageRequest\RequestImageMessage( + $request = new Request\MessageRequest\RequestImageMessage( $message, $this->app->accessToken(), $this->app->fromPhoneNumberId(), @@ -194,7 +193,7 @@ public function sendImage(string $to, MediaID $document_id, ?string $caption = ' public function sendVideo(string $to, MediaID $link, string $caption = ''): Response { $message = new VideoMessage($to, $link, $caption); - $request = new MessageRequest\RequestVideoMessage( + $request = new Request\MessageRequest\RequestVideoMessage( $message, $this->app->accessToken(), $this->app->fromPhoneNumberId(), @@ -217,7 +216,7 @@ public function sendVideo(string $to, MediaID $link, string $caption = ''): Resp public function sendSticker(string $to, MediaID $link): Response { $message = new StickerMessage($to, $link); - $request = new MessageRequest\RequestStickerMessage( + $request = new Request\MessageRequest\RequestStickerMessage( $message, $this->app->accessToken(), $this->app->fromPhoneNumberId(), @@ -243,7 +242,7 @@ public function sendSticker(string $to, MediaID $link): Response public function sendLocation(string $to, float $longitude, float $latitude, string $name = '', string $address = ''): Response { $message = new LocationMessage($to, $longitude, $latitude, $name, $address); - $request = new MessageRequest\RequestLocationMessage( + $request = new Request\MessageRequest\RequestLocationMessage( $message, $this->app->accessToken(), $this->app->fromPhoneNumberId(), @@ -267,7 +266,7 @@ public function sendLocation(string $to, float $longitude, float $latitude, stri public function sendContact(string $to, ContactName $name, Phone ...$phone): Response { $message = new ContactMessage($to, $name, ...$phone); - $request = new MessageRequest\RequestContactMessage( + $request = new Request\MessageRequest\RequestContactMessage( $message, $this->app->accessToken(), $this->app->fromPhoneNumberId(), @@ -280,7 +279,7 @@ public function sendContact(string $to, ContactName $name, Phone ...$phone): Res public function sendList(string $to, string $header, string $body, string $footer, Action $action): Response { $message = new OptionsListMessage($to, $header, $body, $footer, $action); - $request = new MessageRequest\RequestOptionsListMessage( + $request = new Request\MessageRequest\RequestOptionsListMessage( $message, $this->app->accessToken(), $this->app->fromPhoneNumberId(), @@ -290,6 +289,27 @@ public function sendList(string $to, string $header, string $body, string $foote return $this->client->sendMessage($request); } + /** + * Upload a media file (image, audio, video...) to Facebook servers. + * + * @param string $file_path Path of the media file to upload. + * + * @return Response + * + * @throws Response\ResponseException + */ + public function uploadMedia(string $file_path): Response + { + $request = new Request\MediaRequest\UploadMediaRequest( + $file_path, + $this->app->fromPhoneNumberId(), + $this->app->accessToken(), + $this->timeout + ); + + return $this->client->uploadMedia($request); + } + /** * Returns the Facebook Whatsapp Access Token. * diff --git a/tests/Integration/ClientTest.php b/tests/Integration/ClientTest.php index 317aa10..51b86ae 100644 --- a/tests/Integration/ClientTest.php +++ b/tests/Integration/ClientTest.php @@ -4,7 +4,7 @@ use Netflie\WhatsAppCloudApi\Client; use Netflie\WhatsAppCloudApi\Message\TextMessage; -use Netflie\WhatsAppCloudApi\Request\MessageRequest\RequestTextMessage; +use Netflie\WhatsAppCloudApi\Request; use Netflie\WhatsAppCloudApi\Tests\WhatsAppCloudApiTestConfiguration; use Netflie\WhatsAppCloudApi\WhatsAppCloudApi; use PHPUnit\Framework\TestCase; @@ -28,7 +28,7 @@ public function test_send_text_message() 'Hey there! I\'m using WhatsApp Cloud API. Visit https://www.netflie.es', true ); - $request = new RequestTextMessage( + $request = new Request\MessageRequest\RequestTextMessage( $message, WhatsAppCloudApiTestConfiguration::$access_token, WhatsAppCloudApiTestConfiguration::$from_phone_number_id @@ -40,4 +40,20 @@ public function test_send_text_message() $this->assertEquals($request, $response->request()); $this->assertEquals(false, $response->isError()); } + + public function test_upload_media() + { + $request = new Request\MediaRequest\UploadMediaRequest( + 'tests/Support/netflie.png', + WhatsAppCloudApiTestConfiguration::$from_phone_number_id, + WhatsAppCloudApiTestConfiguration::$access_token + ); + + $response = $this->client->uploadMedia($request); + + $this->assertEquals(200, $response->httpStatusCode()); + $this->assertEquals($request, $response->request()); + $this->assertEquals(false, $response->isError()); + $this->assertArrayHasKey('id', $response->decodedBody()); + } } diff --git a/tests/Integration/WhatsAppCloudApiTest.php b/tests/Integration/WhatsAppCloudApiTest.php index 32d4667..0bc2fb7 100644 --- a/tests/Integration/WhatsAppCloudApiTest.php +++ b/tests/Integration/WhatsAppCloudApiTest.php @@ -252,4 +252,12 @@ public function test_send_list() $this->assertEquals(200, $response->httpStatusCode()); $this->assertEquals(false, $response->isError()); } + + public function test_upload_media() + { + $response = $this->whatsapp_app_cloud_api->uploadMedia('tests/Support/netflie.png'); + + $this->assertEquals(200, $response->httpStatusCode()); + $this->assertEquals(false, $response->isError()); + } } diff --git a/tests/Support/netflie.png b/tests/Support/netflie.png new file mode 100644 index 0000000000000000000000000000000000000000..96c45edf2ebbaed6313d81a286245022cbaef890 GIT binary patch literal 3909 zcma)9c{r49+aF`>*_!a!WgezxP?24fC0j;WCm~CdWDJjGX6%%w3?gMoL$amJped%Z zj*=;|JYozJS;8o5ChOpxzVCg%Kfb@->pt%LI*;=@j`O;%-|u&w=Y0{*BW%S*6+{65 zfVjP#wF>~i7sa!~gavtDu7GJ9uM-S#w6zB8|9&2~RNd!!M8fSZMgjn0;NLGF;9=1p zJf~2Uy_1d5Hvz>%+R~TLjfL=B%E4~PD66p0P!#5OQ~n5gaTz7L2Rrk6f z0K5KdhEK!St{)G8<)faZeW(neWx$RrfFvc3o>0jJirx?r$+?@VgxR6L`|zT0BPIyj zxU;yll~6rfk}$qhSe&p?h#ODbi8*8aU$+WkOR~&q^Z64JTG%XjhaEX3EscX#^&8f) z=u5V0uf|&x@`MNz&F7C~^ajkT(2DiJ0|B;k=y~~DFV)0thbj3vpK(7!3ycdUy z`?QSJrL#mze(27<_WcA}(%gG!p=y$n3k$Y%HlEQ{HZo;ARgWR3oE$wcL8R5Cl&36y zVftv2{$ww@zz-p=GZmZpCXYhT>Y z*9)X(>%$7qvKLQ^zXstw$o@AMltPZTlac(v1tx!TPCd_XRkw*VTq#4At9+3UIbRwV zD6WZ=4^dmx)n+_aU*_xO#}M?j2xr+hjX_4aHB~1Qg}B#9jG+e-@oEhAVMD$)Acl5u zRskI?i*#v*gnNs6`n%?xP^3K5%z29^d23qa=U}qdbl%oElDwz(5E~nqV%+(UzRt~k zKKy1DQ8)N<-*lnk&;-;1ZYPQja;Yx$nb92)OPLP1<8Y6zkQNeKVo~=tCudVK`5i@{ z?es!IvGnU={TqXSePMqXE?_4hB+cf@ z+a~?QH(+pZeXdA?^okxdQQ_DrCMbYToTy>bMTeh$2+(mD1!&dm)^$lpwzl4z81-m>uZqPI~@A;1zKHK=I=% z<%40kBF$mbu$zq=7Yo(v^1BK5M-%D6O(IO<&o5^;-;io&bOoFF;w+kn^|vUZn}^q1 zml}sI3Pm2j6xQR>8K|@g`it(5v+eBl>xG2c}r~yE^V_W@9>&%abht zm#?gdTUV2BT%3t)aF<#3AKO_t?@-v^WGqK2RsMO=4+=GRk0GD2$G&AuC1pfl3*`C6 z1-P;g6ocUdV!uWdNS7>C>8-VUhEiW;j381W4~>zhmI~f0$ohq$`ER!bh}$TAf2m)f zTY9@yc3e3As1!fz7dHkG535!w?Gvc2+}L`nM4a2#4%eW{f6zvL9Q5rGOtegjbHS3+ z^pBiB*7vIpc@tPu)wc1b<{=~Kwo9AmGATmzdGeA?zU~B^`M#^)uC@0`AQ5h6K7fIQ z9}PFBidel$N5-q3jOn;E$F;zUWu z_`81PQhJ~&cPw6L(^8Gmqs$a7@n>|H!!=esAkCkRCD8J@ICt);NvjT}MUcun?lt;;Usn!!!Q-OL5nYREgCopE;>k}`XMpHR9q-N+GxKhSeXq5Yf*YDj!^TbFNKdanI2#smbyRr#GG~dC^)tb!pYhI zh5#v{$;X5OqE}p?9)s1l?=~>6mtxKy!OCRU-`+;^^G@t;y3DeP5;2LGu4~JN&?1ZR zPp4()MWNWo;QNym=B)F)U~Q17G`VH3OqtJh_fI7krZ8p6gJ(we*d5I=F7gQ8Uh{NI zoqr>O=AyC=qq6#2Se45(o$dTZER>r4e{r_m&a$PBjYk9bhTm65lWad<7?SmdQzYME zWS$r?@`8 z+>xHkM=XAgsmdqt0>Mz0$8LBwhiI;BohCa3A{K6L%FL$6X7W2jwA?(sZ{6QUTk%l3 znbqH(j!PINv`Oy^60*z~et_LiGgSkr!$*dKEt9Uj4Iz7XrTXUTtz_U53hPT|o$%Sj z6@^L@C$^*${!X6b){{dgEN|miYoUCq5mV=vgj+sh-9s7Ms=oGb9h; zNYvqnj~M8?4lO6X0=u3PeFph8a;qBl%w5}~IYh-k|MP`e;ayb(Iv)(S=k4=1xjc>i zR)7ih@YxXt5Dq+1w^VNj`(Filw!_C8Ak^;yUCO0ac+(o=%}NIwmkTAKkEu`&a<$gb zpXgYiUH>TrWTYh(B32hsGB|vEW2BRpNV^|J%p37pA%;flA@cUEPt6J0+deU8Bw|S{dy7k>bNoQP=Lo61Sw@dzKui*%4y)5bP5gTKcFi zJm7|el_y}_DIZiE0C_O22f!;@GANZ5%)%Q4eb#oa$(94gPb z#~cd>%|8M33XV&q6@%g>Uu#OKcC6D3@!xK5cuz!3h@$IfG`{(gJ6q?YB0(#^PM`Z; z(~z*vD8KyX9VYNILbFe{|5?YwnWT?v3r&(_1&cX~r#GeD!s?WGx3IE`&UjvzK21D@ z`A4`2^~Phna#{>c54gHTQ%ONuG)H&WY5L3u$e)AnEPM}~?HfrzkN%P$fxgJxeUUkT zv6GXD-p|R@!#&Xd`9mhmyewU#=WoZtepe&8O&{_Fg85PhmcLeDJ$jTAcxl3Aq5(k^ zTRry2#&<~s`p^!2SbMr>u+iMq;GPd~#MN^3z$4@0e=@9AO%$oAD)4c4ss;^(;I7i@ zl5r=Pj|e+~5E7)=V86YMNL6i1>)7DOl)c*Zsr2<43L4fbO2X%(ggQshU{@08&EF@G zq(vOJ0{cl^@^$Y7qfl1Eh$?Re17Tpd4+MZzokEzryQtknM+9#K7kl3;jr9qx_SMGH|h|mRe8fCmdg0 zd)GnqbUjOM6Sg8SVM2cQ^@dOK0yna!V-{Q@&U|x+7Bi)` zAAk!1#6jDTZM0xh$0+wQqq$L^mlvz0O?YYhM;Y?nJ+1d=73aW7s?>K&SGA|I7)qBu zHyJU)Ma_kjGD^sC7v67Qo&W0M)gSaytI3ct>%Y~7%xJVXp1J2m#(R$F&}PMkI~8QF zoT7q4FOVk>h0;9g4{-93*K1Z2uxm!1^wB7^W)qC@1j7?DvnpPOSFPNUKr>0CN{G?p z692W&1)T~K>^|llYl}?%+|KJ!C1dl=8r^F9D6)@)=MuL@W4Lp4$cd+GAzrF+M`qOk zjpd0US_8kBMI3lCYxl|DTTp}axD)xz7V+CnN6UUz;8QRRbys&e}9#SxKR6LNmnl^^y^D)D7Dv~2dCbGTf1NI{8Qs2KHUpw4>Dq+w#@ zFl>UwBkgWAkElM&gsDF2Ko@TD+o ziFK~;Kcqyi`O2DU@(z%qu21djZ4Cc7hReDtG&@~%Z|+Oc{jd*_j5%Y&EWN!w{U07G z^*e@%19b1WghBsNt45ZXkuYmNDhYk_hcabApqyzo>xF(Z5uruc72J_>YUuU7McgKj zeTsm{{8~*~EvRq66 literal 0 HcmV?d00001 diff --git a/tests/Unit/WhatsAppCloudApiTest.php b/tests/Unit/WhatsAppCloudApiTest.php index 219166c..3389461 100644 --- a/tests/Unit/WhatsAppCloudApiTest.php +++ b/tests/Unit/WhatsAppCloudApiTest.php @@ -2,6 +2,7 @@ namespace Netflie\WhatsAppCloudApi\Tests\Unit; +use GuzzleHttp\Psr7; use Netflie\WhatsAppCloudApi\Client; use Netflie\WhatsAppCloudApi\Http\ClientHandler; use Netflie\WhatsAppCloudApi\Http\RawResponse; @@ -53,7 +54,7 @@ public function setUp(): void public function test_send_text_message_fails() { $to = $this->faker->phoneNumber; - $url = $this->buildRequestUri(); + $url = $this->buildMessageRequestUri(); $text_message = $this->faker->text; $preview_url = $this->faker->boolean; @@ -87,7 +88,7 @@ public function test_send_text_message_fails() public function test_send_text_message() { $to = $this->faker->phoneNumber; - $url = $this->buildRequestUri(); + $url = $this->buildMessageRequestUri(); $text_message = $this->faker->text; $preview_url = $this->faker->boolean; @@ -125,7 +126,7 @@ public function test_send_text_message() public function test_send_document_id() { $to = $this->faker->phoneNumber; - $url = $this->buildRequestUri(); + $url = $this->buildMessageRequestUri(); $caption = $this->faker->text; $filename = $this->faker->text; $document_id = $this->faker->uuid; @@ -167,7 +168,7 @@ public function test_send_document_id() public function test_send_document_link() { $to = $this->faker->phoneNumber; - $url = $this->buildRequestUri(); + $url = $this->buildMessageRequestUri(); $caption = $this->faker->text; $filename = $this->faker->text; $document_link = $this->faker->url; @@ -209,7 +210,7 @@ public function test_send_document_link() public function test_send_template_without_components() { $to = $this->faker->phoneNumber; - $url = $this->buildRequestUri(); + $url = $this->buildMessageRequestUri(); $template_name = $this->faker->name; $language = $this->faker->locale; @@ -248,7 +249,7 @@ public function test_send_template_without_components() public function test_send_template_with_components() { $to = $this->faker->phoneNumber; - $url = $this->buildRequestUri(); + $url = $this->buildMessageRequestUri(); $template_name = $this->faker->name; $language = $this->faker->locale; @@ -359,7 +360,7 @@ public function test_send_template_with_components() public function test_send_audio_id() { $to = $this->faker->phoneNumber; - $url = $this->buildRequestUri(); + $url = $this->buildMessageRequestUri(); $document_id = $this->faker->uuid; $body = [ @@ -395,7 +396,7 @@ public function test_send_audio_id() public function test_send_audio_link() { $to = $this->faker->phoneNumber; - $url = $this->buildRequestUri(); + $url = $this->buildMessageRequestUri(); $document_link = $this->faker->url; $body = [ @@ -431,7 +432,7 @@ public function test_send_audio_link() public function test_send_image_id() { $to = $this->faker->phoneNumber; - $url = $this->buildRequestUri(); + $url = $this->buildMessageRequestUri(); $caption = $this->faker->text; $document_id = $this->faker->uuid; @@ -470,7 +471,7 @@ public function test_send_image_id() public function test_send_image_link() { $to = $this->faker->phoneNumber; - $url = $this->buildRequestUri(); + $url = $this->buildMessageRequestUri(); $caption = $this->faker->text; $document_link = $this->faker->url; @@ -509,7 +510,7 @@ public function test_send_image_link() public function test_send_video_with_link() { $to = $this->faker->phoneNumber; - $url = $this->buildRequestUri(); + $url = $this->buildMessageRequestUri(); $video_link = $this->faker->url; $caption = $this->faker->text; @@ -548,7 +549,7 @@ public function test_send_video_with_link() public function test_send_video_with_id() { $to = $this->faker->phoneNumber; - $url = $this->buildRequestUri(); + $url = $this->buildMessageRequestUri(); $video_id = $this->faker->uuid; $caption = $this->faker->text; @@ -587,7 +588,7 @@ public function test_send_video_with_id() public function test_send_sticker() { $to = $this->faker->phoneNumber; - $url = $this->buildRequestUri(); + $url = $this->buildMessageRequestUri(); $sticker_link = $this->faker->url; $body = [ @@ -623,7 +624,7 @@ public function test_send_sticker() public function test_send_location() { $to = $this->faker->phoneNumber; - $url = $this->buildRequestUri(); + $url = $this->buildMessageRequestUri(); $latitude = $this->faker->latitude; $longitude = $this->faker->latitude; $name = $this->faker->city; @@ -667,7 +668,7 @@ public function test_send_location() public function test_send_contact() { $to = $this->faker->phoneNumber; - $url = $this->buildRequestUri(); + $url = $this->buildMessageRequestUri(); $first_name = $this->faker->firstName(); $last_name = $this->faker->lastName; $phone = $this->faker->e164PhoneNumber; @@ -719,7 +720,7 @@ public function test_send_contact() public function test_send_contact_with_wa_id() { $to = $this->faker->phoneNumber; - $url = $this->buildRequestUri(); + $url = $this->buildMessageRequestUri(); $first_name = $this->faker->firstName(); $last_name = $this->faker->lastName; $phone = $this->faker->e164PhoneNumber; @@ -772,7 +773,7 @@ public function test_send_contact_with_wa_id() public function test_send_list() { $to = $this->faker->phoneNumber; - $url = $this->buildRequestUri(); + $url = $this->buildMessageRequestUri(); $listHeader = ['type' => 'text', 'text' => $this->faker->text(60)]; $listBody = ['text' => $this->faker->text(1024)]; @@ -833,11 +834,53 @@ public function test_send_list() $this->assertEquals(false, $response->isError()); } - private function buildRequestUri(): string + public function test_upload_media() + { + $url = $this->buildMediaRequestUri(); + $form = [ + [ + 'name' => 'file', + 'contents' => Psr7\Utils::tryFopen('tests/Support/netflie.png', 'r'), + ], + [ + 'name' => 'type', + 'contents' => 'image/png', + ], + [ + 'name' => 'messaging_product', + 'contents' => 'whatsapp', + ], + ]; + $headers = [ + 'Authorization' => 'Bearer ' . $this->access_token, + ]; + $response_body = '{"id":""}'; + + $this->client_handler + ->postFormData($url, Argument::that(function ($arg) use ($form) { + return json_encode($arg) == json_encode($form); + }), $headers, Argument::type('int')) + ->shouldBeCalled() + ->willReturn(new RawResponse($headers, $response_body, 200)); + + $response = $this->whatsapp_app_cloud_api->uploadMedia('tests/Support/netflie.png'); + + $this->assertEquals(200, $response->httpStatusCode()); + $this->assertEquals(json_decode($response_body, true), $response->decodedBody()); + $this->assertEquals($response_body, $response->body()); + $this->assertEquals(false, $response->isError()); + } + + private function buildMessageRequestUri(): string { return Client::BASE_GRAPH_URL . '/' . static::TEST_GRAPH_VERSION . '/' . $this->from_phone_number_id . '/messages'; } + private function buildMediaRequestUri(): string + { + return Client::BASE_GRAPH_URL . '/' . static::TEST_GRAPH_VERSION . '/' . $this->from_phone_number_id . '/media'; + } + private function successfulMessageNodeResponse(): string { return '{"messaging_product": "whatsapp", "contacts": [{"input": "PHONE_NUMBER", "wa_id": "WHATSAPP_ID"}], "messages": [{"id": "wamid.ID"}]}'; From 97b4f34dd1839fe5a24813996c81cac4a3489a0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Fri, 6 Jan 2023 16:03:21 +0100 Subject: [PATCH 05/20] 2.x: Add support to download media files from Facebook servers --- src/Client.php | 33 ++++++++++++++ src/Http/ClientHandler.php | 13 ++++++ src/Http/GuzzleClientHandler.php | 14 ++++++ .../MediaRequest/DownloadMediaRequest.php | 44 +++++++++++++++++++ src/Response.php | 32 +++++--------- src/WhatsAppCloudApi.php | 20 +++++++++ tests/Integration/ClientTest.php | 19 ++++++++ tests/Integration/WhatsAppCloudApiTest.php | 13 ++++++ tests/Unit/WhatsAppCloudApiTest.php | 36 ++++++++++++++- 9 files changed, 202 insertions(+), 22 deletions(-) create mode 100644 src/Request/MediaRequest/DownloadMediaRequest.php diff --git a/src/Client.php b/src/Client.php index 62470bb..368a94d 100644 --- a/src/Client.php +++ b/src/Client.php @@ -95,6 +95,39 @@ public function uploadMedia(Request\MediaRequest\UploadMediaRequest $request): R return $return_response; } + /** + * Download a media file from Facebook servers. + * + * @return Response Raw response from the server. + * + * @throws Netflie\WhatsAppCloudApi\Response\ResponseException + */ + public function downloadMedia(Request\MediaRequest\DownloadMediaRequest $request): Response + { + $raw_response = $this->handler->get( + $this->buildRequestUri($request->nodePath()), + $request->headers(), + $request->timeout() + ); + + $response = Response::fromClientResponse($request, $raw_response); + $media_url = $response->decodedBody()['url']; + + $raw_response = $this->handler->get( + $media_url, + $request->headers(), + $request->timeout() + ); + + $return_response = Response::fromClientResponse($request, $raw_response); + + if ($return_response->isError()) { + $return_response->throwException(); + } + + return $return_response; + } + private function defaultHandler(): ClientHandler { return new GuzzleClientHandler(); diff --git a/src/Http/ClientHandler.php b/src/Http/ClientHandler.php index 118cfa5..36b5741 100644 --- a/src/Http/ClientHandler.php +++ b/src/Http/ClientHandler.php @@ -31,4 +31,17 @@ public function postJsonData(string $url, array $body, array $headers, int $time * @throws Netflie\WhatsAppCloudApi\Response\ResponseException */ public function postFormData(string $url, array $form, array $headers, int $timeout): RawResponse; + + /** + * Sends a GET request to the server and returns the raw response. + * + * @param string $url The endpoint to send the request to. + * @param array $headers The request headers. + * @param int $timeout The timeout in seconds for the request. + * + * @return RawResponse Response from the server. + * + * @throws Netflie\WhatsAppCloudApi\Response\ResponseException + */ + public function get(string $url, array $headers, int $timeout): RawResponse; } diff --git a/src/Http/GuzzleClientHandler.php b/src/Http/GuzzleClientHandler.php index 7145fce..ee847bd 100644 --- a/src/Http/GuzzleClientHandler.php +++ b/src/Http/GuzzleClientHandler.php @@ -42,6 +42,20 @@ public function postFormData(string $url, array $form, array $headers, int $time return $this->buildResponse($raw_handler_response); } + /** + * {@inheritDoc} + * + */ + public function get(string $url, array $headers, int $timeout): RawResponse + { + $raw_handler_response = $this->guzzle_client->get($url, [ + 'headers' => $headers, + 'timeout' => $timeout, + ]); + + return $this->buildResponse($raw_handler_response); + } + protected function post(string $url, array $data, string $data_type, array $headers, int $timeout): ResponseInterface { return $this->guzzle_client->post($url, [ diff --git a/src/Request/MediaRequest/DownloadMediaRequest.php b/src/Request/MediaRequest/DownloadMediaRequest.php new file mode 100644 index 0000000..82cad5a --- /dev/null +++ b/src/Request/MediaRequest/DownloadMediaRequest.php @@ -0,0 +1,44 @@ +media_id = $media_id; + + parent::__construct($access_token, $timeout); + } + + /** + * Media Identifier (Id). + * + * @return string + */ + public function mediaId(): string + { + return $this->media_id; + } + + /** + * WhatsApp node path. + * + * @return string + */ + public function nodePath(): string + { + return $this->media_id; + } +} diff --git a/src/Response.php b/src/Response.php index 1ea8165..1ea7996 100644 --- a/src/Response.php +++ b/src/Response.php @@ -2,6 +2,7 @@ namespace Netflie\WhatsAppCloudApi; +use Netflie\WhatsAppCloudApi\Http\RawResponse; use Netflie\WhatsAppCloudApi\Response\ResponseException; class Response @@ -49,6 +50,16 @@ public function __construct(Request $request, string $body, ?int $http_status_co $this->decodeBody(); } + public static function fromClientResponse(Request $request, RawResponse $response): static + { + return new static( + $request, + $response->body(), + $response->httpResponseCode(), + $response->headers() + ); + } + /** * Return the original request that returned this response. * @@ -141,28 +152,9 @@ public function throwException() /** * Convert the raw response into an array if possible. - * - * Graph will return 2 types of responses: - * - JSON(P) - * Most responses from Graph are JSON(P) - * - application/x-www-form-urlencoded key/value pairs - * Happens on the `/oauth/access_token` endpoint when exchanging - * a short-lived access token for a long-lived access token - * - And sometimes nothing :/ but that'd be a bug. */ public function decodeBody(): void { - $this->decoded_body = json_decode($this->body, true); - - if ($this->decoded_body === null) { - $this->decoded_body = []; - parse_str($this->body, $this->decoded_body); - } elseif (is_numeric($this->decoded_body)) { - $this->decoded_body = ['id' => $this->decoded_body]; - } - - if (!is_array($this->decoded_body)) { - $this->decoded_body = []; - } + $this->decoded_body = json_decode($this->body, true) ?? []; } } diff --git a/src/WhatsAppCloudApi.php b/src/WhatsAppCloudApi.php index c606108..450892d 100644 --- a/src/WhatsAppCloudApi.php +++ b/src/WhatsAppCloudApi.php @@ -310,6 +310,26 @@ public function uploadMedia(string $file_path): Response return $this->client->uploadMedia($request); } + /** + * Download a media file (image, audio, video...) from Facebook servers. + * + * @param string $media_id Id of the media uploaded with the `uploadMedia` method + * + * @return Response + * + * @throws Response\ResponseException + */ + public function downloadMedia(string $media_id): Response + { + $request = new Request\MediaRequest\DownloadMediaRequest( + $media_id, + $this->app->accessToken(), + $this->timeout + ); + + return $this->client->downloadMedia($request); + } + /** * Returns the Facebook Whatsapp Access Token. * diff --git a/tests/Integration/ClientTest.php b/tests/Integration/ClientTest.php index 51b86ae..79d656f 100644 --- a/tests/Integration/ClientTest.php +++ b/tests/Integration/ClientTest.php @@ -55,5 +55,24 @@ public function test_upload_media() $this->assertEquals($request, $response->request()); $this->assertEquals(false, $response->isError()); $this->assertArrayHasKey('id', $response->decodedBody()); + + return $response->decodedBody()['id']; + } + + /** + * @depends test_upload_media + */ + public function test_download_media(string $media_id) + { + $request = new Request\MediaRequest\DownloadMediaRequest( + $media_id, + WhatsAppCloudApiTestConfiguration::$access_token + ); + + $response = $this->client->downloadMedia($request); + + $this->assertEquals(200, $response->httpStatusCode()); + $this->assertEquals($request, $response->request()); + $this->assertEquals(false, $response->isError()); } } diff --git a/tests/Integration/WhatsAppCloudApiTest.php b/tests/Integration/WhatsAppCloudApiTest.php index 0bc2fb7..0d7e98f 100644 --- a/tests/Integration/WhatsAppCloudApiTest.php +++ b/tests/Integration/WhatsAppCloudApiTest.php @@ -259,5 +259,18 @@ public function test_upload_media() $this->assertEquals(200, $response->httpStatusCode()); $this->assertEquals(false, $response->isError()); + + return $response->decodedBody()['id']; + } + + /** + * @depends test_upload_media + */ + public function test_download_media(string $media_id) + { + $response = $this->whatsapp_app_cloud_api->downloadMedia($media_id); + + $this->assertEquals(200, $response->httpStatusCode()); + $this->assertEquals(false, $response->isError()); } } diff --git a/tests/Unit/WhatsAppCloudApiTest.php b/tests/Unit/WhatsAppCloudApiTest.php index 3389461..c09cf9c 100644 --- a/tests/Unit/WhatsAppCloudApiTest.php +++ b/tests/Unit/WhatsAppCloudApiTest.php @@ -871,14 +871,46 @@ public function test_upload_media() $this->assertEquals(false, $response->isError()); } + public function test_download_media() + { + $media_id = (string) $this->faker->randomNumber; + $url = $this->buildBaseUri() . $media_id; + $headers = [ + 'Authorization' => 'Bearer ' . $this->access_token, + ]; + $media_url_response_body = '{"url": ""}'; + $binary_media_response_body = $this->faker->text; + + $this->client_handler + ->get($url, $headers, Argument::type('int')) + ->shouldBeCalled() + ->willReturn(new RawResponse($headers, $media_url_response_body, 200)); + $this->client_handler + ->get('', $headers, Argument::type('int')) + ->shouldBeCalled() + ->willReturn(new RawResponse($headers, $binary_media_response_body, 200)); + + $response = $this->whatsapp_app_cloud_api->downloadMedia($media_id); + + $this->assertEquals(200, $response->httpStatusCode()); + $this->assertEquals([], $response->decodedBody()); + $this->assertEquals($binary_media_response_body, $response->body()); + $this->assertEquals(false, $response->isError()); + } + + private function buildBaseUri(): string + { + return Client::BASE_GRAPH_URL . '/' . static::TEST_GRAPH_VERSION . '/'; + } + private function buildMessageRequestUri(): string { - return Client::BASE_GRAPH_URL . '/' . static::TEST_GRAPH_VERSION . '/' . $this->from_phone_number_id . '/messages'; + return $this->buildBaseUri() . $this->from_phone_number_id . '/messages'; } private function buildMediaRequestUri(): string { - return Client::BASE_GRAPH_URL . '/' . static::TEST_GRAPH_VERSION . '/' . $this->from_phone_number_id . '/media'; + return $this->buildBaseUri() . $this->from_phone_number_id . '/media'; } private function successfulMessageNodeResponse(): string From 0e5239c6fc25cb21de0a46bc8044189590242229 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Sat, 28 Jan 2023 12:39:34 +0100 Subject: [PATCH 06/20] 2.x: fix returned type hint --- src/Response.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Response.php b/src/Response.php index 1ea7996..c32d241 100644 --- a/src/Response.php +++ b/src/Response.php @@ -50,7 +50,7 @@ public function __construct(Request $request, string $body, ?int $http_status_co $this->decodeBody(); } - public static function fromClientResponse(Request $request, RawResponse $response): static + public static function fromClientResponse(Request $request, RawResponse $response): self { return new static( $request, From 1219b01204e6368ad6fe2d3b89be731083256be0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Sat, 28 Jan 2023 17:17:16 +0100 Subject: [PATCH 07/20] 2.x: disable Guzzle HTTP exceptions --- README.md | 15 +++++++++++++++ src/Http/GuzzleClientHandler.php | 2 ++ 2 files changed, 17 insertions(+) diff --git a/README.md b/README.md index aaad824..c90312b 100644 --- a/README.md +++ b/README.md @@ -222,6 +222,21 @@ $whatsapp_cloud_api->sendList( ); ``` +## Message Response +WhatsAppCloudApi instance returns a Response class or a ClientException if WhatsApp servers return an error. + +```php +try { + $response = $this->whatsapp_app_cloud_api->sendTextMessage( + ', + 'Hey there! I\'m using WhatsApp Cloud API. Visit https://www.netflie.es', + true + ); +} catch (\Netflie\WhatsAppCloudApi\Response\ResponseException $e) { + print_r($e->response()); // You can still check the Response returned from Meta servers +} +``` + ## Features - Send Text Messages diff --git a/src/Http/GuzzleClientHandler.php b/src/Http/GuzzleClientHandler.php index ee847bd..50796a7 100644 --- a/src/Http/GuzzleClientHandler.php +++ b/src/Http/GuzzleClientHandler.php @@ -51,6 +51,7 @@ public function get(string $url, array $headers, int $timeout): RawResponse $raw_handler_response = $this->guzzle_client->get($url, [ 'headers' => $headers, 'timeout' => $timeout, + 'http_errors' => false, ]); return $this->buildResponse($raw_handler_response); @@ -62,6 +63,7 @@ protected function post(string $url, array $data, string $data_type, array $head $data_type => $data, 'headers' => $headers, 'timeout' => $timeout, + 'http_errors' => false, ]); } From 4cf094b1ff9a477eda34151a0e68fc7417950bbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Sat, 28 Jan 2023 18:06:37 +0100 Subject: [PATCH 08/20] 2.x: a lot of classes are now final classes and it protected properties have been declared as privated --- src/Http/GuzzleClientHandler.php | 4 ++-- src/Http/RawResponse.php | 8 ++++---- src/Message/AudioMessage.php | 4 ++-- src/Message/Contact/ContactName.php | 6 +++--- src/Message/Contact/Phone.php | 8 ++++---- src/Message/Contact/PhoneType.php | 2 +- src/Message/Contact/Phones.php | 2 +- src/Message/ContactMessage.php | 6 +++--- src/Message/DocumentMessage.php | 8 ++++---- src/Message/Error/InvalidMessage.php | 2 +- src/Message/ImageMessage.php | 6 +++--- src/Message/LocationMessage.php | 10 +++++----- src/Message/Media/LinkID.php | 2 +- src/Message/Media/MediaID.php | 2 +- src/Message/Media/MediaObjectID.php | 2 +- src/Message/Message.php | 6 +++--- src/Message/OptionsListMessage.php | 10 +++++----- src/Message/StickerMessage.php | 4 ++-- src/Message/Template/Component.php | 8 ++++---- src/Message/TemplateMessage.php | 8 ++++---- src/Message/TextMessage.php | 12 ++++++------ src/Message/VideoMessage.php | 6 +++--- src/Request.php | 4 ++-- src/Request/MediaRequest/DownloadMediaRequest.php | 4 ++-- src/Request/MediaRequest/UploadMediaRequest.php | 6 +++--- src/Request/MessageRequest.php | 2 +- src/Request/MessageRequest/RequestAudioMessage.php | 2 +- src/Request/MessageRequest/RequestContactMessage.php | 2 +- .../MessageRequest/RequestDocumentMessage.php | 2 +- src/Request/MessageRequest/RequestImageMessage.php | 2 +- .../MessageRequest/RequestLocationMessage.php | 2 +- .../MessageRequest/RequestOptionsListMessage.php | 6 +----- src/Request/MessageRequest/RequestStickerMessage.php | 2 +- .../MessageRequest/RequestTemplateMessage.php | 2 +- src/Request/MessageRequest/RequestTextMessage.php | 2 +- src/Request/MessageRequest/RequestVideoMessage.php | 2 +- src/Response/ResponseException.php | 6 +++--- 37 files changed, 84 insertions(+), 88 deletions(-) diff --git a/src/Http/GuzzleClientHandler.php b/src/Http/GuzzleClientHandler.php index 50796a7..58bcc76 100644 --- a/src/Http/GuzzleClientHandler.php +++ b/src/Http/GuzzleClientHandler.php @@ -5,12 +5,12 @@ use GuzzleHttp\Client; use Psr\Http\Message\ResponseInterface; -class GuzzleClientHandler implements ClientHandler +final class GuzzleClientHandler implements ClientHandler { /** * @var \GuzzleHttp\Client The Guzzle client. */ - protected $guzzle_client; + private $guzzle_client; /** * @param \GuzzleHttp\Client|null The Guzzle client. diff --git a/src/Http/RawResponse.php b/src/Http/RawResponse.php index ed7bebc..72aec64 100644 --- a/src/Http/RawResponse.php +++ b/src/Http/RawResponse.php @@ -2,22 +2,22 @@ namespace Netflie\WhatsAppCloudApi\Http; -class RawResponse +final class RawResponse { /** * @var array The response headers in the form of an associative array. */ - protected array $headers; + private array $headers; /** * @var string The raw response body. */ - protected string $body; + private string $body; /** * @var int The HTTP status response code. */ - protected $http_response_code; + private $http_response_code; /** * Creates a new GraphRawResponse entity. diff --git a/src/Message/AudioMessage.php b/src/Message/AudioMessage.php index 6abfdbc..4fa7986 100644 --- a/src/Message/AudioMessage.php +++ b/src/Message/AudioMessage.php @@ -4,7 +4,7 @@ use Netflie\WhatsAppCloudApi\Message\Media\MediaID; -class AudioMessage extends Message +final class AudioMessage extends Message { /** * {@inheritdoc} @@ -16,7 +16,7 @@ class AudioMessage extends Message * * You can get a WhatsApp Media ID uploading the document to the WhatsApp Cloud servers. */ - protected MediaID $id; + private MediaID $id; /** * {@inheritdoc} diff --git a/src/Message/Contact/ContactName.php b/src/Message/Contact/ContactName.php index 5c3988c..d26f8b8 100644 --- a/src/Message/Contact/ContactName.php +++ b/src/Message/Contact/ContactName.php @@ -2,11 +2,11 @@ namespace Netflie\WhatsAppCloudApi\Message\Contact; -class ContactName +final class ContactName { - protected string $first_name; + private string $first_name; - protected string $last_name; + private string $last_name; public function __construct(string $first_name, string $last_name = '') { diff --git a/src/Message/Contact/Phone.php b/src/Message/Contact/Phone.php index 5ddd435..7a32387 100644 --- a/src/Message/Contact/Phone.php +++ b/src/Message/Contact/Phone.php @@ -2,13 +2,13 @@ namespace Netflie\WhatsAppCloudApi\Message\Contact; -class Phone +final class Phone { - protected string $number; + private string $number; - protected string $wa_id; + private string $wa_id; - protected PhoneType $type; + private PhoneType $type; public function __construct(string $number, PhoneType $type, string $wa_id = '') { diff --git a/src/Message/Contact/PhoneType.php b/src/Message/Contact/PhoneType.php index 68f8176..63a355e 100644 --- a/src/Message/Contact/PhoneType.php +++ b/src/Message/Contact/PhoneType.php @@ -4,7 +4,7 @@ use MyCLabs\Enum\Enum; -class PhoneType extends Enum +final class PhoneType extends Enum { private const CELL = 'cell'; private const MAIN = 'main'; diff --git a/src/Message/Contact/Phones.php b/src/Message/Contact/Phones.php index e5c498b..29b366e 100644 --- a/src/Message/Contact/Phones.php +++ b/src/Message/Contact/Phones.php @@ -2,7 +2,7 @@ namespace Netflie\WhatsAppCloudApi\Message\Contact; -class Phones implements \Countable, \IteratorAggregate +final class Phones implements \Countable, \IteratorAggregate { private array $phones; diff --git a/src/Message/ContactMessage.php b/src/Message/ContactMessage.php index 9083f6a..8e8873e 100644 --- a/src/Message/ContactMessage.php +++ b/src/Message/ContactMessage.php @@ -6,16 +6,16 @@ use Netflie\WhatsAppCloudApi\Message\Contact\Phone; use Netflie\WhatsAppCloudApi\Message\Contact\Phones; -class ContactMessage extends Message +final class ContactMessage extends Message { /** * {@inheritdoc} */ protected string $type = 'contacts'; - protected ContactName $name; + private ContactName $name; - protected Phones $phones; + private Phones $phones; /** * {@inheritdoc} diff --git a/src/Message/DocumentMessage.php b/src/Message/DocumentMessage.php index 87ad178..fc40cdd 100644 --- a/src/Message/DocumentMessage.php +++ b/src/Message/DocumentMessage.php @@ -4,7 +4,7 @@ use Netflie\WhatsAppCloudApi\Message\Media\MediaID; -class DocumentMessage extends Message +final class DocumentMessage extends Message { /** * {@inheritdoc} @@ -16,17 +16,17 @@ class DocumentMessage extends Message * * You can get a WhatsApp Media ID uploading the document to the WhatsApp Cloud servers. */ - protected MediaID $id; + private MediaID $id; /** * Describes the filename for the specific document: eg. my-document.pdf. */ - protected string $name; + private string $name; /** * Describes the specified document. */ - protected ?string $caption; + private ?string $caption; /** * {@inheritdoc} diff --git a/src/Message/Error/InvalidMessage.php b/src/Message/Error/InvalidMessage.php index 972d8d1..4ba9285 100644 --- a/src/Message/Error/InvalidMessage.php +++ b/src/Message/Error/InvalidMessage.php @@ -2,6 +2,6 @@ namespace Netflie\WhatsAppCloudApi\Message\Error; -class InvalidMessage extends \Exception +final class InvalidMessage extends \Exception { } diff --git a/src/Message/ImageMessage.php b/src/Message/ImageMessage.php index 3737bca..79a2a14 100644 --- a/src/Message/ImageMessage.php +++ b/src/Message/ImageMessage.php @@ -4,7 +4,7 @@ use Netflie\WhatsAppCloudApi\Message\Media\MediaID; -class ImageMessage extends Message +final class ImageMessage extends Message { /** * {@inheritdoc} @@ -16,12 +16,12 @@ class ImageMessage extends Message * * You can get a WhatsApp Media ID uploading the document to the WhatsApp Cloud servers. */ - protected MediaID $id; + private MediaID $id; /** * Describes the specified document. */ - protected ?string $caption; + private ?string $caption; /** * {@inheritdoc} diff --git a/src/Message/LocationMessage.php b/src/Message/LocationMessage.php index 2d777ad..c1def83 100644 --- a/src/Message/LocationMessage.php +++ b/src/Message/LocationMessage.php @@ -4,23 +4,23 @@ use Netflie\WhatsAppCloudApi\Message\Error\InvalidMessage; -class LocationMessage extends Message +final class LocationMessage extends Message { /** * {@inheritdoc} */ protected string $type = 'location'; - protected float $longitude; + private float $longitude; - protected float $latitude; + private float $latitude; /** * Name of the location */ - protected string $name; + private string $name; - protected string $address; + private string $address; /** * {@inheritdoc} diff --git a/src/Message/Media/LinkID.php b/src/Message/Media/LinkID.php index ff722e9..a7fab98 100644 --- a/src/Message/Media/LinkID.php +++ b/src/Message/Media/LinkID.php @@ -4,7 +4,7 @@ use Netflie\WhatsAppCloudApi\Message\Error\InvalidMessage; -class LinkID extends MediaID +final class LinkID extends MediaID { /** * {@inheritdoc} diff --git a/src/Message/Media/MediaID.php b/src/Message/Media/MediaID.php index 81802ce..5d76e71 100644 --- a/src/Message/Media/MediaID.php +++ b/src/Message/Media/MediaID.php @@ -12,7 +12,7 @@ abstract class MediaID /** * Value of the identifier */ - protected string $value; + private string $value; public function __construct(string $id) { diff --git a/src/Message/Media/MediaObjectID.php b/src/Message/Media/MediaObjectID.php index 145774f..bec0803 100644 --- a/src/Message/Media/MediaObjectID.php +++ b/src/Message/Media/MediaObjectID.php @@ -2,7 +2,7 @@ namespace Netflie\WhatsAppCloudApi\Message\Media; -class MediaObjectID extends MediaID +final class MediaObjectID extends MediaID { /** * {@inheritdoc} diff --git a/src/Message/Message.php b/src/Message/Message.php index ae79ffd..0975c67 100644 --- a/src/Message/Message.php +++ b/src/Message/Message.php @@ -7,17 +7,17 @@ abstract class Message /** * @var string Currently only "whatsapp" value is supported. */ - protected string $messaging_product = 'whatsapp'; + private string $messaging_product = 'whatsapp'; /** * @var string Currently only "individual" value is supported. */ - protected string $recipient_type = 'individual'; + private string $recipient_type = 'individual'; /** * @var string WhatsApp ID or phone number for the person you want to send a message to. */ - protected string $to; + private string $to; /** * @var string Type of message object. diff --git a/src/Message/OptionsListMessage.php b/src/Message/OptionsListMessage.php index 1042108..7d307d3 100644 --- a/src/Message/OptionsListMessage.php +++ b/src/Message/OptionsListMessage.php @@ -4,20 +4,20 @@ use Netflie\WhatsAppCloudApi\Message\OptionsList\Action; -class OptionsListMessage extends Message +final class OptionsListMessage extends Message { /** * {@inheritdoc} */ protected string $type = 'list'; - protected string $header; + private string $header; - protected string $body; + private string $body; - protected string $footer; + private string $footer; - protected Action $action; + private Action $action; /** * {@inheritdoc} diff --git a/src/Message/StickerMessage.php b/src/Message/StickerMessage.php index ef0c718..44a0413 100644 --- a/src/Message/StickerMessage.php +++ b/src/Message/StickerMessage.php @@ -4,7 +4,7 @@ use Netflie\WhatsAppCloudApi\Message\Media\MediaID; -class StickerMessage extends Message +final class StickerMessage extends Message { /** * {@inheritdoc} @@ -16,7 +16,7 @@ class StickerMessage extends Message * * You can get a WhatsApp Media ID uploading the document to the WhatsApp Cloud servers. */ - protected MediaID $id; + private MediaID $id; /** * {@inheritdoc} diff --git a/src/Message/Template/Component.php b/src/Message/Template/Component.php index bceec19..b231b38 100644 --- a/src/Message/Template/Component.php +++ b/src/Message/Template/Component.php @@ -2,22 +2,22 @@ namespace Netflie\WhatsAppCloudApi\Message\Template; -class Component +final class Component { /** * Parameters of a header template. */ - protected array $header; + private array $header; /** * Parameters of a body template. */ - protected array $body; + private array $body; /** * Buttons to attach to a template. */ - protected array $buttons; + private array $buttons; public function __construct(array $header = [], array $body = [], array $buttons = []) { diff --git a/src/Message/TemplateMessage.php b/src/Message/TemplateMessage.php index 2fbd64f..7fdf33a 100644 --- a/src/Message/TemplateMessage.php +++ b/src/Message/TemplateMessage.php @@ -4,7 +4,7 @@ use Netflie\WhatsAppCloudApi\Message\Template\Component; -class TemplateMessage extends Message +final class TemplateMessage extends Message { /** * {@inheritdoc} @@ -15,18 +15,18 @@ class TemplateMessage extends Message * Name of the template * @link https://business.facebook.com/wa/manage/message-templates/ Dashboard to manage (create, edit and delete) templates. */ - protected string $name; + private string $name; /** * @link https://developers.facebook.com/docs/whatsapp/api/messages/message-templates#supported-languages See supported language codes. */ - protected string $language; + private string $language; /** * Templates header, body and buttons can be personalized * @link https://developers.facebook.com/docs/whatsapp/cloud-api/guides/send-message-templates See how you can personalized your templates. */ - protected ?Component $components; + private ?Component $components; /** * {@inheritdoc} diff --git a/src/Message/TextMessage.php b/src/Message/TextMessage.php index 0fdcabf..42f4bc3 100644 --- a/src/Message/TextMessage.php +++ b/src/Message/TextMessage.php @@ -2,27 +2,27 @@ namespace Netflie\WhatsAppCloudApi\Message; -class TextMessage extends Message +final class TextMessage extends Message { /** * @const int Maximum length for body message. */ - protected const MAXIMUM_LENGTH = 4096; + private const MAXIMUM_LENGTH = 4096; /** - * @var string Type of message object. - */ + * {@inheritdoc} + */ protected string $type = 'text'; /** * @var string The body of the text message. */ - protected string $text; + private string $text; /** * @var bool Determines if show a preview box for URLs contained in the text message. */ - protected bool $preview_url; + private bool $preview_url; /** * Creates a new message of type text. diff --git a/src/Message/VideoMessage.php b/src/Message/VideoMessage.php index 1e2c64a..fd88d8d 100644 --- a/src/Message/VideoMessage.php +++ b/src/Message/VideoMessage.php @@ -4,7 +4,7 @@ use Netflie\WhatsAppCloudApi\Message\Media\MediaID; -class VideoMessage extends Message +final class VideoMessage extends Message { /** * {@inheritdoc} @@ -14,14 +14,14 @@ class VideoMessage extends Message /** * Describes the specified document. */ - protected string $caption; + private string $caption; /** * Document identifier: WhatsApp Media ID or any Internet public link document. * * You can get a WhatsApp Media ID uploading the document to the WhatsApp Cloud servers. */ - protected MediaID $id; + private MediaID $id; /** * {@inheritdoc} diff --git a/src/Request.php b/src/Request.php index c606590..cbe15d2 100644 --- a/src/Request.php +++ b/src/Request.php @@ -12,14 +12,14 @@ abstract class Request /** * @var string The access token to use for this request. */ - protected string $access_token; + private string $access_token; /** * The timeout request. * * @return int */ - protected int $timeout; + private int $timeout; /** * Creates a new Request entity. diff --git a/src/Request/MediaRequest/DownloadMediaRequest.php b/src/Request/MediaRequest/DownloadMediaRequest.php index 82cad5a..ad488d2 100644 --- a/src/Request/MediaRequest/DownloadMediaRequest.php +++ b/src/Request/MediaRequest/DownloadMediaRequest.php @@ -4,12 +4,12 @@ use Netflie\WhatsAppCloudApi\Request; -class DownloadMediaRequest extends Request +final class DownloadMediaRequest extends Request { /** * @var string Id of the media uploaded to the Facebook servers. */ - protected string $media_id; + private string $media_id; /** * Creates a new Media Request instance. diff --git a/src/Request/MediaRequest/UploadMediaRequest.php b/src/Request/MediaRequest/UploadMediaRequest.php index aedf506..1381657 100644 --- a/src/Request/MediaRequest/UploadMediaRequest.php +++ b/src/Request/MediaRequest/UploadMediaRequest.php @@ -5,17 +5,17 @@ use GuzzleHttp\Psr7; use Netflie\WhatsAppCloudApi\Request; -class UploadMediaRequest extends Request +final class UploadMediaRequest extends Request { /** * @var string File path of file will sent. */ - protected string $file_path; + private string $file_path; /** * @var string WhatsApp Number Id from messages will sent. */ - protected string $phone_number_id; + private string $phone_number_id; /** * Creates a new Media Request instance. diff --git a/src/Request/MessageRequest.php b/src/Request/MessageRequest.php index fb0b647..9301008 100644 --- a/src/Request/MessageRequest.php +++ b/src/Request/MessageRequest.php @@ -15,7 +15,7 @@ abstract class MessageRequest extends Request /** * @var string WhatsApp Number Id from messages will sent. */ - protected string $from_phone_number_id; + private string $from_phone_number_id; public function __construct(Message $message, string $access_token, string $from_phone_number_id, ?int $timeout = null) { diff --git a/src/Request/MessageRequest/RequestAudioMessage.php b/src/Request/MessageRequest/RequestAudioMessage.php index 7352834..8a270cf 100644 --- a/src/Request/MessageRequest/RequestAudioMessage.php +++ b/src/Request/MessageRequest/RequestAudioMessage.php @@ -4,7 +4,7 @@ use Netflie\WhatsAppCloudApi\Request\MessageRequest; -class RequestAudioMessage extends MessageRequest +final class RequestAudioMessage extends MessageRequest { /** * {@inheritdoc} diff --git a/src/Request/MessageRequest/RequestContactMessage.php b/src/Request/MessageRequest/RequestContactMessage.php index d93b50f..05837c2 100644 --- a/src/Request/MessageRequest/RequestContactMessage.php +++ b/src/Request/MessageRequest/RequestContactMessage.php @@ -4,7 +4,7 @@ use Netflie\WhatsAppCloudApi\Request\MessageRequest; -class RequestContactMessage extends MessageRequest +final class RequestContactMessage extends MessageRequest { /** * {@inheritdoc} diff --git a/src/Request/MessageRequest/RequestDocumentMessage.php b/src/Request/MessageRequest/RequestDocumentMessage.php index 3f6c607..21eeda1 100644 --- a/src/Request/MessageRequest/RequestDocumentMessage.php +++ b/src/Request/MessageRequest/RequestDocumentMessage.php @@ -4,7 +4,7 @@ use Netflie\WhatsAppCloudApi\Request\MessageRequest; -class RequestDocumentMessage extends MessageRequest +final class RequestDocumentMessage extends MessageRequest { /** * {@inheritdoc} diff --git a/src/Request/MessageRequest/RequestImageMessage.php b/src/Request/MessageRequest/RequestImageMessage.php index ce8f03a..e71396d 100644 --- a/src/Request/MessageRequest/RequestImageMessage.php +++ b/src/Request/MessageRequest/RequestImageMessage.php @@ -4,7 +4,7 @@ use Netflie\WhatsAppCloudApi\Request\MessageRequest; -class RequestImageMessage extends MessageRequest +final class RequestImageMessage extends MessageRequest { /** * {@inheritdoc} diff --git a/src/Request/MessageRequest/RequestLocationMessage.php b/src/Request/MessageRequest/RequestLocationMessage.php index 9e7c6ae..2cfd882 100644 --- a/src/Request/MessageRequest/RequestLocationMessage.php +++ b/src/Request/MessageRequest/RequestLocationMessage.php @@ -4,7 +4,7 @@ use Netflie\WhatsAppCloudApi\Request\MessageRequest; -class RequestLocationMessage extends MessageRequest +final class RequestLocationMessage extends MessageRequest { /** * {@inheritdoc} diff --git a/src/Request/MessageRequest/RequestOptionsListMessage.php b/src/Request/MessageRequest/RequestOptionsListMessage.php index ccf2ac5..6e1b4ec 100644 --- a/src/Request/MessageRequest/RequestOptionsListMessage.php +++ b/src/Request/MessageRequest/RequestOptionsListMessage.php @@ -2,13 +2,9 @@ namespace Netflie\WhatsAppCloudApi\Request\MessageRequest; -use Netflie\WhatsAppCloudApi\Message\OptionsListMessage; use Netflie\WhatsAppCloudApi\Request\MessageRequest; -/** - * @property OptionsListMessage $message - */ -class RequestOptionsListMessage extends MessageRequest +final class RequestOptionsListMessage extends MessageRequest { /** * {@inheritdoc} diff --git a/src/Request/MessageRequest/RequestStickerMessage.php b/src/Request/MessageRequest/RequestStickerMessage.php index 0077968..f2106fa 100644 --- a/src/Request/MessageRequest/RequestStickerMessage.php +++ b/src/Request/MessageRequest/RequestStickerMessage.php @@ -4,7 +4,7 @@ use Netflie\WhatsAppCloudApi\Request\MessageRequest; -class RequestStickerMessage extends MessageRequest +final class RequestStickerMessage extends MessageRequest { /** * {@inheritdoc} diff --git a/src/Request/MessageRequest/RequestTemplateMessage.php b/src/Request/MessageRequest/RequestTemplateMessage.php index 9b493a0..5192302 100644 --- a/src/Request/MessageRequest/RequestTemplateMessage.php +++ b/src/Request/MessageRequest/RequestTemplateMessage.php @@ -4,7 +4,7 @@ use Netflie\WhatsAppCloudApi\Request\MessageRequest; -class RequestTemplateMessage extends MessageRequest +final class RequestTemplateMessage extends MessageRequest { /** * {@inheritdoc} diff --git a/src/Request/MessageRequest/RequestTextMessage.php b/src/Request/MessageRequest/RequestTextMessage.php index 286fef5..e28a2ca 100644 --- a/src/Request/MessageRequest/RequestTextMessage.php +++ b/src/Request/MessageRequest/RequestTextMessage.php @@ -4,7 +4,7 @@ use Netflie\WhatsAppCloudApi\Request\MessageRequest; -class RequestTextMessage extends MessageRequest +final class RequestTextMessage extends MessageRequest { /** * {@inheritdoc} diff --git a/src/Request/MessageRequest/RequestVideoMessage.php b/src/Request/MessageRequest/RequestVideoMessage.php index e0411c6..cfe014a 100644 --- a/src/Request/MessageRequest/RequestVideoMessage.php +++ b/src/Request/MessageRequest/RequestVideoMessage.php @@ -4,7 +4,7 @@ use Netflie\WhatsAppCloudApi\Request\MessageRequest; -class RequestVideoMessage extends MessageRequest +final class RequestVideoMessage extends MessageRequest { /** * {@inheritdoc} diff --git a/src/Response/ResponseException.php b/src/Response/ResponseException.php index 69d5cb7..4a1725f 100644 --- a/src/Response/ResponseException.php +++ b/src/Response/ResponseException.php @@ -4,17 +4,17 @@ use Netflie\WhatsAppCloudApi\Response; -class ResponseException extends \Exception +final class ResponseException extends \Exception { /** * @var Response The response that threw the exception. */ - protected $response; + private $response; /** * @var array Decoded response. */ - protected $response_data; + private $response_data; /** * Creates a ResponseException. From f67f8d42d7cf64a08a467cdb715245657932e565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Sun, 29 Jan 2023 12:08:17 +0100 Subject: [PATCH 09/20] 2.x: fix unsafe usage of new static() --- src/Response.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Response.php b/src/Response.php index c32d241..6783df4 100644 --- a/src/Response.php +++ b/src/Response.php @@ -52,7 +52,7 @@ public function __construct(Request $request, string $body, ?int $http_status_co public static function fromClientResponse(Request $request, RawResponse $response): self { - return new static( + return new self( $request, $response->body(), $response->httpResponseCode(), From d717fff838cf1ca5769a945ff4e712d7bec45277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Sun, 29 Jan 2023 12:08:51 +0100 Subject: [PATCH 10/20] 2.x: add phpstan --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d78d8eb..1860d7e 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,8 @@ "symfony/var-dumper": "^5.0", "phpspec/prophecy-phpunit": "^2.0", "fakerphp/faker": "^1.19", - "friendsofphp/php-cs-fixer": "^3.13" + "friendsofphp/php-cs-fixer": "^3.13", + "phpstan/phpstan": "^1.9" }, "autoload": { "psr-4": { From bebb308b7a53558bc918f6a119310788308c7ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Sun, 20 Nov 2022 19:14:54 +0100 Subject: [PATCH 11/20] webhook: Add class to verify a webhook --- src/WebHook/VerificationRequest.php | 34 ++++++++++++++++ .../Unit/WebHook/VerificationRequestTest.php | 40 +++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 src/WebHook/VerificationRequest.php create mode 100644 tests/Unit/WebHook/VerificationRequestTest.php diff --git a/src/WebHook/VerificationRequest.php b/src/WebHook/VerificationRequest.php new file mode 100644 index 0000000..dbe4780 --- /dev/null +++ b/src/WebHook/VerificationRequest.php @@ -0,0 +1,34 @@ +verify_token = $verify_token; + } + + public function validate(array $payload): string + { + $mode = $payload['hub_mode'] ?? null; + $token = $payload['hub_verify_token'] ?? null; + $challenge = $payload['hub_challenge'] ?? ''; + + if ('subscribe' !== $mode || $token !== $this->verify_token) { + http_response_code(403); + + return $challenge; + } + + http_response_code(200); + + return $challenge; + } +} diff --git a/tests/Unit/WebHook/VerificationRequestTest.php b/tests/Unit/WebHook/VerificationRequestTest.php new file mode 100644 index 0000000..8f0a26f --- /dev/null +++ b/tests/Unit/WebHook/VerificationRequestTest.php @@ -0,0 +1,40 @@ +validate([ + 'hub_mode' => 'subscribe', + 'hub_verify_token' => 'super-secret', + 'hub_challenge' => 'challenge_code', + ]); + + $this->assertEquals('challenge_code', $response); + $this->assertEquals(200, http_response_code()); + } + + public function test_verification_request_fails() + { + $verification_request = new VerificationRequest('super-secret'); + + $response = $verification_request->validate([ + 'hub_mode' => 'subscribe', + 'hub_verify_token' => 'bad-super-secret', + 'hub_challenge' => 'challenge_code', + ]); + + $this->assertEquals('challenge_code', $response); + $this->assertEquals(403, http_response_code()); + } +} From 3787191e51f581ff9f2c3b96b8f615dd3d764c63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Sun, 20 Nov 2022 19:28:53 +0100 Subject: [PATCH 12/20] webhook: Add WebHook superclass to manage with webhooks verification --- src/WebHook.php | 20 ++++++++++++++++++++ tests/Unit/WebHookTest.php | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 src/WebHook.php create mode 100644 tests/Unit/WebHookTest.php diff --git a/src/WebHook.php b/src/WebHook.php new file mode 100644 index 0000000..1abc289 --- /dev/null +++ b/src/WebHook.php @@ -0,0 +1,20 @@ +validate($payload); + } +} diff --git a/tests/Unit/WebHookTest.php b/tests/Unit/WebHookTest.php new file mode 100644 index 0000000..a6e2bb2 --- /dev/null +++ b/tests/Unit/WebHookTest.php @@ -0,0 +1,33 @@ +webhook = new WebHook(); + } + + public function test_verify_a_webhook() + { + $response = $this->webhook->verify([ + 'hub_mode' => 'subscribe', + 'hub_verify_token' => 'verify-token', + 'hub_challenge' => 'challenge_code', + ], 'verify-token'); + + $this->assertEquals('challenge_code', $response); + $this->assertEquals(200, http_response_code()); + } +} From af383f6c5cf90440cb39024af95949c5de01c456 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Sun, 29 Jan 2023 13:13:49 +0100 Subject: [PATCH 13/20] 2.x: ignore ./idea Jetbrain folder --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0aaf97c..b1f7502 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ composer.phar /tests/WhatsAppCloudApiTestConfiguration.php .phpunit.result.cache composer.lock -.php-cs-fixer.cache \ No newline at end of file +.php-cs-fixer.cache +.idea/ \ No newline at end of file From 61e1c8287f6e1571f367a94cf055c77fb32970ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Sun, 8 Jan 2023 18:40:52 +0100 Subject: [PATCH 14/20] webhook: Add webhook notifications --- src/WebHook.php | 14 + src/WebHook/Notification.php | 41 + src/WebHook/Notification/Button.php | 33 + src/WebHook/Notification/Contact.php | 113 +++ src/WebHook/Notification/Interactive.php | 42 + src/WebHook/Notification/Location.php | 51 + src/WebHook/Notification/Media.php | 42 + .../Notification/MessageNotification.php | 68 ++ .../MessageNotificationFactory.php | 153 +++ src/WebHook/Notification/Order.php | 42 + src/WebHook/Notification/Reaction.php | 33 + .../Notification/StatusNotification.php | 145 +++ .../StatusNotificationFactory.php | 34 + src/WebHook/Notification/Support/Business.php | 26 + src/WebHook/Notification/Support/Context.php | 55 ++ .../Notification/Support/Conversation.php | 49 + .../Notification/Support/ConversationType.php | 17 + src/WebHook/Notification/Support/Customer.php | 34 + src/WebHook/Notification/Support/Error.php | 26 + src/WebHook/Notification/Support/Products.php | 38 + src/WebHook/Notification/Support/Referral.php | 82 ++ .../Notification/Support/ReferredProduct.php | 26 + src/WebHook/Notification/Support/Status.php | 20 + src/WebHook/Notification/System.php | 28 + src/WebHook/Notification/Text.php | 20 + src/WebHook/Notification/Unknown.php | 11 + src/WebHook/NotificationFactory.php | 38 + src/WebHook/VerificationRequest.php | 2 +- .../Unit/WebHook/NotificationFactoryTest.php | 905 ++++++++++++++++++ 29 files changed, 2187 insertions(+), 1 deletion(-) create mode 100644 src/WebHook/Notification.php create mode 100644 src/WebHook/Notification/Button.php create mode 100644 src/WebHook/Notification/Contact.php create mode 100644 src/WebHook/Notification/Interactive.php create mode 100644 src/WebHook/Notification/Location.php create mode 100644 src/WebHook/Notification/Media.php create mode 100644 src/WebHook/Notification/MessageNotification.php create mode 100644 src/WebHook/Notification/MessageNotificationFactory.php create mode 100644 src/WebHook/Notification/Order.php create mode 100644 src/WebHook/Notification/Reaction.php create mode 100644 src/WebHook/Notification/StatusNotification.php create mode 100644 src/WebHook/Notification/StatusNotificationFactory.php create mode 100644 src/WebHook/Notification/Support/Business.php create mode 100644 src/WebHook/Notification/Support/Context.php create mode 100644 src/WebHook/Notification/Support/Conversation.php create mode 100644 src/WebHook/Notification/Support/ConversationType.php create mode 100644 src/WebHook/Notification/Support/Customer.php create mode 100644 src/WebHook/Notification/Support/Error.php create mode 100644 src/WebHook/Notification/Support/Products.php create mode 100644 src/WebHook/Notification/Support/Referral.php create mode 100644 src/WebHook/Notification/Support/ReferredProduct.php create mode 100644 src/WebHook/Notification/Support/Status.php create mode 100644 src/WebHook/Notification/System.php create mode 100644 src/WebHook/Notification/Text.php create mode 100644 src/WebHook/Notification/Unknown.php create mode 100644 src/WebHook/NotificationFactory.php create mode 100644 tests/Unit/WebHook/NotificationFactoryTest.php diff --git a/src/WebHook.php b/src/WebHook.php index 1abc289..77952f7 100644 --- a/src/WebHook.php +++ b/src/WebHook.php @@ -2,6 +2,8 @@ namespace Netflie\WhatsAppCloudApi; +use Netflie\WhatsAppCloudApi\WebHook\Notification; +use Netflie\WhatsAppCloudApi\WebHook\NotificationFactory; use Netflie\WhatsAppCloudApi\WebHook\VerificationRequest; class WebHook @@ -17,4 +19,16 @@ public function verify(array $payload, string $verify_token): string return (new VerificationRequest($verify_token)) ->validate($payload); } + + /** + * Get a notification from incoming webhook messages. + * + * @param array $payload Payload received in your endpoint URL. + * @return Notification A PHP representation of WhatsApp webhook notifications + */ + public function read(array $payload): ?Notification + { + return (new NotificationFactory()) + ->buildFromPayload($payload); + } } diff --git a/src/WebHook/Notification.php b/src/WebHook/Notification.php new file mode 100644 index 0000000..89261af --- /dev/null +++ b/src/WebHook/Notification.php @@ -0,0 +1,41 @@ +id = $id; + $this->business = $business; + $this->received_at = (new \DateTimeImmutable())->setTimestamp($received_at_timestamp); + } + + public function id(): string + { + return $this->id; + } + + public function businessPhoneNumberId(): string + { + return $this->business->phoneNumberId(); + } + + public function businessPhoneNumber(): string + { + return $this->business->phoneNumber(); + } + + public function receivedAt(): \DateTimeImmutable + { + return $this->received_at; + } +} diff --git a/src/WebHook/Notification/Button.php b/src/WebHook/Notification/Button.php new file mode 100644 index 0000000..3f89772 --- /dev/null +++ b/src/WebHook/Notification/Button.php @@ -0,0 +1,33 @@ +text = $text; + $this->payload = $payload; + } + + public function text(): string + { + return $this->text; + } + + public function payload(): string + { + return $this->payload; + } +} diff --git a/src/WebHook/Notification/Contact.php b/src/WebHook/Notification/Contact.php new file mode 100644 index 0000000..72c318d --- /dev/null +++ b/src/WebHook/Notification/Contact.php @@ -0,0 +1,113 @@ +name = $name; + $this->addresses = $addresses; + $this->emails = $emails; + $this->company = $company; + $this->phones = $phones; + $this->urls = $urls; + $this->birthday = $birthday ? \DateTimeImmutable::createFromFormat('Y-m-d', $birthday) : null; + } + + public function name(): array + { + return $this->name; + } + + public function formattedName(): string + { + return $this->name['formatted_name']; + } + + public function firstName(): string + { + return $this->name['first_name']; + } + + public function lastName(): string + { + return $this->name['last_name']; + } + + public function middleName(): string + { + return $this->name['middle_name']; + } + + public function addresses(): array + { + return $this->addresses; + } + + public function birthday(): ?\DateTimeImmutable + { + return $this->birthday; + } + + public function emails(): array + { + return $this->emails; + } + + public function company(): array + { + return $this->company; + } + + public function companyName(): string + { + return $this->company['company']; + } + + public function companyDepartment(): string + { + return $this->company['department']; + } + + public function companyTitle(): string + { + return $this->company['title']; + } + + public function phones(): array + { + return $this->phones; + } + + public function urls(): array + { + return $this->urls; + } +} diff --git a/src/WebHook/Notification/Interactive.php b/src/WebHook/Notification/Interactive.php new file mode 100644 index 0000000..7b1894e --- /dev/null +++ b/src/WebHook/Notification/Interactive.php @@ -0,0 +1,42 @@ +item_id = $item_id; + $this->title = $title; + $this->description = $description; + } + + public function itemId(): string + { + return $this->item_id; + } + + public function title(): string + { + return $this->title; + } + + public function description(): string + { + return $this->description; + } +} diff --git a/src/WebHook/Notification/Location.php b/src/WebHook/Notification/Location.php new file mode 100644 index 0000000..9627c5e --- /dev/null +++ b/src/WebHook/Notification/Location.php @@ -0,0 +1,51 @@ +latitude = $latitude; + $this->longitude = $longitude; + $this->name = $name; + $this->address = $address; + } + + public function latitude(): string + { + return $this->latitude; + } + + public function longitude(): string + { + return $this->longitude; + } + + public function name(): string + { + return $this->name; + } + + public function address(): string + { + return $this->address; + } +} diff --git a/src/WebHook/Notification/Media.php b/src/WebHook/Notification/Media.php new file mode 100644 index 0000000..749b16e --- /dev/null +++ b/src/WebHook/Notification/Media.php @@ -0,0 +1,42 @@ +image_id = $image_id; + $this->mime_type = $mime_type; + $this->caption = $caption; + } + + public function imageId(): string + { + return $this->image_id; + } + + public function mimeType(): string + { + return $this->mime_type; + } + + public function caption(): string + { + return $this->caption; + } +} diff --git a/src/WebHook/Notification/MessageNotification.php b/src/WebHook/Notification/MessageNotification.php new file mode 100644 index 0000000..020cc59 --- /dev/null +++ b/src/WebHook/Notification/MessageNotification.php @@ -0,0 +1,68 @@ +customer; + } + + public function replyingToMessageId(): ?string + { + if (!$this->context) { + return null; + } + + return $this->context->replyingToMessageId(); + } + + public function isForwarded(): bool + { + if (!$this->context) { + return false; + } + + return $this->context->isForwarded(); + } + + public function context(): ?Support\Context + { + return $this->context; + } + + public function referral(): ?Support\Referral + { + return $this->referral; + } + + public function withContext(Support\Context $context): self + { + $this->context = $context; + + return $this; + } + + public function withReferral(Support\Referral $referral): self + { + $this->referral = $referral; + + return $this; + } + + public function withCustomer(Support\Customer $customer): self + { + $this->customer = $customer; + + return $this; + } +} diff --git a/src/WebHook/Notification/MessageNotificationFactory.php b/src/WebHook/Notification/MessageNotificationFactory.php new file mode 100644 index 0000000..f2af97f --- /dev/null +++ b/src/WebHook/Notification/MessageNotificationFactory.php @@ -0,0 +1,153 @@ +buildMessageNotification($metadata, $message); + + return $this->decorateNotification($notification, $message, $contact); + } + + private function buildMessageNotification(array $metadata, array $message): MessageNotification + { + switch ($message['type']) { + case 'text': + return new Text( + $message['id'], + new Support\Business($metadata['phone_number_id'], $metadata['display_phone_number']), + $message['text']['body'], + $message['timestamp'] + ); + case 'reaction': + return new Reaction( + $message['id'], + new Support\Business($metadata['phone_number_id'], $metadata['display_phone_number']), + $message['reaction']['message_id'], + $message['reaction']['emoji'], + $message['timestamp'] + ); + case 'sticker': + case 'image': + case 'document': + case 'audio': + case 'video': + case 'voice': + return new Media( + $message['id'], + new Support\Business($metadata['phone_number_id'], $metadata['display_phone_number']), + $message[$message['type']]['id'], + $message[$message['type']]['mime_type'], + $message[$message['type']]['caption'] ?? '', + $message['timestamp'] + ); + case 'location': + return new Location( + $message['id'], + new Support\Business($metadata['phone_number_id'], $metadata['display_phone_number']), + $message['location']['latitude'], + $message['location']['longitude'], + $message['location']['name'] ?? '', + $message['location']['address'] ?? '', + $message['timestamp'] + ); + case 'contacts': + return new Contact( + $message['id'], + new Support\Business($metadata['phone_number_id'], $metadata['display_phone_number']), + $message['contacts'][0]['addresses'] ?? [], + $message['contacts'][0]['emails'] ?? [], + $message['contacts'][0]['name'], + $message['contacts'][0]['org'] ?? [], + $message['contacts'][0]['phones'], + $message['contacts'][0]['urls'] ?? [], + $message['contacts'][0]['birthday'] ?? null, + $message['timestamp'] + ); + case 'button': + return new Button( + $message['id'], + new Support\Business($metadata['phone_number_id'], $metadata['display_phone_number']), + $message['button']['text'], + $message['button']['payload'], + $message['timestamp'] + ); + case 'interactive': + return new Interactive( + $message['id'], + new Support\Business($metadata['phone_number_id'], $metadata['display_phone_number']), + $message['interactive']['list_reply']['id'] ?? $message['interactive']['button_reply']['id'], + $message['interactive']['list_reply']['title'] ?? $message['interactive']['button_reply']['title'], + $message['interactive']['list_reply']['description'] ?? '', + $message['timestamp'] + ); + case 'order': + return new Order( + $message['id'], + new Support\Business($metadata['phone_number_id'], $metadata['display_phone_number']), + $message['order']['catalog_id'], + $message['order']['text'], + new Support\Products($message['order']['product_items']), + $message['timestamp'] + ); + case 'system': + return new System( + $message['id'], + new Support\Business($metadata['phone_number_id'], $metadata['display_phone_number']), + new Support\Business($message['system']['customer'], ''), + $message['system']['body'], + $message['timestamp'] + ); + case 'unknown': + default: + return new Unknown( + $message['id'], + new Support\Business($metadata['phone_number_id'], $metadata['display_phone_number']), + $message['timestamp'] + ); + } + } + + private function decorateNotification(MessageNotification $notification, array $message, array $contact): MessageNotification + { + if ($contact) { + $notification->withCustomer(new Support\Customer( + $contact['wa_id'], + $contact['profile']['name'], + $message['from'] + )); + } + + if (isset($message['context'])) { + if (isset($message['context']['referred_product'])) { + $referred_product = new Support\ReferredProduct( + $message['context']['referred_product']['catalog_id'], + $message['context']['referred_product']['product_retailer_id'] + ); + } + + $notification->withContext(new Support\Context( + $message['context']['id'], + $message['context']['forwarded'] ?? false, + $referred_product ?? null + )); + } + + if (isset($message['referral'])) { + $notification->withReferral(new Support\Referral( + $message['referral']['source_id'] ?? '', + $message['referral']['source_url'] ?? '', + $message['referral']['source_type'] ?? '', + $message['referral']['headline'] ?? '', + $message['referral']['body'] ?? '', + $message['referral']['media_type'] ?? '', + $message['referral']['image_url'] ?? $message['referral']['video_url'] ?? '', + $message['referral']['thumbnail_url'] ?? '' + )); + } + + return $notification; + } +} diff --git a/src/WebHook/Notification/Order.php b/src/WebHook/Notification/Order.php new file mode 100644 index 0000000..a6e9277 --- /dev/null +++ b/src/WebHook/Notification/Order.php @@ -0,0 +1,42 @@ +catalog_id = $catalog_id; + $this->message = $message; + $this->products = $products; + } + + public function catalogId(): string + { + return $this->catalog_id; + } + + public function message(): string + { + return $this->message; + } + + public function products(): Products + { + return $this->products; + } +} diff --git a/src/WebHook/Notification/Reaction.php b/src/WebHook/Notification/Reaction.php new file mode 100644 index 0000000..2a060ef --- /dev/null +++ b/src/WebHook/Notification/Reaction.php @@ -0,0 +1,33 @@ +message_id = $message_id; + $this->emoji = $emoji; + } + + public function messageId(): string + { + return $this->message_id; + } + + public function emoji(): string + { + return $this->emoji; + } +} diff --git a/src/WebHook/Notification/StatusNotification.php b/src/WebHook/Notification/StatusNotification.php new file mode 100644 index 0000000..3fdc244 --- /dev/null +++ b/src/WebHook/Notification/StatusNotification.php @@ -0,0 +1,145 @@ +customer_id = $customer_id; + $this->status = new Support\Status($status); + } + + public function withConversation(Support\Conversation $conversation): self + { + $this->conversation = $conversation; + + return $this; + } + + public function withError(Support\Error $error): self + { + $this->error = $error; + + return $this; + } + + public function customerId(): string + { + return $this->customer_id; + } + + public function conversationId(): ?string + { + if (!$this->conversation) { + return null; + } + + return $this->conversation->id(); + } + + public function conversationType(): ?string + { + if (!$this->conversation) { + return null; + } + + return (string) $this->conversation->type(); + } + + public function conversationExpiresAt(): ?\DateTimeImmutable + { + if (!$this->conversation) { + return null; + } + + return $this->conversation->expiresAt(); + } + + public function isBusinessInitiatedConversation(): ?bool + { + if (!$this->conversation) { + return null; + } + + return $this->conversation->isBusinessInitiated(); + } + + public function isCustomerInitiatedConversation(): ?bool + { + if (!$this->conversation) { + return null; + } + + return $this->conversation->isCustomerInitiated(); + } + + public function isReferralInitiatedConversation(): ?bool + { + if (!$this->conversation) { + return null; + } + + return $this->conversation->isReferralInitiated(); + } + + public function status(): string + { + return (string) $this->status; + } + + public function isMessageRead(): bool + { + return $this->status->equals(Support\Status::READ()); + } + + public function isMessageDelivered(): bool + { + return $this->isMessageRead() || $this->status->equals(Support\Status::DELIVERED()); + } + + public function isMessageSent(): bool + { + return $this->isMessageDelivered() || $this->status->equals(Support\Status::SENT()); + } + + public function hasErrors(): bool + { + return null !== $this->error; + } + + public function errorCode(): ?int + { + if (!$this->error) { + return null; + } + + return $this->error->code(); + } + + public function errorTitle(): ?string + { + if (!$this->error) { + return null; + } + + return $this->error->title(); + } +} diff --git a/src/WebHook/Notification/StatusNotificationFactory.php b/src/WebHook/Notification/StatusNotificationFactory.php new file mode 100644 index 0000000..3d9b8f1 --- /dev/null +++ b/src/WebHook/Notification/StatusNotificationFactory.php @@ -0,0 +1,34 @@ +withConversation(new Support\Conversation( + $status['conversation']['id'], + $status['conversation']['origin']['type'], + $status['conversation']['expiration_timestamp'] ?? null, + )); + } + + if (isset($status['errors'])) { + $notification->withError(new Support\Error( + $status['errors'][0]['code'], + $status['errors'][0]['title'] + )); + } + + return $notification; + } +} diff --git a/src/WebHook/Notification/Support/Business.php b/src/WebHook/Notification/Support/Business.php new file mode 100644 index 0000000..26e25f8 --- /dev/null +++ b/src/WebHook/Notification/Support/Business.php @@ -0,0 +1,26 @@ +phone_number_id = $phone_number_id; + $this->phone_number = $phone_number; + } + + public function phoneNumberId(): string + { + return $this->phone_number_id; + } + + public function phoneNumber(): string + { + return $this->phone_number; + } +} diff --git a/src/WebHook/Notification/Support/Context.php b/src/WebHook/Notification/Support/Context.php new file mode 100644 index 0000000..9696de2 --- /dev/null +++ b/src/WebHook/Notification/Support/Context.php @@ -0,0 +1,55 @@ +replying_to_message_id = $replying_to_message_id; + $this->forwarded = $forwarded; + $this->referred_product = $referred_product; + } + + public function replyingToMessageId(): string + { + return $this->replying_to_message_id; + } + + public function isForwarded(): bool + { + return $this->forwarded; + } + + public function hasReferredProduct(): bool + { + return null !== $this->referred_product; + } + + public function catalogId(): ?string + { + if (!$this->hasReferredProduct()) { + return null; + } + + return $this->referred_product->catalogId(); + } + + public function productRetailerId(): ?string + { + if (!$this->hasReferredProduct()) { + return null; + } + + return $this->referred_product->productRetailerId(); + } +} diff --git a/src/WebHook/Notification/Support/Conversation.php b/src/WebHook/Notification/Support/Conversation.php new file mode 100644 index 0000000..49123a6 --- /dev/null +++ b/src/WebHook/Notification/Support/Conversation.php @@ -0,0 +1,49 @@ +id = $id; + $this->type = new ConversationType($type); + $this->expires_at = $expires_at ? (new \DateTimeImmutable())->setTimestamp($expires_at) : null; + } + + public function id(): string + { + return $this->id; + } + + public function type(): ConversationType + { + return $this->type; + } + + public function expiresAt(): ?\DateTimeImmutable + { + return $this->expires_at; + } + + public function isBusinessInitiated(): bool + { + return $this->type->equals(ConversationType::BUSINESS_INITIATED()); + } + + public function isCustomerInitiated(): bool + { + return $this->type->equals(ConversationType::CUSTOMER_INITIATED()); + } + + public function isReferralInitiated(): bool + { + return $this->type->equals(ConversationType::REFERRAL_INITIATED()); + } +} diff --git a/src/WebHook/Notification/Support/ConversationType.php b/src/WebHook/Notification/Support/ConversationType.php new file mode 100644 index 0000000..841aa78 --- /dev/null +++ b/src/WebHook/Notification/Support/ConversationType.php @@ -0,0 +1,17 @@ +id = $id; + $this->name = $name; + $this->phone_number = $phone_number; + } + + public function id(): string + { + return $this->id; + } + + public function name(): string + { + return $this->name; + } + + public function phoneNumber(): string + { + return $this->phone_number; + } +} diff --git a/src/WebHook/Notification/Support/Error.php b/src/WebHook/Notification/Support/Error.php new file mode 100644 index 0000000..b72433b --- /dev/null +++ b/src/WebHook/Notification/Support/Error.php @@ -0,0 +1,26 @@ +code = $code; + $this->title = $title; + } + + public function code(): int + { + return $this->code; + } + + public function title(): string + { + return $this->title; + } +} diff --git a/src/WebHook/Notification/Support/Products.php b/src/WebHook/Notification/Support/Products.php new file mode 100644 index 0000000..a8ec6af --- /dev/null +++ b/src/WebHook/Notification/Support/Products.php @@ -0,0 +1,38 @@ +current()['product_retailer_id']; + } + + public function quantity(): string + { + return $this->current()['quantity']; + } + + public function price(): string + { + return $this->current()['item_price']; + } + + public function currency(): string + { + return $this->current()['currency']; + } + + public function hasProductsToIterate(): bool + { + return $this->valid(); + } + + public function nextProduct(): self + { + $this->next(); + + return $this; + } +} diff --git a/src/WebHook/Notification/Support/Referral.php b/src/WebHook/Notification/Support/Referral.php new file mode 100644 index 0000000..6ff70aa --- /dev/null +++ b/src/WebHook/Notification/Support/Referral.php @@ -0,0 +1,82 @@ +source_id = $source_id; + $this->source_url = $source_url; + $this->source_type = $source_type; + $this->headline = $headline; + $this->body = $body; + $this->media_type = $media_type; + $this->media_url = $media_url; + $this->thumbnail_url = $thumbnail_url; + } + + public function sourceId(): string + { + return $this->source_id; + } + + public function sourceUrl(): string + { + return $this->source_url; + } + + public function sourceType(): string + { + return $this->source_type; + } + + public function headline(): string + { + return $this->headline; + } + + public function body(): string + { + return $this->body; + } + + public function mediaType(): string + { + return $this->media_type; + } + + public function mediaUrl(): string + { + return $this->media_url; + } + + public function thumbnailUrl(): string + { + return $this->thumbnail_url; + } +} diff --git a/src/WebHook/Notification/Support/ReferredProduct.php b/src/WebHook/Notification/Support/ReferredProduct.php new file mode 100644 index 0000000..c5e986e --- /dev/null +++ b/src/WebHook/Notification/Support/ReferredProduct.php @@ -0,0 +1,26 @@ +catalog_id = $catalog_id; + $this->product_retailer_id = $product_retailer_id; + } + + public function catalogId(): string + { + return $this->catalog_id; + } + + public function productRetailerId(): string + { + return $this->product_retailer_id; + } +} diff --git a/src/WebHook/Notification/Support/Status.php b/src/WebHook/Notification/Support/Status.php new file mode 100644 index 0000000..2672af5 --- /dev/null +++ b/src/WebHook/Notification/Support/Status.php @@ -0,0 +1,20 @@ +message = $message; + $this->old_business_data = $old_business_data; + } + + public function message(): string + { + return $this->message; + } + + public function oldBusinessPhoneNumberId(): string + { + return $this->old_business_data->phoneNumberId(); + } +} diff --git a/src/WebHook/Notification/Text.php b/src/WebHook/Notification/Text.php new file mode 100644 index 0000000..d0d2777 --- /dev/null +++ b/src/WebHook/Notification/Text.php @@ -0,0 +1,20 @@ +message = $message; + } + + public function message(): string + { + return $this->message; + } +} diff --git a/src/WebHook/Notification/Unknown.php b/src/WebHook/Notification/Unknown.php new file mode 100644 index 0000000..948a96b --- /dev/null +++ b/src/WebHook/Notification/Unknown.php @@ -0,0 +1,11 @@ +message_notification_factory = new Notification\MessageNotificationFactory(); + $this->status_notification_factory = new Notification\StatusNotificationFactory(); + } + + public function buildFromPayload(array $payload): ?Notification + { + if (!is_array($payload['entry'] ?? null)) { + return null; + } + + $entry = $payload['entry'][0] ?? []; + $message = $entry['changes'][0]['value']['messages'][0] ?? []; + $status = $entry['changes'][0]['value']['statuses'][0] ?? []; + $contact = $entry['changes'][0]['value']['contacts'][0] ?? []; + $metadata = $entry['changes'][0]['value']['metadata'] ?? []; + + if ($message) { + return $this->message_notification_factory->buildFromPayload($metadata, $message, $contact); + } + + if ($status) { + return $this->status_notification_factory->buildFromPayload($metadata, $status); + } + + return null; + } +} diff --git a/src/WebHook/VerificationRequest.php b/src/WebHook/VerificationRequest.php index dbe4780..1d8b976 100644 --- a/src/WebHook/VerificationRequest.php +++ b/src/WebHook/VerificationRequest.php @@ -2,7 +2,7 @@ namespace Netflie\WhatsAppCloudApi\WebHook; -class VerificationRequest +final class VerificationRequest { /** * Verify Token field configured in your app's App Dashboard. diff --git a/tests/Unit/WebHook/NotificationFactoryTest.php b/tests/Unit/WebHook/NotificationFactoryTest.php new file mode 100644 index 0000000..6f78c29 --- /dev/null +++ b/tests/Unit/WebHook/NotificationFactoryTest.php @@ -0,0 +1,905 @@ +notification_factory = new NotificationFactory(); + } + + public function test_build_from_payload_can_build_a_notification() + { + $payload = json_decode('{ + "object": "whatsapp_business_account", + "entry": [{ + "id": "WHATSAPP_BUSINESS_ACCOUNT_ID", + "changes": [{ + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "PHONE_NUMBER", + "phone_number_id": "PHONE_NUMBER_ID" + }, + "contacts": [{ + "profile": { + "name": "NAME" + }, + "wa_id": "WHATSAPP_ID" + }], + "messages": [{ + "context": { + "from": "PHONE_NUMBER", + "id": "wamid.ID", + "forwarded": true, + "referred_product": { + "catalog_id": "CATALOG_ID", + "product_retailer_id": "PRODUCT_ID" + } + }, + "referral": { + "source_url": "AD_OR_POST_FB_URL", + "source_id": "ADID", + "source_type": "ad or post", + "headline": "AD_TITLE", + "body": "AD_DESCRIPTION", + "media_type": "image or video", + "image_url": "RAW_IMAGE_URL", + "video_url": "RAW_VIDEO_URL", + "thumbnail_url": "RAW_THUMBNAIL_URL" + }, + "from": "16315551234", + "id": "wamid.ID", + "timestamp": 1669233778, + "type": "button", + "button": { + "text": "No", + "payload": "No-Button-Payload" + } + }] + }, + "field": "messages" + }] + }] + }', true); + + $notification = $this->notification_factory->buildFromPayload($payload); + + $this->assertEquals('wamid.ID', $notification->replyingToMessageId()); + $this->assertEquals('PHONE_NUMBER_ID', $notification->businessPhoneNumberId()); + $this->assertEquals('PHONE_NUMBER', $notification->businessPhoneNumber()); + $this->assertTrue($notification->isForwarded()); + $this->assertEquals('WHATSAPP_ID', $notification->customer()->id()); + $this->assertEquals('NAME', $notification->customer()->name()); + $this->assertEquals('ADID', $notification->referral()->sourceId()); + $this->assertEquals('AD_OR_POST_FB_URL', $notification->referral()->sourceUrl()); + $this->assertEquals('ad or post', $notification->referral()->sourceType()); + $this->assertEquals('AD_TITLE', $notification->referral()->headline()); + $this->assertEquals('AD_DESCRIPTION', $notification->referral()->body()); + $this->assertEquals('image or video', $notification->referral()->mediaType()); + $this->assertEquals('RAW_IMAGE_URL', $notification->referral()->mediaUrl()); + $this->assertEquals('RAW_THUMBNAIL_URL', $notification->referral()->thumbnailUrl()); + $this->assertEquals('CATALOG_ID', $notification->context()->catalogId()); + $this->assertEquals('PRODUCT_ID', $notification->context()->productRetailerId()); + } + + public function test_build_from_payload_can_build_a_text_notification() + { + $payload = json_decode('{ + "object": "whatsapp_business_account", + "entry": [{ + "id": "WHATSAPP_BUSINESS_ACCOUNT_ID", + "changes": [{ + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "PHONE_NUMBER", + "phone_number_id": "PHONE_NUMBER_ID" + }, + "contacts": [{ + "profile": { + "name": "NAME" + }, + "wa_id": "PHONE_NUMBER" + }], + "messages": [{ + "from": "PHONE_NUMBER", + "id": "wamid.ID", + "timestamp": "1669233778", + "text": { + "body": "MESSAGE_BODY" + }, + "type": "text" + }] + }, + "field": "messages" + }] + }] + }', true); + + $notification = $this->notification_factory->buildFromPayload($payload); + + $this->assertInstanceOf(Notification\Text::class, $notification); + $this->assertEquals('MESSAGE_BODY', $notification->message()); + } + + public function test_build_from_payload_can_build_a_reaction_notification() + { + $payload = json_decode('{ + "object": "whatsapp_business_account", + "entry": [{ + "id": "WHATSAPP_BUSINESS_ACCOUNT_ID", + "changes": [{ + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "PHONE_NUMBER", + "phone_number_id": "PHONE_NUMBER_ID" + }, + "contacts": [{ + "profile": { + "name": "NAME" + }, + "wa_id": "PHONE_NUMBER" + }], + "messages": [{ + "from": "PHONE_NUMBER", + "id": "wamid.ID", + "timestamp": "1669233778", + "reaction": { + "message_id": "MESSAGE_ID", + "emoji": "EMOJI" + }, + "type": "reaction" + }] + }, + "field": "messages" + }] + }] + }', true); + + $notification = $this->notification_factory->buildFromPayload($payload); + + $this->assertInstanceOf(Notification\Reaction::class, $notification); + $this->assertEquals('MESSAGE_ID', $notification->messageId()); + $this->assertEquals('EMOJI', $notification->emoji()); + } + + public function test_build_from_payload_can_build_an_image_notification() + { + $payload = json_decode('{ + "object": "whatsapp_business_account", + "entry": [{ + "id": "WHATSAPP_BUSINESS_ACCOUNT_ID", + "changes": [{ + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "PHONE_NUMBER", + "phone_number_id": "PHONE_NUMBER_ID" + }, + "contacts": [{ + "profile": { + "name": "NAME" + }, + "wa_id": "WHATSAPP_ID" + }], + "messages": [{ + "from": "PHONE_NUMBER", + "id": "wamid.ID", + "timestamp": "1669233778", + "type": "image", + "image": { + "caption": "CAPTION", + "mime_type": "image/jpeg", + "sha256": "IMAGE_HASH", + "id": "IMAGE_ID", + "caption": "CAPTION_TEXT" + } + }] + }, + "field": "messages" + }] + }] + }', true); + + $notification = $this->notification_factory->buildFromPayload($payload); + + $this->assertInstanceOf(Notification\Media::class, $notification); + $this->assertEquals('IMAGE_ID', $notification->imageId()); + $this->assertEquals('image/jpeg', $notification->mimeType()); + $this->assertEquals('CAPTION_TEXT', $notification->caption()); + } + + public function test_build_from_payload_can_build_a_sticker_notification() + { + $payload = json_decode('{ + "object": "whatsapp_business_account", + "entry": [ + { + "id": "ID", + "changes": [ + { + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "PHONE_NUMBER", + "phone_number_id": "PHONE_NUMBER_ID" + }, + "contacts": [ + { + "profile": { + "name": "NAME" + }, + "wa_id": "ID" + } + ], + "messages": [ + { + "from": "SENDER_PHONE_NUMBER", + "id": "wamid.ID", + "timestamp": "1669233778", + "type": "sticker", + "sticker": { + "mime_type": "image/webp", + "sha256": "HASH", + "id": "STICKER_ID" + } + } + ] + }, + "field": "messages" + } + ] + } + ] + }', true); + + $notification = $this->notification_factory->buildFromPayload($payload); + + $this->assertInstanceOf(Notification\Media::class, $notification); + $this->assertEquals('STICKER_ID', $notification->imageId()); + $this->assertEquals('image/webp', $notification->mimeType()); + } + + public function test_build_from_payload_can_build_an_unknown_notification() + { + $payload = json_decode('{ + "object": "whatsapp_business_account", + "entry": [{ + "id": "WHATSAPP_BUSINESS_ACCOUNT_ID", + "changes": [{ + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "PHONE_NUMBER", + "phone_number_id": "PHONE_NUMBER_ID" + }, + "contacts": [{ + "profile": { + "name": "NAME" + }, + "wa_id": "WHATSAPP_ID" + }], + "messages": [{ + "from": "PHONE_NUMBER", + "id": "wamid.ID", + "timestamp": "1669233778", + "errors": [ + { + "code": 131051, + "details": "Message type is not currently supported", + "title": "Unsupported message type" + }], + "type": "unknown" + }] + }, + "field": "messages" + }] + }] + }', true); + + $notification = $this->notification_factory->buildFromPayload($payload); + + $this->assertInstanceOf(Notification\Unknown::class, $notification); + } + + public function test_build_from_payload_can_build_a_location_notification() + { + $payload = json_decode('{ + "object": "whatsapp_business_account", + "entry": [{ + "id": "WHATSAPP_BUSINESS_ACCOUNT_ID", + "changes": [{ + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "PHONE_NUMBER", + "phone_number_id": "PHONE_NUMBER_ID" + }, + "contacts": [{ + "profile": { + "name": "NAME" + }, + "wa_id": "WHATSAPP_ID" + }], + "messages": [{ + "from": "PHONE_NUMBER", + "id": "wamid.ID", + "timestamp": "1669233778", + "type": "location", + "location": { + "latitude": "LOCATION_LATITUDE", + "longitude": "LOCATION_LONGITUDE", + "name": "LOCATION_NAME", + "address": "LOCATION_ADDRESS" + } + }] + }, + "field": "messages" + }] + }] + }', true); + + $notification = $this->notification_factory->buildFromPayload($payload); + + $this->assertInstanceOf(Notification\Location::class, $notification); + $this->assertEquals('LOCATION_LATITUDE', $notification->latitude()); + $this->assertEquals('LOCATION_LONGITUDE', $notification->longitude()); + $this->assertEquals('LOCATION_NAME', $notification->name()); + $this->assertEquals('LOCATION_ADDRESS', $notification->address()); + } + + public function test_build_from_payload_can_build_a_contact_notification() + { + $payload = json_decode('{ + "object":"whatsapp_business_account", + "entry":[{ + "id":"WHATSAPP_BUSINESS_ACCOUNT_ID", + "changes":[{ + "value":{ + "messaging_product":"whatsapp", + "metadata": { + "display_phone_number":"PHONE_NUMBER", + "phone_number_id":"PHONE_NUMBER_ID" + }, + "contacts": [{ + "profile":{ + "name":"NAME" + }, + "wa_id":"WHATSAPP_ID" + }], + "messages":[{ + "from":"PHONE_NUMBER", + "id":"wamid.ID", + "timestamp":"1669233778", + "type": "contacts", + "contacts":[{ + "addresses":[{ + "city":"CONTACT_CITY", + "country":"CONTACT_COUNTRY", + "country_code":"CONTACT_COUNTRY_CODE", + "state":"CONTACT_STATE", + "street":"CONTACT_STREET", + "type":"HOME or WORK", + "zip":"CONTACT_ZIP" + }], + "birthday":"1989-03-16", + "emails":[{ + "email":"CONTACT_EMAIL", + "type":"WORK" + }], + "name":{ + "formatted_name":"CONTACT_FORMATTED_NAME", + "first_name":"CONTACT_FIRST_NAME", + "last_name":"CONTACT_LAST_NAME", + "middle_name":"CONTACT_MIDDLE_NAME", + "suffix":"CONTACT_SUFFIX", + "prefix":"CONTACT_PREFIX" + }, + "org":{ + "company":"CONTACT_ORG_COMPANY", + "department":"CONTACT_ORG_DEPARTMENT", + "title":"CONTACT_ORG_TITLE" + }, + "phones":[{ + "phone":"CONTACT_PHONE", + "wa_id":"CONTACT_WA_ID", + "type":"HOME or WORK>" + }], + "urls":[{ + "url":"CONTACT_URL", + "type":"HOME or WORK" + }] + }] + }] + }, + "field":"messages" + }] + }] + }', true); + + $notification = $this->notification_factory->buildFromPayload($payload); + + $this->assertInstanceOf(Notification\Contact::class, $notification); + $this->assertIsArray($notification->name()); + $this->assertEquals('CONTACT_FORMATTED_NAME', $notification->formattedName()); + $this->assertEquals('CONTACT_FIRST_NAME', $notification->firstName()); + $this->assertEquals('CONTACT_LAST_NAME', $notification->lastName()); + $this->assertEquals('CONTACT_MIDDLE_NAME', $notification->middleName()); + $this->assertIsArray($notification->addresses()); + $this->assertInstanceOf(\DateTimeImmutable::class, $notification->birthday()); + $this->assertIsArray($notification->emails()); + $this->assertEquals('CONTACT_EMAIL', $notification->emails()[0]['email']); + $this->assertEquals('WORK', $notification->emails()[0]['type']); + $this->assertIsArray($notification->company()); + $this->assertEquals('CONTACT_ORG_COMPANY', $notification->companyName()); + $this->assertEquals('CONTACT_ORG_DEPARTMENT', $notification->companyDepartment()); + $this->assertEquals('CONTACT_ORG_TITLE', $notification->companyTitle()); + $this->assertIsArray($notification->phones()); + $this->assertEquals('CONTACT_PHONE', $notification->phones()[0]['phone']); + $this->assertIsArray($notification->urls()); + $this->assertEquals('CONTACT_URL', $notification->urls()[0]['url']); + } + + public function test_build_from_payload_can_build_a_button_notification() + { + $payload = json_decode('{ + "object": "whatsapp_business_account", + "entry": [{ + "id": "WHATSAPP_BUSINESS_ACCOUNT_ID", + "changes": [{ + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "PHONE_NUMBER", + "phone_number_id": "PHONE_NUMBER_ID" + }, + "contacts": [{ + "profile": { + "name": "NAME" + }, + "wa_id": "WHATSAPP_ID" + }], + "messages": [{ + "context": { + "from": "PHONE_NUMBER", + "id": "wamid.ID" + }, + "from": "16315551234", + "id": "wamid.ID", + "timestamp": 1669233778, + "type": "button", + "button": { + "text": "No", + "payload": "No-Button-Payload" + } + }] + }, + "field": "messages" + }] + }] + }', true); + + $notification = $this->notification_factory->buildFromPayload($payload); + + $this->assertInstanceOf(Notification\Button::class, $notification); + $this->assertEquals('No', $notification->text()); + $this->assertEquals('No-Button-Payload', $notification->payload()); + } + + public function test_build_from_payload_can_build_a_list_notification() + { + $payload = json_decode('{ + "object": "whatsapp_business_account", + "entry": [ + { + "id": "WHATSAPP_BUSINESS_ACCOUNT_ID", + "changes": [ + { + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "PHONE_NUMBER", + "phone_number_id": "PHONE_NUMBER_ID" + }, + "contacts": [ + { + "profile": { + "name": "NAME" + }, + "wa_id": "PHONE_NUMBER_ID" + } + ], + "messages": [ + { + "from": "PHONE_NUMBER_ID", + "id": "wamid.ID", + "timestamp": 1669233778, + "interactive": { + "list_reply": { + "id": "list_reply_id", + "title": "list_reply_title", + "description": "list_reply_description" + }, + "type": "list_reply" + }, + "type": "interactive" + } + ] + }, + "field": "messages" + } + ] + } + ] + }', true); + + $notification = $this->notification_factory->buildFromPayload($payload); + + $this->assertInstanceOf(Notification\Interactive::class, $notification); + $this->assertEquals('list_reply_id', $notification->itemId()); + $this->assertEquals('list_reply_title', $notification->title()); + $this->assertEquals('list_reply_description', $notification->description()); + } + + public function test_build_from_payload_can_build_a_button_reply_notification() + { + $payload = json_decode('{ + "object": "whatsapp_business_account", + "entry": [ + { + "id": "WHATSAPP_BUSINESS_ACCOUNT_ID", + "changes": [ + { + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "PHONE_NUMBER", + "phone_number_id": "PHONE_NUMBER_ID" + }, + "contacts": [ + { + "profile": { + "name": "NAME" + }, + "wa_id": "PHONE_NUMBER_ID" + } + ], + "messages": [ + { + "from": "PHONE_NUMBER_ID", + "id": "wamid.ID", + "timestamp": 1669233778, + "interactive": { + "button_reply": { + "id": "unique-button-identifier-here", + "title": "button-text" + }, + "type": "button_reply" + }, + "type": "interactive" + } + ] + }, + "field": "messages" + } + ] + } + ] + }', true); + + $notification = $this->notification_factory->buildFromPayload($payload); + + $this->assertInstanceOf(Notification\Interactive::class, $notification); + $this->assertEquals('unique-button-identifier-here', $notification->itemId()); + $this->assertEquals('button-text', $notification->title()); + $this->assertEquals('', $notification->description()); + } + + public function test_build_from_payload_can_build_an_order_notification() + { + $payload = json_decode('{ + "object": "whatsapp_business_account", + "entry": [ + { + "id": "WHATSAPP_BUSINESS_ACCOUNT_ID", + "changes": [ + { + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "PHONE_NUMBER", + "phone_number_id": "PHONE_NUMBER_ID" + }, + "contacts": [ + { + "profile": { + "name": "NAME" + }, + "wa_id": "PHONE_NUMBER_ID" + } + ], + "messages": [ + { + "from": "PHONE_NUMBER_ID", + "id": "wamid.ID", + "timestamp": 1669233778, + "order": { + "catalog_id": "the-catalog_id", + "product_items": [ + { + "product_retailer_id":"the-product-SKU-identifier", + "quantity":"number-of-item", + "item_price":"unitary-price-of-item", + "currency":"price-currency" + }, + { + "product_retailer_id":"the-product-SKU-identifier-2", + "quantity":"number-of-item", + "item_price":"unitary-price-of-item", + "currency":"price-currency" + } + ], + "text":"text-message-sent-along-with-the-order" + }, + "type": "order" + } + ] + }, + "field": "messages" + } + ] + } + ] + }', true); + + $notification = $this->notification_factory->buildFromPayload($payload); + + $this->assertInstanceOf(Notification\Order::class, $notification); + $this->assertEquals('the-catalog_id', $notification->catalogId()); + $this->assertEquals('text-message-sent-along-with-the-order', $notification->message()); + $this->assertEquals('the-product-SKU-identifier', $notification->products()->productRetailerId()); + $this->assertEquals('number-of-item', $notification->products()->quantity()); + $this->assertEquals('unitary-price-of-item', $notification->products()->price()); + $this->assertEquals('price-currency', $notification->products()->currency()); + } + + public function test_build_from_payload_can_build_a_system_notification() + { + $payload = json_decode('{ + "object": "whatsapp_business_account", + "entry": [{ + "id": "WHATSAPP_BUSINESS_ACCOUNT_ID", + "changes": [{ + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "PHONE_NUMBER", + "phone_number_id": "NEW_PHONE_NUMBER_ID" + }, + "messages": [{ + "from": "PHONE_NUMBER", + "id": "wamid.ID", + "system": { + "body": "NAME changed from PHONE_NUMBER to PHONE_NUMBER", + "wa_id": "NEW_PHONE_NUMBER_ID", + "type": "user_changed_number", + "customer": "OLD_PHONE_NUMBER_ID" + }, + "timestamp": 1669233778, + "type": "system" + }] + }, + "field": "messages" + }] + }] + }', true); + + $notification = $this->notification_factory->buildFromPayload($payload); + + $this->assertInstanceOf(Notification\System::class, $notification); + $this->assertEquals('NEW_PHONE_NUMBER_ID', $notification->businessPhoneNumberId()); + $this->assertEquals('OLD_PHONE_NUMBER_ID', $notification->oldBusinessPhoneNumberId()); + } + + public function test_build_from_payload_can_build_a_status_notification() + { + $payload = json_decode('{ + "object": "whatsapp_business_account", + "entry": [ + { + "id": "114957184830690", + "changes": [ + { + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "CUSTOMER_PHONE_NUMBER", + "phone_number_id": "CUSTOMER_PHONE_NUMBER" + }, + "statuses": [ + { + "id": "wamid.ID", + "status": "read", + "timestamp": "1674914356", + "recipient_id": "CUSTOMER_PHONE_NUMBER" + } + ] + }, + "field": "messages" + } + ] + } + ] + }', true); + + $notification = $this->notification_factory->buildFromPayload($payload); + + $this->assertInstanceOf(Notification\StatusNotification::class, $notification); + $this->assertEquals('wamid.ID', $notification->id()); + $this->assertEquals('CUSTOMER_PHONE_NUMBER', $notification->customerId()); + $this->assertNull($notification->isBusinessInitiatedConversation()); + $this->assertNull($notification->isCustomerInitiatedConversation()); + $this->assertNull($notification->isReferralInitiatedConversation()); + $this->assertEquals('read', $notification->status()); + $this->assertTrue($notification->isMessageRead()); + $this->assertTrue($notification->isMessageDelivered()); + $this->assertTrue($notification->isMessageSent()); + } + + public function test_build_from_payload_can_build_a_status_conversation_initiated_notification() + { + $payload = json_decode('{ + "object": "whatsapp_business_account", + "entry": [{ + "id": "WHATSAPP_BUSINESS_ACCOUNT_ID", + "changes": [{ + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "PHONE_NUMBER", + "phone_number_id": "PHONE_NUMBER_ID" + }, + "statuses": [{ + "id": "wamid.ID", + "recipient_id": "CUSTOMER_PHONE_NUMBER", + "status": "read", + "timestamp": "1669233778", + "conversation": { + "id": "CONVERSATION_ID", + "expiration_timestamp": 1669233778, + "origin": { + "type": "customer_initiated" + } + } + }] + }, + "field": "messages" + }] + }] + }', true); + + $notification = $this->notification_factory->buildFromPayload($payload); + + $this->assertInstanceOf(Notification\StatusNotification::class, $notification); + $this->assertEquals('wamid.ID', $notification->id()); + $this->assertEquals('CUSTOMER_PHONE_NUMBER', $notification->customerId()); + $this->assertEquals('CONVERSATION_ID', $notification->conversationId()); + $this->assertEquals('1669233778', $notification->conversationExpiresAt()->getTimestamp()); + $this->assertFalse($notification->isBusinessInitiatedConversation()); + $this->assertTrue($notification->isCustomerInitiatedConversation()); + $this->assertFalse($notification->isReferralInitiatedConversation()); + $this->assertEquals('read', $notification->status()); + $this->assertTrue($notification->isMessageRead()); + $this->assertTrue($notification->isMessageDelivered()); + $this->assertTrue($notification->isMessageSent()); + } + + public function test_build_from_payload_can_build_a_status_notification_without_expiration_time() + { + $payload = json_decode('{ + "object": "whatsapp_business_account", + "entry": [{ + "id": "WHATSAPP_BUSINESS_ACCOUNT_ID", + "changes": [{ + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "PHONE_NUMBER", + "phone_number_id": "PHONE_NUMBER_ID" + }, + "statuses": [{ + "id": "wamid.ID", + "recipient_id": "CUSTOMER_PHONE_NUMBER", + "status": "delivered", + "timestamp": "1669233778", + "conversation": { + "id": "CONVERSATION_ID", + "origin": { + "type": "customer_initiated" + } + } + }] + }, + "field": "messages" + }] + }] + }', true); + + $notification = $this->notification_factory->buildFromPayload($payload); + + $this->assertNull($notification->conversationExpiresAt()); + } + + public function test_build_from_payload_can_build_a_status_notification_with_errors() + { + $payload = json_decode('{ + "object": "whatsapp_business_account", + "entry": [ + { + "id": "114957184830690", + "changes": [ + { + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "15550483457", + "phone_number_id": "102944729380254" + }, + "statuses": [ + { + "id": "amid.ID", + "status": "failed", + "timestamp": "1674912647", + "recipient_id": "CUSTOMER_PHONE_NUMBER", + "errors": [ + { + "code": 131053, + "title": "ERROR_TITLE" + } + ] + } + ] + }, + "field": "messages" + } + ] + } + ] + }', true); + + $notification = $this->notification_factory->buildFromPayload($payload); + + $this->assertEquals('failed', $notification->status()); + $this->assertFalse($notification->isMessageRead()); + $this->assertFalse($notification->isMessageDelivered()); + $this->assertFalse($notification->isMessageSent()); + $this->assertTrue($notification->hasErrors()); + $this->assertEquals(131053, $notification->errorCode()); + $this->assertEquals('ERROR_TITLE', $notification->errorTitle()); + } + + public function test_build_from_payload_return_null_when_payload_is_empty() + { + $notification = $this->notification_factory->buildFromPayload([]); + + $this->assertNull($notification); + + $notification = $this->notification_factory->buildFromPayload(['entry' => []]); + + $this->assertNull($notification); + } +} From 06db992e5b44ee511613a6705d419da79698f422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Sat, 28 Jan 2023 18:34:40 +0100 Subject: [PATCH 15/20] webhook: update README --- README.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/README.md b/README.md index c90312b..f9eec7b 100644 --- a/README.md +++ b/README.md @@ -237,6 +237,47 @@ try { } ``` +## Webhooks + +### Webhook verification +Add your webhook in your Meta App dashboard. You need to verify your webhook: + +```php +verify($_GET, ""); +``` + +### Webhook notifications +Webhook is now verified, you will start receiving notifications every time your customers send messages. + + +```php +read(json_decode($payload, true)), true) . "\n"); +``` + +The `Webhook::read` function will return a `Notification` instance. Please, [explore](https://github.com/netflie/whatsapp-cloud-api/tree/main/src/WebHook/Notification "explore") the different notifications availables. + ## Features - Send Text Messages From 392f291502e59590a6a79db9b8912ae0d2a27e38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Sun, 29 Jan 2023 13:56:18 +0100 Subject: [PATCH 16/20] webhook: add support to mark messages as read --- src/Client.php | 2 +- src/Request/MessageReadRequest.php | 61 +++++++++++++++++++++++++++++ src/Request/MessageRequest.php | 9 +---- src/Request/RequestWithBody.php | 13 ++++++ src/WhatsAppCloudApi.php | 21 ++++++++++ tests/Unit/WhatsAppCloudApiTest.php | 29 ++++++++++++++ 6 files changed, 126 insertions(+), 9 deletions(-) create mode 100644 src/Request/MessageReadRequest.php create mode 100644 src/Request/RequestWithBody.php diff --git a/src/Client.php b/src/Client.php index 368a94d..64fd066 100644 --- a/src/Client.php +++ b/src/Client.php @@ -42,7 +42,7 @@ public function __construct(string $graph_version, ?ClientHandler $handler = nul * * @throws Netflie\WhatsAppCloudApi\Response\ResponseException */ - public function sendMessage(Request\MessageRequest $request): Response + public function sendMessage(Request\RequestWithBody $request): Response { $raw_response = $this->handler->postJsonData( $this->buildRequestUri($request->nodePath()), diff --git a/src/Request/MessageReadRequest.php b/src/Request/MessageReadRequest.php new file mode 100644 index 0000000..9d9e584 --- /dev/null +++ b/src/Request/MessageReadRequest.php @@ -0,0 +1,61 @@ +message_id = $message_id; + $this->from_phone_number_id = $from_phone_number_id; + + parent::__construct($access_token, $timeout); + } + + /** + * Returns the raw body of the request. + * + * @return array + */ + public function body(): array + { + return [ + 'messaging_product' => 'whatsapp', + 'status' => 'read', + 'message_id' => $this->message_id, + ]; + } + + /** + * Return WhatsApp Number Id for this request. + * + * @return string + */ + public function fromPhoneNumberId(): string + { + return $this->from_phone_number_id; + } + + /** + * WhatsApp node path. + * + * @return string + */ + public function nodePath(): string + { + return $this->from_phone_number_id . '/messages'; + } +} diff --git a/src/Request/MessageRequest.php b/src/Request/MessageRequest.php index 9301008..7f0eb05 100644 --- a/src/Request/MessageRequest.php +++ b/src/Request/MessageRequest.php @@ -5,7 +5,7 @@ use Netflie\WhatsAppCloudApi\Message\Message; use Netflie\WhatsAppCloudApi\Request; -abstract class MessageRequest extends Request +abstract class MessageRequest extends Request implements RequestWithBody { /** * @var Message WhatsApp Message to be sent. @@ -25,13 +25,6 @@ public function __construct(Message $message, string $access_token, string $from parent::__construct($access_token, $timeout); } - /** - * Returns the raw body of the request. - * - * @return array - */ - abstract public function body(): array; - /** * Return WhatsApp Number Id for this request. * diff --git a/src/Request/RequestWithBody.php b/src/Request/RequestWithBody.php new file mode 100644 index 0000000..9c2ccaf --- /dev/null +++ b/src/Request/RequestWithBody.php @@ -0,0 +1,13 @@ +client->downloadMedia($request); } + /** + * Mark a message as read + * + * @param string $message_id WhatsApp Message Id will be marked as read. + * + * @return Response + * + * @throws Response\ResponseException + */ + public function markMessageAsRead(string $message_id): Response + { + $request = new Request\MessageReadRequest( + $message_id, + $this->app->accessToken(), + $this->app->fromPhoneNumberId(), + $this->timeout + ); + + return $this->client->sendMessage($request); + } + /** * Returns the Facebook Whatsapp Access Token. * diff --git a/tests/Unit/WhatsAppCloudApiTest.php b/tests/Unit/WhatsAppCloudApiTest.php index c09cf9c..f8b2d2d 100644 --- a/tests/Unit/WhatsAppCloudApiTest.php +++ b/tests/Unit/WhatsAppCloudApiTest.php @@ -898,6 +898,35 @@ public function test_download_media() $this->assertEquals(false, $response->isError()); } + public function test_mark_a_message_as_read() + { + $to = $this->faker->phoneNumber; + $url = $this->buildMessageRequestUri(); + $text_message = $this->faker->text; + $preview_url = $this->faker->boolean; + + $body = [ + 'messaging_product' => 'whatsapp', + 'status' => 'read', + 'message_id' => '', + ]; + $headers = [ + 'Authorization' => 'Bearer ' . $this->access_token, + ]; + + $this->client_handler + ->postJsonData($url, $body, $headers, Argument::type('int')) + ->shouldBeCalled() + ->willReturn(new RawResponse($headers, $this->successfulMessageNodeResponse(), 200)); + + $response = $this->whatsapp_app_cloud_api->markMessageAsRead(''); + + $this->assertEquals(200, $response->httpStatusCode()); + $this->assertEquals(json_decode($this->successfulMessageNodeResponse(), true), $response->decodedBody()); + $this->assertEquals($this->successfulMessageNodeResponse(), $response->body()); + $this->assertEquals(false, $response->isError()); + } + private function buildBaseUri(): string { return Client::BASE_GRAPH_URL . '/' . static::TEST_GRAPH_VERSION . '/'; From e8c24532e76d81e07cebc843e1d83e5734090bc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Sun, 29 Jan 2023 15:41:08 +0100 Subject: [PATCH 17/20] 2.x: update README --- README.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c90312b..9abc78d 100644 --- a/README.md +++ b/README.md @@ -222,8 +222,30 @@ $whatsapp_cloud_api->sendList( ); ``` +## Media messages +### Upload media resources +Media messages accept as identifiers an Internet URL pointing to a public resource (image, video, audio, etc.). When you try to send a media message from a URL you must instantiate the `LinkID` object. + +You can also upload your media resources to WhatsApp servers and you will receive a resource identifier: + +```php +$response = $whatsapp_cloud_api->uploadMedia('my-image.png'); + +$media_id = new MediaObjectID($response->decodedBody()['id']); +$whatsapp_cloud_api->sendImage('', $media_id); + +``` + +### Upload media resources +To download a media resource: + +```php +$response = $whatsapp_cloud_api->downloadMedia(''); +``` + + ## Message Response -WhatsAppCloudApi instance returns a Response class or a ClientException if WhatsApp servers return an error. +WhatsAppCloudApi instance returns a Response class or a ResponseException if WhatsApp servers return an error. ```php try { From d19247ad25fc73d4f96dedeb0c36c9744ab428c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Sun, 29 Jan 2023 15:50:45 +0100 Subject: [PATCH 18/20] 2.x: update README --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 6a5421b..e69cd4b 100644 --- a/README.md +++ b/README.md @@ -312,6 +312,11 @@ The `Webhook::read` function will return a `Notification` instance. Please, [exp - Send Locations - Send Contacts - Send Lists +- Upload media resources to WhatsApp servers +- Download media resources from WhatsApp servers +- Mark messages as read +- Webhook verification +- Webhook notifications ## Getting Help - Ask a question on the [Discussions forum](https://github.com/netflie/whatsapp-cloud-api/discussions "Discussions forum") From ecfeb9a2aa9b67de7b376906a9955ac0fc8ab06c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Sun, 29 Jan 2023 16:09:15 +0100 Subject: [PATCH 19/20] 2.x: remove some imports --- src/WhatsAppCloudApi.php | 67 +++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 39 deletions(-) diff --git a/src/WhatsAppCloudApi.php b/src/WhatsAppCloudApi.php index dabf7a3..256c087 100644 --- a/src/WhatsAppCloudApi.php +++ b/src/WhatsAppCloudApi.php @@ -2,22 +2,11 @@ namespace Netflie\WhatsAppCloudApi; -use Netflie\WhatsAppCloudApi\Message\AudioMessage; use Netflie\WhatsAppCloudApi\Message\Contact\ContactName; use Netflie\WhatsAppCloudApi\Message\Contact\Phone; -use Netflie\WhatsAppCloudApi\Message\ContactMessage; -use Netflie\WhatsAppCloudApi\Message\Document\Document; -use Netflie\WhatsAppCloudApi\Message\DocumentMessage; -use Netflie\WhatsAppCloudApi\Message\ImageMessage; -use Netflie\WhatsAppCloudApi\Message\LocationMessage; use Netflie\WhatsAppCloudApi\Message\Media\MediaID; use Netflie\WhatsAppCloudApi\Message\OptionsList\Action; -use Netflie\WhatsAppCloudApi\Message\OptionsListMessage; -use Netflie\WhatsAppCloudApi\Message\StickerMessage; use Netflie\WhatsAppCloudApi\Message\Template\Component; -use Netflie\WhatsAppCloudApi\Message\TemplateMessage; -use Netflie\WhatsAppCloudApi\Message\TextMessage; -use Netflie\WhatsAppCloudApi\Message\VideoMessage; class WhatsAppCloudApi { @@ -73,7 +62,7 @@ public function __construct(array $config) */ public function sendTextMessage(string $to, string $text, bool $preview_url = false): Response { - $message = new TextMessage($to, $text, $preview_url); + $message = new Message\TextMessage($to, $text, $preview_url); $request = new Request\MessageRequest\RequestTextMessage( $message, $this->app->accessToken(), @@ -89,14 +78,14 @@ public function sendTextMessage(string $to, string $text, bool $preview_url = fa * can put any public URL of some document uploaded on Internet. * * @param string $to WhatsApp ID or phone number for the person you want to send a message to. - * @param Document $document Document to send. See documents accepted in the Message/Document folder. + * @param MediaID $document_id WhatsApp Media ID or any Internet public document link. * @return Response * * @throws Response\ResponseException */ public function sendDocument(string $to, MediaID $document_id, string $name, ?string $caption): Response { - $message = new DocumentMessage($to, $document_id, $name, $caption); + $message = new Message\DocumentMessage($to, $document_id, $name, $caption); $request = new Request\MessageRequest\RequestDocumentMessage( $message, $this->app->accessToken(), @@ -122,7 +111,7 @@ public function sendDocument(string $to, MediaID $document_id, string $name, ?st */ public function sendTemplate(string $to, string $template_name, string $language = 'en_US', ?Component $components = null): Response { - $message = new TemplateMessage($to, $template_name, $language, $components); + $message = new Message\TemplateMessage($to, $template_name, $language, $components); $request = new Request\MessageRequest\RequestTemplateMessage( $message, $this->app->accessToken(), @@ -134,18 +123,18 @@ public function sendTemplate(string $to, string $template_name, string $language } /** - * Sends a document uploaded to the WhatsApp Cloud servers by it Media ID or you also - * can put any public URL of some document uploaded on Internet. + * Sends an audio uploaded to the WhatsApp Cloud servers by it Media ID or you also + * can put any public URL of some audio uploaded on Internet. * - * @param string $to WhatsApp ID or phone number for the person you want to send a message to. - * @param MediaId $document_id WhatsApp Media ID or any Internet public link document. + * @param string $to WhatsApp ID or phone number for the person you want to send a message to. + * @param MediaId $audio_id WhatsApp Media ID or any Internet public audio link. * @return Response * * @throws Response\ResponseException */ - public function sendAudio(string $to, MediaID $document_id): Response + public function sendAudio(string $to, MediaID $audio_id): Response { - $message = new AudioMessage($to, $document_id); + $message = new Message\AudioMessage($to, $audio_id); $request = new Request\MessageRequest\RequestAudioMessage( $message, $this->app->accessToken(), @@ -157,19 +146,19 @@ public function sendAudio(string $to, MediaID $document_id): Response } /** - * Sends a document uploaded to the WhatsApp Cloud servers by it Media ID or you also - * can put any public URL of some document uploaded on Internet. + * Sends an image uploaded to the WhatsApp Cloud servers by it Media ID or you also + * can put any public URL of some image uploaded on Internet. * * @param string $to WhatsApp ID or phone number for the person you want to send a message to. * @param string $caption Description of the specified image file. - * @param MediaId $document_id WhatsApp Media ID or any Internet public link document. + * @param MediaId $image_id WhatsApp Media ID or any Internet public image link. * @return Response * * @throws Response\ResponseException */ - public function sendImage(string $to, MediaID $document_id, ?string $caption = ''): Response + public function sendImage(string $to, MediaID $image_id, ?string $caption = ''): Response { - $message = new ImageMessage($to, $document_id, $caption); + $message = new Message\ImageMessage($to, $image_id, $caption); $request = new Request\MessageRequest\RequestImageMessage( $message, $this->app->accessToken(), @@ -181,18 +170,18 @@ public function sendImage(string $to, MediaID $document_id, ?string $caption = ' } /** - * Sends a document uploaded to the WhatsApp Cloud servers by it Media ID or you also - * can put any public URL of some document uploaded on Internet. + * Sends a video uploaded to the WhatsApp Cloud servers by it Media ID or you also + * can put any public URL of some video uploaded on Internet. * - * @param string $to WhatsApp ID or phone number for the person you want to send a message to. - * @param MediaId $document_id WhatsApp Media ID or any Internet public link document. + * @param string $to WhatsApp ID or phone number for the person you want to send a message to. + * @param MediaId $video_id WhatsApp Media ID or any Internet public video link. * @return Response * * @throws Response\ResponseException */ - public function sendVideo(string $to, MediaID $link, string $caption = ''): Response + public function sendVideo(string $to, MediaID $video_id, string $caption = ''): Response { - $message = new VideoMessage($to, $link, $caption); + $message = new Message\VideoMessage($to, $video_id, $caption); $request = new Request\MessageRequest\RequestVideoMessage( $message, $this->app->accessToken(), @@ -205,17 +194,17 @@ public function sendVideo(string $to, MediaID $link, string $caption = ''): Resp /** * Sends a sticker uploaded to the WhatsApp Cloud servers by it Media ID or you also - * can put any public URL of some document uploaded on Internet. + * can put any public URL of some sticker uploaded on Internet. * * @param string $to WhatsApp ID or phone number for the person you want to send a message to. - * @param MediaId $document_id WhatsApp Media ID or any Internet public link document. + * @param MediaId $sticker_id WhatsApp Media ID or any Internet public sticker link. * @return Response * * @throws Response\ResponseException */ - public function sendSticker(string $to, MediaID $link): Response + public function sendSticker(string $to, MediaID $sticker_id): Response { - $message = new StickerMessage($to, $link); + $message = new Message\StickerMessage($to, $sticker_id); $request = new Request\MessageRequest\RequestStickerMessage( $message, $this->app->accessToken(), @@ -241,7 +230,7 @@ public function sendSticker(string $to, MediaID $link): Response */ public function sendLocation(string $to, float $longitude, float $latitude, string $name = '', string $address = ''): Response { - $message = new LocationMessage($to, $longitude, $latitude, $name, $address); + $message = new Message\LocationMessage($to, $longitude, $latitude, $name, $address); $request = new Request\MessageRequest\RequestLocationMessage( $message, $this->app->accessToken(), @@ -265,7 +254,7 @@ public function sendLocation(string $to, float $longitude, float $latitude, stri */ public function sendContact(string $to, ContactName $name, Phone ...$phone): Response { - $message = new ContactMessage($to, $name, ...$phone); + $message = new Message\ContactMessage($to, $name, ...$phone); $request = new Request\MessageRequest\RequestContactMessage( $message, $this->app->accessToken(), @@ -278,7 +267,7 @@ public function sendContact(string $to, ContactName $name, Phone ...$phone): Res public function sendList(string $to, string $header, string $body, string $footer, Action $action): Response { - $message = new OptionsListMessage($to, $header, $body, $footer, $action); + $message = new Message\OptionsListMessage($to, $header, $body, $footer, $action); $request = new Request\MessageRequest\RequestOptionsListMessage( $message, $this->app->accessToken(), From e413979d9ac7f5dd34b2bd0eb3d022a5af527978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Sun, 29 Jan 2023 16:35:15 +0100 Subject: [PATCH 20/20] 2.x: add UPGRADE.md file to upgrade to v2 --- README.md | 4 ++++ UPGRADE.md | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 UPGRADE.md diff --git a/README.md b/README.md index e69cd4b..5a31ee5 100644 --- a/README.md +++ b/README.md @@ -322,6 +322,10 @@ The `Webhook::read` function will return a `Notification` instance. Please, [exp - Ask a question on the [Discussions forum](https://github.com/netflie/whatsapp-cloud-api/discussions "Discussions forum") - To report bugs, please [open an issue](https://github.com/netflie/whatsapp-cloud-api/issues/new/choose "open an issue") +## Migration to v2 + +Please see [UPGRADE](https://github.com/netflie/whatsapp-cloud-api/blob/main/UPGRADE.md "UPGRADE") for more information on how to upgrade to v2. + ## Changelog Please see [CHANGELOG](https://github.com/netflie/whatsapp-cloud-api/blob/main/CHANGELOG.md "CHANGELOG") for more information what has changed recently. diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 0000000..6200c99 --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,18 @@ +# Upgrade to v2 + +All instructions to upgrade this project from one major version to the next will be documented in this file. Upgrades must be run sequentially, meaning you should not skip major releases while upgrading (fix releases can be skipped). + +## 1.x to 2.x + +# Final classes +A lot of classes have been marked as final classes. If you have created new classes that extend any of them you will have to create new implementations. The reason for this change is to hide implementation details and avoid breaking versions in subsequent releases. Check the affected classes: https://github.com/netflie/whatsapp-cloud-api/commit/4cf094b1ff9a477eda34151a0e68fc7417950bbb + +# Response errors +In previous versions when a request to WhatsApp servers failed a `GuzzleHttp\Exception\ClientException` exception was thrown. From now on a `Netflie\WhatsAppCloudApi\Response\ResponseException` exception will be thrown. + +# Client +`Client::sendRequest(Request $request)` has been refactored to `Client::sendMessage(Request\RequestWithBody $request)` + +# Request +Request class has been refactored: https://github.com/netflie/whatsapp-cloud-api/commit/17f76e90122d245aace6640a1f8766fb77c29ef6#diff-74d71c4d1f9d84b9b0d946ca96eb875274f95d60611611d84cc01cdf6ed04021L5 +