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 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/README.md b/README.md index aaad824..5a31ee5 100644 --- a/README.md +++ b/README.md @@ -222,6 +222,84 @@ $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 ResponseException 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 +} +``` + +## 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 @@ -234,11 +312,20 @@ $whatsapp_cloud_api->sendList( - 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") - 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 + diff --git a/composer.json b/composer.json index 71f3c87..1860d7e 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,9 @@ "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", + "phpstan/phpstan": "^1.9" }, "autoload": { "psr-4": { diff --git a/src/Client.php b/src/Client.php index 29272f7..64fd066 100644 --- a/src/Client.php +++ b/src/Client.php @@ -36,17 +36,17 @@ 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. * * @throws Netflie\WhatsAppCloudApi\Response\ResponseException */ - public function sendRequest(Request $request): Response + public function sendMessage(Request\RequestWithBody $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() ); @@ -65,6 +65,69 @@ public function sendRequest(Request $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; + } + + /** + * 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(); @@ -75,8 +138,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..36b5741 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 JSON 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,32 @@ 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; + + /** + * 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; + + /** + * 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 106d8bd..58bcc76 100644 --- a/src/Http/GuzzleClientHandler.php +++ b/src/Http/GuzzleClientHandler.php @@ -3,13 +3,14 @@ namespace Netflie\WhatsAppCloudApi\Http; 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. @@ -23,18 +24,55 @@ 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, + $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); + } + + /** + * {@inheritDoc} + * + */ + 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); + } + + 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, + 'http_errors' => false, ]); + } + 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/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 b968d4f..cbe15d2 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,41 +9,17 @@ 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; + private string $access_token; /** * The timeout request. * * @return int */ - protected int $timeout; + private int $timeout; /** * Creates a new Request entity. @@ -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/MediaRequest/DownloadMediaRequest.php b/src/Request/MediaRequest/DownloadMediaRequest.php new file mode 100644 index 0000000..ad488d2 --- /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/Request/MediaRequest/UploadMediaRequest.php b/src/Request/MediaRequest/UploadMediaRequest.php new file mode 100644 index 0000000..1381657 --- /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/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 new file mode 100644 index 0000000..7f0eb05 --- /dev/null +++ b/src/Request/MessageRequest.php @@ -0,0 +1,47 @@ +message = $message; + $this->from_phone_number_id = $from_phone_number_id; + + parent::__construct($access_token, $timeout); + } + + /** + * 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..8a270cf 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..05837c2 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..21eeda1 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..e71396d 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..2cfd882 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 70% rename from src/Request/RequestOptionsListMessage.php rename to src/Request/MessageRequest/RequestOptionsListMessage.php index 1bcd3e7..6e1b4ec 100644 --- a/src/Request/RequestOptionsListMessage.php +++ b/src/Request/MessageRequest/RequestOptionsListMessage.php @@ -1,21 +1,17 @@ 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..f2106fa 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..5192302 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..e28a2ca 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..cfe014a 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/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 @@ +request = $request; $this->body = $body; @@ -49,12 +50,22 @@ public function __construct(Request $request, $body, $http_status_code = null, a $this->decodeBody(); } + public static function fromClientResponse(Request $request, RawResponse $response): self + { + return new self( + $request, + $response->body(), + $response->httpResponseCode(), + $response->headers() + ); + } + /** * Return the original request that returned this response. * * @return Resquest */ - public function request() + public function request(): Request { return $this->request; } @@ -64,7 +75,7 @@ public function request() * * @return string */ - public function accessToken() + public function accessToken(): string { return $this->request->accessToken(); } @@ -74,7 +85,7 @@ public function accessToken() * * @return int */ - public function httpStatusCode() + public function httpStatusCode(): int { return $this->http_status_code; } @@ -84,7 +95,7 @@ public function httpStatusCode() * * @return array */ - public function headers() + public function headers(): array { return $this->headers; } @@ -94,7 +105,7 @@ public function headers() * * @return string */ - public function body() + public function body(): string { return $this->body; } @@ -104,7 +115,7 @@ public function body() * * @return array */ - public function decodedBody() + public function decodedBody(): array { return $this->decoded_body; } @@ -114,7 +125,7 @@ public function decodedBody() * * @return string|null */ - public function graphVersion() + public function graphVersion(): ?string { return $this->headers['facebook-api-version'] ?? null; } @@ -124,7 +135,7 @@ public function graphVersion() * * @return bool */ - public function isError() + public function isError(): bool { return isset($this->decoded_body['error']); } @@ -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() + */ + 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/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. diff --git a/src/WebHook.php b/src/WebHook.php new file mode 100644 index 0000000..77952f7 --- /dev/null +++ b/src/WebHook.php @@ -0,0 +1,34 @@ +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 new file mode 100644 index 0000000..1d8b976 --- /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/src/WhatsAppCloudApi.php b/src/WhatsAppCloudApi.php index f952f4e..49092c0 100644 --- a/src/WhatsAppCloudApi.php +++ b/src/WhatsAppCloudApi.php @@ -2,32 +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; -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; class WhatsAppCloudApi { @@ -83,15 +62,15 @@ 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( + $message = new Message\TextMessage($to, $text, $preview_url); + $request = new Request\MessageRequest\RequestTextMessage( $message, $this->app->accessToken(), $this->app->fromPhoneNumberId(), $this->timeout ); - return $this->client->sendRequest($request); + return $this->client->sendMessage($request); } /** @@ -99,22 +78,22 @@ 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); - $request = new RequestDocumentMessage( + $message = new Message\DocumentMessage($to, $document_id, $name, $caption); + $request = new Request\MessageRequest\RequestDocumentMessage( $message, $this->app->accessToken(), $this->app->fromPhoneNumberId(), $this->timeout ); - return $this->client->sendRequest($request); + return $this->client->sendMessage($request); } /** @@ -132,108 +111,108 @@ 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( + $message = new Message\TemplateMessage($to, $template_name, $language, $components); + $request = new Request\MessageRequest\RequestTemplateMessage( $message, $this->app->accessToken(), $this->app->fromPhoneNumberId(), $this->timeout ); - return $this->client->sendRequest($request); + return $this->client->sendMessage($request); } /** - * 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); - $request = new RequestAudioMessage( + $message = new Message\AudioMessage($to, $audio_id); + $request = new Request\MessageRequest\RequestAudioMessage( $message, $this->app->accessToken(), $this->app->fromPhoneNumberId(), $this->timeout ); - return $this->client->sendRequest($request); + return $this->client->sendMessage($request); } /** - * 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); - $request = new RequestImageMessage( + $message = new Message\ImageMessage($to, $image_id, $caption); + $request = new Request\MessageRequest\RequestImageMessage( $message, $this->app->accessToken(), $this->app->fromPhoneNumberId(), $this->timeout ); - return $this->client->sendRequest($request); + return $this->client->sendMessage($request); } /** - * 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); - $request = new RequestVideoMessage( + $message = new Message\VideoMessage($to, $video_id, $caption); + $request = new Request\MessageRequest\RequestVideoMessage( $message, $this->app->accessToken(), $this->app->fromPhoneNumberId(), $this->timeout ); - return $this->client->sendRequest($request); + return $this->client->sendMessage($request); } /** * 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); - $request = new RequestStickerMessage( + $message = new Message\StickerMessage($to, $sticker_id); + $request = new Request\MessageRequest\RequestStickerMessage( $message, $this->app->accessToken(), $this->app->fromPhoneNumberId(), $this->timeout ); - return $this->client->sendRequest($request); + return $this->client->sendMessage($request); } /** @@ -251,15 +230,15 @@ 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( + $message = new Message\LocationMessage($to, $longitude, $latitude, $name, $address); + $request = new Request\MessageRequest\RequestLocationMessage( $message, $this->app->accessToken(), $this->app->fromPhoneNumberId(), $this->timeout ); - return $this->client->sendRequest($request); + return $this->client->sendMessage($request); } /** @@ -275,28 +254,90 @@ 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( + $message = new Message\ContactMessage($to, $name, ...$phone); + $request = new Request\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( + $message = new Message\OptionsListMessage($to, $header, $body, $footer, $action); + $request = new Request\MessageRequest\RequestOptionsListMessage( $message, $this->app->accessToken(), $this->app->fromPhoneNumberId(), $this->timeout ); - return $this->client->sendRequest($request); + 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); + } + + /** + * 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); + } + + /** + * 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); } /** diff --git a/tests/Integration/ClientTest.php b/tests/Integration/ClientTest.php index 0e69188..79d656f 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; use Netflie\WhatsAppCloudApi\Tests\WhatsAppCloudApiTestConfiguration; use Netflie\WhatsAppCloudApi\WhatsAppCloudApi; use PHPUnit\Framework\TestCase; @@ -28,13 +28,48 @@ 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 ); - $response = $this->client->sendRequest($request); + $response = $this->client->sendMessage($request); + + $this->assertEquals(200, $response->httpStatusCode()); + $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()); + + 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()); diff --git a/tests/Integration/WhatsAppCloudApiTest.php b/tests/Integration/WhatsAppCloudApiTest.php index 38cbf0b..4676841 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, @@ -252,4 +256,25 @@ 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()); + + 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/Support/netflie.png b/tests/Support/netflie.png new file mode 100644 index 0000000..96c45ed Binary files /dev/null and b/tests/Support/netflie.png differ 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); + } +} 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()); + } +} 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()); + } +} diff --git a/tests/Unit/WhatsAppCloudApiTest.php b/tests/Unit/WhatsAppCloudApiTest.php index 5b1cbf2..4e7eb3e 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; @@ -67,17 +68,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( @@ -90,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; @@ -104,16 +102,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,15 +118,15 @@ 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()); } 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; @@ -146,16 +142,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,15 +160,15 @@ 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()); } 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; @@ -190,16 +184,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,15 +202,15 @@ 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()); } 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; @@ -233,16 +225,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,15 +241,15 @@ 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()); } 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; @@ -344,16 +334,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,15 +352,15 @@ 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()); } public function test_send_audio_id() { $to = $this->faker->phoneNumber; - $url = $this->buildRequestUri(); + $url = $this->buildMessageRequestUri(); $document_id = $this->faker->uuid; $body = [ @@ -384,16 +372,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,15 +388,15 @@ 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()); } public function test_send_audio_link() { $to = $this->faker->phoneNumber; - $url = $this->buildRequestUri(); + $url = $this->buildMessageRequestUri(); $document_link = $this->faker->url; $body = [ @@ -422,16 +408,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,15 +424,15 @@ 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()); } 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; @@ -462,16 +446,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,15 +463,15 @@ 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()); } 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; @@ -503,16 +485,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,15 +502,15 @@ 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()); } 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; @@ -544,16 +524,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,15 +541,15 @@ 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()); } 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; @@ -585,16 +563,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,15 +580,15 @@ 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()); } public function test_send_sticker() { $to = $this->faker->phoneNumber; - $url = $this->buildRequestUri(); + $url = $this->buildMessageRequestUri(); $sticker_link = $this->faker->url; $body = [ @@ -624,16 +600,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,15 +616,15 @@ 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()); } 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; @@ -668,16 +642,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,15 +660,15 @@ 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()); } 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; @@ -723,16 +695,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,15 +712,15 @@ 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()); } 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; @@ -778,16 +748,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,15 +765,15 @@ 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()); } 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)]; @@ -831,16 +799,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,13 +829,126 @@ 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()); } - private function buildRequestUri(): string + public function test_upload_media() { - return Client::BASE_GRAPH_URL . '/' . static::TEST_GRAPH_VERSION . '/' . $this->from_phone_number_id . '/messages'; + $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()); + } + + 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()); + } + + 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 . '/'; + } + + private function buildMessageRequestUri(): string + { + return $this->buildBaseUri() . $this->from_phone_number_id . '/messages'; + } + + private function buildMediaRequestUri(): string + { + return $this->buildBaseUri() . $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"}]}'; + } + + private function failedMessageResponse(): string + { + return '{"error":{"message":"Invalid OAuth access token - Cannot parse access token","type":"OAuthException","code":190,"fbtrace_id":"AbJuG-rMVv36mjw-r78mKwg"}}'; } }