From 22b02306345d34f79c521c1ddd92d452704192a8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Oct 2023 17:19:03 +0000 Subject: [PATCH 01/18] Bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/dependency-review.yml | 2 +- .github/workflows/php.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index b0dedc4..4e75197 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -15,6 +15,6 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Checkout Repository' - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: 'Dependency Review' uses: actions/dependency-review-action@v3 diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 2435438..a62d9d9 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 From 6a32d7cea2e76a5e03df34c4be25613de5907faf Mon Sep 17 00:00:00 2001 From: Arne <1255879+arneee@users.noreply.github.com> Date: Fri, 8 Sep 2023 22:20:44 +0200 Subject: [PATCH 02/18] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index e4caa5f..c8f4b0f 100644 --- a/README.md +++ b/README.md @@ -294,7 +294,11 @@ fwrite(STDOUT, print_r($payload, true) . "\n"); // Instantiate the Webhook super class. $webhook = new WebHook(); +// Read the first message fwrite(STDOUT, print_r($webhook->read(json_decode($payload, true)), true) . "\n"); + +//Read all messages in case Meta decided to batch them +fwrite(STDOUT, print_r($webhook->readAll(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. From 8ec015e02723638292328ce463edf0c48ee3b24b Mon Sep 17 00:00:00 2001 From: arneee <1255879+arneee@users.noreply.github.com> Date: Tue, 22 Aug 2023 21:18:30 +0200 Subject: [PATCH 03/18] Allow multiple entries and changes within one webhook call --- src/WebHook.php | 12 ++++ src/WebHook/NotificationFactory.php | 45 ++++++++---- .../Unit/WebHook/NotificationFactoryTest.php | 69 +++++++++++++++++++ 3 files changed, 114 insertions(+), 12 deletions(-) diff --git a/src/WebHook.php b/src/WebHook.php index 77952f7..86ebfd5 100644 --- a/src/WebHook.php +++ b/src/WebHook.php @@ -31,4 +31,16 @@ public function read(array $payload): ?Notification return (new NotificationFactory()) ->buildFromPayload($payload); } + + /** + * Get all notifications from incoming webhook messages. + * + * @param array $payload Payload received in your endpoint URL. + * @return Notification[] A PHP representation of WhatsApp webhook notifications + */ + public function readAll(array $payload): array + { + return (new NotificationFactory()) + ->buildAllFromPayload($payload); + } } diff --git a/src/WebHook/NotificationFactory.php b/src/WebHook/NotificationFactory.php index f5d7176..359281e 100644 --- a/src/WebHook/NotificationFactory.php +++ b/src/WebHook/NotificationFactory.php @@ -14,25 +14,46 @@ public function __construct() } public function buildFromPayload(array $payload): ?Notification + { + $notifications = $this->buildAllFromPayload($payload); + + return $notifications[0] ?? null; + } + + /** + * @return Notification[] + */ + public function buildAllFromPayload(array $payload): array { if (!is_array($payload['entry'] ?? null)) { - return null; + return []; } - $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'] ?? []; + $notifications = []; - if ($message) { - return $this->message_notification_factory->buildFromPayload($metadata, $message, $contact); - } + foreach($payload['entry'] as $entry) { + + if(!is_array($entry['changes'])) { + continue; + } + + foreach($entry['changes'] as $change) { + + $message = $change['value']['messages'][0] ?? []; + $status = $change['value']['statuses'][0] ?? []; + $contact = $change['value']['contacts'][0] ?? []; + $metadata = $change['value']['metadata'] ?? []; + + if ($message) { + $notifications[] = $this->message_notification_factory->buildFromPayload($metadata, $message, $contact); + } - if ($status) { - return $this->status_notification_factory->buildFromPayload($metadata, $status); + if ($status) { + $notifications[] = $this->status_notification_factory->buildFromPayload($metadata, $status); + } + } } - return null; + return $notifications; } } diff --git a/tests/Unit/WebHook/NotificationFactoryTest.php b/tests/Unit/WebHook/NotificationFactoryTest.php index 9471536..3164487 100644 --- a/tests/Unit/WebHook/NotificationFactoryTest.php +++ b/tests/Unit/WebHook/NotificationFactoryTest.php @@ -135,6 +135,75 @@ public function test_build_from_payload_can_build_a_text_notification() $this->assertEquals('MESSAGE_BODY', $notification->message()); } + public function test_build_from_payload_can_build_multiple_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" + }, + { + "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": "1669233779", + "text": { + "body": "MESSAGE_BODY2" + }, + "type": "text" + }] + }, + "field": "messages" + }] + }] + }', true); + + $notifications = $this->notification_factory->buildAllFromPayload($payload); + + $this->assertCount(2, $notifications); + + $this->assertInstanceOf(Notification\Text::class, $notifications[0]); + $this->assertInstanceOf(Notification\Text::class, $notifications[1]); + $this->assertEquals('MESSAGE_BODY', $notifications[0]->message()); + $this->assertEquals('MESSAGE_BODY2', $notifications[1]->message()); + } + public function test_build_from_payload_can_build_a_reaction_notification() { $payload = json_decode('{ From 6e205bea959e8f8d75f54d5ef2a5c08c23d53241 Mon Sep 17 00:00:00 2001 From: arneee <1255879+arneee@users.noreply.github.com> Date: Tue, 22 Aug 2023 21:22:07 +0200 Subject: [PATCH 04/18] Code style --- src/WebHook/NotificationFactory.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/WebHook/NotificationFactory.php b/src/WebHook/NotificationFactory.php index 359281e..6a1886b 100644 --- a/src/WebHook/NotificationFactory.php +++ b/src/WebHook/NotificationFactory.php @@ -31,14 +31,12 @@ public function buildAllFromPayload(array $payload): array $notifications = []; - foreach($payload['entry'] as $entry) { - - if(!is_array($entry['changes'])) { + foreach ($payload['entry'] as $entry) { + if (!is_array($entry['changes'])) { continue; } - foreach($entry['changes'] as $change) { - + foreach ($entry['changes'] as $change) { $message = $change['value']['messages'][0] ?? []; $status = $change['value']['statuses'][0] ?? []; $contact = $change['value']['contacts'][0] ?? []; From 0eb45d5d45cb5925695f7088378c776164dd641e Mon Sep 17 00:00:00 2001 From: horas_ro Date: Tue, 22 Aug 2023 14:36:11 +0300 Subject: [PATCH 05/18] Fix optional header for interactive list messages --- .../MessageRequest/RequestOptionsListMessage.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Request/MessageRequest/RequestOptionsListMessage.php b/src/Request/MessageRequest/RequestOptionsListMessage.php index 32375e7..d758da2 100644 --- a/src/Request/MessageRequest/RequestOptionsListMessage.php +++ b/src/Request/MessageRequest/RequestOptionsListMessage.php @@ -18,15 +18,18 @@ public function body(): array 'type' => 'interactive', 'interactive' => [ 'type' => $this->message->type(), - 'header' => [ - 'type' => 'text', - 'text' => $this->message->header(), - ], 'body' => ['text' => $this->message->body()], 'action' => $this->message->action(), ], ]; + if ($this->message->header()) { + $body['interactive']['header'] = [ + 'type' => 'text', + 'text' => $this->message->header(), + ]; + } + if ($this->message->footer()) { $body['interactive']['footer'] = ['text' => $this->message->footer()]; } From 9ebe85343dc61300613e528ba2f40083b76346f7 Mon Sep 17 00:00:00 2001 From: Derrick Obedgiu Date: Sun, 13 Aug 2023 02:28:12 +0300 Subject: [PATCH 06/18] Add button reply message section --- README.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/README.md b/README.md index c8f4b0f..1c40126 100644 --- a/README.md +++ b/README.md @@ -222,6 +222,36 @@ $whatsapp_cloud_api->sendList( ); ``` +### Send a button reply message + +```php + 'your-configured-from-phone-number-id', + 'access_token' => 'your-facebook-whatsapp-application-token' +]); + +$rows = [ + new Button('button-1', 'Yes'), + new Button('button-2', 'No'), + new Button('button-3', 'Not Now'), +]; +$action = new ButtonAction($rows); + +$whatsapp_cloud_api->sendButton( + '', + 'Would you like to rate us on Trustpilot?', + $action, + 'RATE US', // Optional: Specify a header (type "text") + 'Please choose an option' // Optional: Specify a footer +); +``` + ## 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. @@ -345,6 +375,7 @@ Fields list: https://developers.facebook.com/docs/whatsapp/cloud-api/reference/b - Send Locations - Send Contacts - Send Lists +- Send Buttons - Upload media resources to WhatsApp servers - Download media resources from WhatsApp servers - Mark messages as read From 35ec5f6d8666f5d982b7201e41e928b168f10bc5 Mon Sep 17 00:00:00 2001 From: Derrick Obedgiu Date: Sun, 13 Aug 2023 02:29:40 +0300 Subject: [PATCH 07/18] add sendButton method --- src/WhatsAppCloudApi.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/WhatsAppCloudApi.php b/src/WhatsAppCloudApi.php index ad15cb5..1fadd12 100644 --- a/src/WhatsAppCloudApi.php +++ b/src/WhatsAppCloudApi.php @@ -6,6 +6,8 @@ use Netflie\WhatsAppCloudApi\Message\Contact\Phone; use Netflie\WhatsAppCloudApi\Message\Media\MediaID; use Netflie\WhatsAppCloudApi\Message\OptionsList\Action; +use Netflie\WhatsAppCloudApi\Message\ButtonReply\Button; +use Netflie\WhatsAppCloudApi\Message\ButtonReply\ButtonAction; use Netflie\WhatsAppCloudApi\Message\Template\Component; class WhatsAppCloudApi @@ -288,6 +290,22 @@ public function sendList(string $to, string $header, string $body, string $foote return $this->client->sendMessage($request); } + public function sendButton(string $to, string $body, ButtonAction $action, ?string $header = null, ?string $footer = null): Response + { + $message = new Message\ButtonReplyMessage( + $to, $body, $action, $header, $footer + ); + + $request = new Request\MessageRequest\RequestButtonReplyMessage( + $message, + $this->app->accessToken(), + $this->app->fromPhoneNumberId(), + $this->timeout + ); + + return $this->client->sendMessage($request); + } + /** * Upload a media file (image, audio, video...) to Facebook servers. * From 758f2da9fa8c70d3bcbc659aa4df0c0316b57101 Mon Sep 17 00:00:00 2001 From: Derrick Obedgiu Date: Sun, 13 Aug 2023 02:31:31 +0300 Subject: [PATCH 08/18] Add sendButton --- src/Message/ButtonReply/Button.php | 23 ++++++++++ src/Message/ButtonReply/ButtonAction.php | 31 +++++++++++++ src/Message/ButtonReplyMessage.php | 44 +++++++++++++++++++ .../RequestButtonReplyMessage.php | 42 ++++++++++++++++++ 4 files changed, 140 insertions(+) create mode 100644 src/Message/ButtonReply/Button.php create mode 100644 src/Message/ButtonReply/ButtonAction.php create mode 100644 src/Message/ButtonReplyMessage.php create mode 100644 src/Request/MessageRequest/RequestButtonReplyMessage.php diff --git a/src/Message/ButtonReply/Button.php b/src/Message/ButtonReply/Button.php new file mode 100644 index 0000000..779ddf0 --- /dev/null +++ b/src/Message/ButtonReply/Button.php @@ -0,0 +1,23 @@ +id = $id; + $this->title = $title; + } + + public function id(): string { + return $this->id; + } + + public function title(): string { + return $this->title; + } + +} \ No newline at end of file diff --git a/src/Message/ButtonReply/ButtonAction.php b/src/Message/ButtonReply/ButtonAction.php new file mode 100644 index 0000000..025d9f1 --- /dev/null +++ b/src/Message/ButtonReply/ButtonAction.php @@ -0,0 +1,31 @@ +buttons = $buttons; + } + + public function buttons(): array { + $buttonActions = []; + + foreach ($this->buttons as $button) { + $buttonActions[] = [ + "type" => "reply", + "reply" => [ + "id" => $button->id(), + "title" => $button->title() + ] + ]; + } + + return $buttonActions; + } + +} \ No newline at end of file diff --git a/src/Message/ButtonReplyMessage.php b/src/Message/ButtonReplyMessage.php new file mode 100644 index 0000000..bd55ead --- /dev/null +++ b/src/Message/ButtonReplyMessage.php @@ -0,0 +1,44 @@ +body = $body; + $this->action = $action; + $this->header = $header; + $this->footer = $footer; + + parent::__construct($to); + } + + public function header(): ?string { + return $this->header; + } + + public function body(): string { + return $this->body; + } + + public function action(): ButtonAction { + return $this->action; + } + + public function footer(): ?string { + return $this->footer; + } + +} \ No newline at end of file diff --git a/src/Request/MessageRequest/RequestButtonReplyMessage.php b/src/Request/MessageRequest/RequestButtonReplyMessage.php new file mode 100644 index 0000000..9e63261 --- /dev/null +++ b/src/Request/MessageRequest/RequestButtonReplyMessage.php @@ -0,0 +1,42 @@ + $this->message->messagingProduct(), + 'recipient_type' => $this->message->recipientType(), + 'to' => $this->message->to(), + 'type' => 'interactive', + 'interactive' => [ + 'type' => 'button', + 'body' => ['text' => $this->message->body()], + 'action' => ['buttons' => $this->message->action()->buttons()] + ] + ]; + + if($this->message->header()) { + $body['interactive']['header'] = [ + 'type' => 'text', + 'text' => $this->message->header() + ]; + } + + if($this->message->footer()) { + $body['interactive']['footer'] = [ + 'type' => 'text', + 'text' => $this->message->footer() + ]; + } + + return $body; + + } + +} \ No newline at end of file From e30562d4f6701113464f5f465168f21afd7ab9b5 Mon Sep 17 00:00:00 2001 From: Ian Rothmann Date: Mon, 21 Aug 2023 17:11:26 +0200 Subject: [PATCH 09/18] Footer fix, linting and tests Code linting, removing the "type" key from the footer, unit and integration tests that passes. --- src/Message/ButtonReply/Button.php | 34 ++++---- src/Message/ButtonReplyMessage.php | 80 ++++++++++--------- .../RequestButtonReplyMessage.php | 52 ++++++------ tests/Integration/WhatsAppCloudApiTest.php | 25 ++++++ tests/Unit/WhatsAppCloudApiTest.php | 67 ++++++++++++++++ 5 files changed, 177 insertions(+), 81 deletions(-) diff --git a/src/Message/ButtonReply/Button.php b/src/Message/ButtonReply/Button.php index 779ddf0..f701e64 100644 --- a/src/Message/ButtonReply/Button.php +++ b/src/Message/ButtonReply/Button.php @@ -2,22 +2,24 @@ namespace Netflie\WhatsAppCloudApi\Message\ButtonReply; -class Button { +class Button +{ + private $id; + private $title; - private $id; - private $title; + public function __construct(string $id, string $title) + { + $this->id = $id; + $this->title = $title; + } - public function __construct(string $id, string $title) { - $this->id = $id; - $this->title = $title; - } + public function id(): string + { + return $this->id; + } - public function id(): string { - return $this->id; - } - - public function title(): string { - return $this->title; - } - -} \ No newline at end of file + public function title(): string + { + return $this->title; + } +} diff --git a/src/Message/ButtonReplyMessage.php b/src/Message/ButtonReplyMessage.php index bd55ead..8e58df4 100644 --- a/src/Message/ButtonReplyMessage.php +++ b/src/Message/ButtonReplyMessage.php @@ -4,41 +4,45 @@ use Netflie\WhatsAppCloudApi\Message\ButtonReply\ButtonAction; -class ButtonReplyMessage extends Message { - - protected string $type = 'button'; - - private ?string $header; - - private string $body; - - private ?string $footer; - - private ButtonAction $action; - - public function __construct(string $to, string $body, ButtonAction $action, ?string $header = null, ?string $footer = null) { - $this->body = $body; - $this->action = $action; - $this->header = $header; - $this->footer = $footer; - - parent::__construct($to); - } - - public function header(): ?string { - return $this->header; - } - - public function body(): string { - return $this->body; - } - - public function action(): ButtonAction { - return $this->action; - } - - public function footer(): ?string { - return $this->footer; - } - -} \ No newline at end of file +class ButtonReplyMessage extends Message +{ + protected string $type = 'button'; + + private ?string $header; + + private string $body; + + private ?string $footer; + + private ButtonAction $action; + + public function __construct(string $to, string $body, ButtonAction $action, ?string $header = null, ?string $footer = null) + { + $this->body = $body; + $this->action = $action; + $this->header = $header; + $this->footer = $footer; + + parent::__construct($to); + } + + public function header(): ?string + { + return $this->header; + } + + public function body(): string + { + return $this->body; + } + + public function action(): ButtonAction + { + return $this->action; + } + + public function footer(): ?string + { + return $this->footer; + } +} diff --git a/src/Request/MessageRequest/RequestButtonReplyMessage.php b/src/Request/MessageRequest/RequestButtonReplyMessage.php index 9e63261..cd9fb2a 100644 --- a/src/Request/MessageRequest/RequestButtonReplyMessage.php +++ b/src/Request/MessageRequest/RequestButtonReplyMessage.php @@ -2,41 +2,39 @@ namespace Netflie\WhatsAppCloudApi\Request\MessageRequest; -use Netflie\WhatsAppCloudApi\Message\ButtonReplyMessage; use Netflie\WhatsAppCloudApi\Request\MessageRequest; -class RequestButtonReplyMessage extends MessageRequest { - - public function body(): array { +class RequestButtonReplyMessage extends MessageRequest +{ + public function body(): array + { $body = [ - 'messaging_product' => $this->message->messagingProduct(), - 'recipient_type' => $this->message->recipientType(), - 'to' => $this->message->to(), - 'type' => 'interactive', - 'interactive' => [ - 'type' => 'button', - 'body' => ['text' => $this->message->body()], - 'action' => ['buttons' => $this->message->action()->buttons()] - ] + 'messaging_product' => $this->message->messagingProduct(), + 'recipient_type' => $this->message->recipientType(), + 'to' => $this->message->to(), + 'type' => 'interactive', + 'interactive' => [ + 'type' => 'button', + 'body' => ['text' => $this->message->body()], + 'action' => ['buttons' => $this->message->action()->buttons()], + ], ]; - if($this->message->header()) { - $body['interactive']['header'] = [ - 'type' => 'text', - 'text' => $this->message->header() - ]; + if ($this->message->header()) { + $body['interactive']['header'] = [ + 'type' => 'text', + 'text' => $this->message->header(), + ]; } - if($this->message->footer()) { - $body['interactive']['footer'] = [ - 'type' => 'text', - 'text' => $this->message->footer() - ]; + if ($this->message->footer()) { + $body['interactive']['footer'] = [ + 'text' => $this->message->footer(), + ]; } - + return $body; - - } -} \ No newline at end of file + } +} diff --git a/tests/Integration/WhatsAppCloudApiTest.php b/tests/Integration/WhatsAppCloudApiTest.php index a6a1044..51ff656 100644 --- a/tests/Integration/WhatsAppCloudApiTest.php +++ b/tests/Integration/WhatsAppCloudApiTest.php @@ -2,6 +2,8 @@ namespace Netflie\WhatsAppCloudApi\Tests\Integration; +use Netflie\WhatsAppCloudApi\Message\ButtonReply\Button; +use Netflie\WhatsAppCloudApi\Message\ButtonReply\ButtonAction; use Netflie\WhatsAppCloudApi\Message\Contact\ContactName; use Netflie\WhatsAppCloudApi\Message\Contact\Phone; use Netflie\WhatsAppCloudApi\Message\Contact\PhoneType; @@ -258,6 +260,29 @@ public function test_send_list() $this->assertEquals(false, $response->isError()); } + public function test_send_reply_buttons() + { + $buttonRows = [ + new Button('button-1', 'Yes'), + new Button('button-2', 'No'), + new Button('button-3', 'Not Now'), + ]; + $buttonAction = new ButtonAction($buttonRows); + $header = 'RATE US'; + $footer = 'Please choose an option'; + + $response = $this->whatsapp_app_cloud_api->sendButton( + WhatsAppCloudApiTestConfiguration::$to_phone_number_id, + 'Would you like to rate us?', + $buttonAction, + $header, + $footer + ); + + $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'); diff --git a/tests/Unit/WhatsAppCloudApiTest.php b/tests/Unit/WhatsAppCloudApiTest.php index c859184..2601e89 100644 --- a/tests/Unit/WhatsAppCloudApiTest.php +++ b/tests/Unit/WhatsAppCloudApiTest.php @@ -6,6 +6,8 @@ use Netflie\WhatsAppCloudApi\Client; use Netflie\WhatsAppCloudApi\Http\ClientHandler; use Netflie\WhatsAppCloudApi\Http\RawResponse; +use Netflie\WhatsAppCloudApi\Message\ButtonReply\Button; +use Netflie\WhatsAppCloudApi\Message\ButtonReply\ButtonAction; use Netflie\WhatsAppCloudApi\Message\Contact\ContactName; use Netflie\WhatsAppCloudApi\Message\Contact\Phone; use Netflie\WhatsAppCloudApi\Message\Contact\PhoneType; @@ -897,6 +899,71 @@ public function test_send_list() $this->assertEquals(false, $response->isError()); } + public function test_send_reply_buttons() + { + $to = $this->faker->phoneNumber; + $url = $this->buildMessageRequestUri(); + + $buttonRows = [ + ['id' => $this->faker->uuid, 'title' => $this->faker->text(10)], + ['id' => $this->faker->uuid, 'title' => $this->faker->text(10)], + ['id' => $this->faker->uuid, 'title' => $this->faker->text(10)], + ]; + $buttonAction = ['buttons' => []]; + + foreach($buttonRows as $button) { + $buttonAction['buttons'][] = [ + 'type' => 'reply', + 'reply' => $button, + ]; + } + + $message = $this->faker->text(50); + $header = $this->faker->text(50); + $footer = $this->faker->text(50); + + $body = [ + 'messaging_product' => 'whatsapp', + 'recipient_type' => 'individual', + 'to' => $to, + 'type' => 'interactive', + 'interactive' => [ + 'type' => 'button', + 'body' => ['text' => $message], + 'action' => $buttonAction, + 'header' => ['type' => 'text', 'text' => $header], + 'footer' => ['text' => $footer], + ], + ]; + $headers = [ + 'Authorization' => 'Bearer ' . $this->access_token, + ]; + + $this->client_handler + ->postJsonData($url, $body, $headers, Argument::type('int')) + ->shouldBeCalled() + ->willReturn(new RawResponse($headers, $this->successfulMessageNodeResponse(), 200)); + + $actionButtons = []; + + foreach ($buttonRows as $button) { + $actionButtons[] = new Button($button['id'], $button['title']); + } + + $response = $this->whatsapp_app_cloud_api->sendButton( + $to, + $message, + new ButtonAction($actionButtons), + $header, + $footer + ); + + $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()); + } + public function test_upload_media() { $url = $this->buildMediaRequestUri(); From d4964da9229924affa978230a16ef283984644db Mon Sep 17 00:00:00 2001 From: Ian Rothmann Date: Mon, 11 Sep 2023 19:26:57 +0200 Subject: [PATCH 10/18] StyleCI issues for Reply Buttons --- src/Message/ButtonReply/ButtonAction.php | 43 +++++++++---------- .../RequestButtonReplyMessage.php | 2 - src/WhatsAppCloudApi.php | 27 ++++++------ tests/Unit/WhatsAppCloudApiTest.php | 2 +- 4 files changed, 37 insertions(+), 37 deletions(-) diff --git a/src/Message/ButtonReply/ButtonAction.php b/src/Message/ButtonReply/ButtonAction.php index 025d9f1..09c5d9c 100644 --- a/src/Message/ButtonReply/ButtonAction.php +++ b/src/Message/ButtonReply/ButtonAction.php @@ -2,30 +2,29 @@ namespace Netflie\WhatsAppCloudApi\Message\ButtonReply; -use Netflie\WhatsAppCloudApi\Message\ButtonReply\Button; +class ButtonAction +{ + private $buttons; -class ButtonAction { + public function __construct(array $buttons) + { + $this->buttons = $buttons; + } - private $buttons; + public function buttons(): array + { + $buttonActions = []; - public function __construct(array $buttons) { - $this->buttons = $buttons; - } - - public function buttons(): array { - $buttonActions = []; + foreach ($this->buttons as $button) { + $buttonActions[] = [ + "type" => "reply", + "reply" => [ + "id" => $button->id(), + "title" => $button->title(), + ], + ]; + } - foreach ($this->buttons as $button) { - $buttonActions[] = [ - "type" => "reply", - "reply" => [ - "id" => $button->id(), - "title" => $button->title() - ] - ]; + return $buttonActions; } - - return $buttonActions; - } - -} \ No newline at end of file +} diff --git a/src/Request/MessageRequest/RequestButtonReplyMessage.php b/src/Request/MessageRequest/RequestButtonReplyMessage.php index cd9fb2a..4c3d287 100644 --- a/src/Request/MessageRequest/RequestButtonReplyMessage.php +++ b/src/Request/MessageRequest/RequestButtonReplyMessage.php @@ -8,7 +8,6 @@ class RequestButtonReplyMessage extends MessageRequest { public function body(): array { - $body = [ 'messaging_product' => $this->message->messagingProduct(), 'recipient_type' => $this->message->recipientType(), @@ -35,6 +34,5 @@ public function body(): array } return $body; - } } diff --git a/src/WhatsAppCloudApi.php b/src/WhatsAppCloudApi.php index 1fadd12..0db05a3 100644 --- a/src/WhatsAppCloudApi.php +++ b/src/WhatsAppCloudApi.php @@ -2,12 +2,11 @@ namespace Netflie\WhatsAppCloudApi; +use Netflie\WhatsAppCloudApi\Message\ButtonReply\ButtonAction; use Netflie\WhatsAppCloudApi\Message\Contact\ContactName; use Netflie\WhatsAppCloudApi\Message\Contact\Phone; use Netflie\WhatsAppCloudApi\Message\Media\MediaID; use Netflie\WhatsAppCloudApi\Message\OptionsList\Action; -use Netflie\WhatsAppCloudApi\Message\ButtonReply\Button; -use Netflie\WhatsAppCloudApi\Message\ButtonReply\ButtonAction; use Netflie\WhatsAppCloudApi\Message\Template\Component; class WhatsAppCloudApi @@ -290,21 +289,25 @@ public function sendList(string $to, string $header, string $body, string $foote return $this->client->sendMessage($request); } - public function sendButton(string $to, string $body, ButtonAction $action, ?string $header = null, ?string $footer = null): Response - { + public function sendButton(string $to, string $body, ButtonAction $action, ?string $header = null, ?string $footer = null): Response + { $message = new Message\ButtonReplyMessage( - $to, $body, $action, $header, $footer + $to, + $body, + $action, + $header, + $footer ); - + $request = new Request\MessageRequest\RequestButtonReplyMessage( - $message, - $this->app->accessToken(), - $this->app->fromPhoneNumberId(), - $this->timeout + $message, + $this->app->accessToken(), + $this->app->fromPhoneNumberId(), + $this->timeout ); - + return $this->client->sendMessage($request); - } + } /** * Upload a media file (image, audio, video...) to Facebook servers. diff --git a/tests/Unit/WhatsAppCloudApiTest.php b/tests/Unit/WhatsAppCloudApiTest.php index 2601e89..c5995f2 100644 --- a/tests/Unit/WhatsAppCloudApiTest.php +++ b/tests/Unit/WhatsAppCloudApiTest.php @@ -911,7 +911,7 @@ public function test_send_reply_buttons() ]; $buttonAction = ['buttons' => []]; - foreach($buttonRows as $button) { + foreach ($buttonRows as $button) { $buttonAction['buttons'][] = [ 'type' => 'reply', 'reply' => $button, From 5b65958bf2c507299a4cf4914b315b1ab5e53ebd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Sun, 19 Nov 2023 16:48:40 +0100 Subject: [PATCH 11/18] button-reply: add "reply to" feature for buttons --- src/Message/ButtonReplyMessage.php | 4 ++-- .../RequestButtonReplyMessage.php | 4 ++++ src/WhatsAppCloudApi.php | 3 ++- tests/Unit/WhatsAppCloudApiTest.php | 20 ++++++++++++------- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/Message/ButtonReplyMessage.php b/src/Message/ButtonReplyMessage.php index 8e58df4..0053456 100644 --- a/src/Message/ButtonReplyMessage.php +++ b/src/Message/ButtonReplyMessage.php @@ -16,14 +16,14 @@ class ButtonReplyMessage extends Message private ButtonAction $action; - public function __construct(string $to, string $body, ButtonAction $action, ?string $header = null, ?string $footer = null) + public function __construct(string $to, string $body, ButtonAction $action, ?string $header = null, ?string $footer = null, ?string $reply_to = null) { $this->body = $body; $this->action = $action; $this->header = $header; $this->footer = $footer; - parent::__construct($to); + parent::__construct($to, $reply_to); } public function header(): ?string diff --git a/src/Request/MessageRequest/RequestButtonReplyMessage.php b/src/Request/MessageRequest/RequestButtonReplyMessage.php index 4c3d287..ab3dc6b 100644 --- a/src/Request/MessageRequest/RequestButtonReplyMessage.php +++ b/src/Request/MessageRequest/RequestButtonReplyMessage.php @@ -33,6 +33,10 @@ public function body(): array ]; } + if ($this->message->replyTo()) { + $body['context']['message_id'] = $this->message->replyTo(); + } + return $body; } } diff --git a/src/WhatsAppCloudApi.php b/src/WhatsAppCloudApi.php index 0db05a3..bd46734 100644 --- a/src/WhatsAppCloudApi.php +++ b/src/WhatsAppCloudApi.php @@ -296,7 +296,8 @@ public function sendButton(string $to, string $body, ButtonAction $action, ?stri $body, $action, $header, - $footer + $footer, + $this->reply_to ); $request = new Request\MessageRequest\RequestButtonReplyMessage( diff --git a/tests/Unit/WhatsAppCloudApiTest.php b/tests/Unit/WhatsAppCloudApiTest.php index c5995f2..c04e2f6 100644 --- a/tests/Unit/WhatsAppCloudApiTest.php +++ b/tests/Unit/WhatsAppCloudApiTest.php @@ -903,6 +903,7 @@ public function test_send_reply_buttons() { $to = $this->faker->phoneNumber; $url = $this->buildMessageRequestUri(); + $reply_to = $this->faker->uuid; $buttonRows = [ ['id' => $this->faker->uuid, 'title' => $this->faker->text(10)], @@ -934,6 +935,9 @@ public function test_send_reply_buttons() 'header' => ['type' => 'text', 'text' => $header], 'footer' => ['text' => $footer], ], + 'context' => [ + 'message_id' => $reply_to, + ], ]; $headers = [ 'Authorization' => 'Bearer ' . $this->access_token, @@ -950,13 +954,15 @@ public function test_send_reply_buttons() $actionButtons[] = new Button($button['id'], $button['title']); } - $response = $this->whatsapp_app_cloud_api->sendButton( - $to, - $message, - new ButtonAction($actionButtons), - $header, - $footer - ); + $response = $this->whatsapp_app_cloud_api + ->replyTo($reply_to) + ->sendButton( + $to, + $message, + new ButtonAction($actionButtons), + $header, + $footer + ); $this->assertEquals(200, $response->httpStatusCode()); $this->assertEquals(json_decode($this->successfulMessageNodeResponse(), true), $response->decodedBody()); From 08e5fea8bbba155b5064d88bc1be0fd42c307bd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Sun, 19 Nov 2023 16:58:15 +0100 Subject: [PATCH 12/18] fix-emoji-removed-notification: fix a notification error when an emoji is removed from a message --- .../MessageNotificationFactory.php | 2 +- .../Unit/WebHook/NotificationFactoryTest.php | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/WebHook/Notification/MessageNotificationFactory.php b/src/WebHook/Notification/MessageNotificationFactory.php index dda783e..12773eb 100644 --- a/src/WebHook/Notification/MessageNotificationFactory.php +++ b/src/WebHook/Notification/MessageNotificationFactory.php @@ -26,7 +26,7 @@ private function buildMessageNotification(array $metadata, array $message): Mess $message['id'], new Support\Business($metadata['phone_number_id'], $metadata['display_phone_number']), $message['reaction']['message_id'], - $message['reaction']['emoji'], + $message['reaction']['emoji'] ?? '', $message['timestamp'] ); case 'sticker': diff --git a/tests/Unit/WebHook/NotificationFactoryTest.php b/tests/Unit/WebHook/NotificationFactoryTest.php index 3164487..0b1a29f 100644 --- a/tests/Unit/WebHook/NotificationFactoryTest.php +++ b/tests/Unit/WebHook/NotificationFactoryTest.php @@ -246,6 +246,47 @@ public function test_build_from_payload_can_build_a_reaction_notification() $this->assertEquals('EMOJI', $notification->emoji()); } + public function test_build_from_payload_can_build_a_removed_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" + }, + "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('', $notification->emoji()); + } + public function test_build_from_payload_can_build_an_image_notification() { $payload = json_decode('{ From 1ece11a4d2ff4a24b5608b69cdc2e56f6bdd8e18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Sun, 19 Nov 2023 17:05:13 +0100 Subject: [PATCH 13/18] fix-non-nullable-superclass-vars: set to empty string non nullable and variables --- src/WhatsAppCloudApiApp.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/WhatsAppCloudApiApp.php b/src/WhatsAppCloudApiApp.php index 8f7dc01..211efcf 100644 --- a/src/WhatsAppCloudApiApp.php +++ b/src/WhatsAppCloudApiApp.php @@ -46,8 +46,8 @@ public function __construct(?string $from_phone_number_id = null, ?string $acces { $this->loadEnv(); - $this->from_phone_number_id = $from_phone_number_id ?: $_ENV[static::APP_FROM_PHONE_NUMBER_ENV_NAME] ?? null; - $this->access_token = $access_token ?: $_ENV[static::APP_TOKEN_ENV_NAME] ?? null; + $this->from_phone_number_id = $from_phone_number_id ?: $_ENV[static::APP_FROM_PHONE_NUMBER_ENV_NAME] ?? ''; + $this->access_token = $access_token ?: $_ENV[static::APP_TOKEN_ENV_NAME] ?? ''; $this->business_id = $business_id ?: $_ENV[static::APP_BUSINESS_ID_ENV_NAME] ?? ''; $this->validate($this->from_phone_number_id, $this->access_token, $this->business_id); From f3b9626c478379c9a2a4a1f8d8a2d3ce66b02eb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Albarca?= Date: Sun, 19 Nov 2023 17:20:33 +0100 Subject: [PATCH 14/18] update-default-graph-version-to-18: update default Graph version to v18 --- src/WhatsAppCloudApi.php | 2 +- tests/Unit/WhatsAppCloudApiTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/WhatsAppCloudApi.php b/src/WhatsAppCloudApi.php index bd46734..8c4c1b3 100644 --- a/src/WhatsAppCloudApi.php +++ b/src/WhatsAppCloudApi.php @@ -14,7 +14,7 @@ class WhatsAppCloudApi /** * @const string Default Graph API version. */ - public const DEFAULT_GRAPH_VERSION = 'v17.0'; + public const DEFAULT_GRAPH_VERSION = 'v18.0'; /** * @var WhatsAppCloudApiApp The WhatsAppCloudApiApp entity. diff --git a/tests/Unit/WhatsAppCloudApiTest.php b/tests/Unit/WhatsAppCloudApiTest.php index c04e2f6..7a4f6e8 100644 --- a/tests/Unit/WhatsAppCloudApiTest.php +++ b/tests/Unit/WhatsAppCloudApiTest.php @@ -30,7 +30,7 @@ final class WhatsAppCloudApiTest extends TestCase { use ProphecyTrait; - private const TEST_GRAPH_VERSION = 'v17.0'; + private const TEST_GRAPH_VERSION = 'v18.0'; private $whatsapp_app_cloud_api; private $client_handler; From 7c7f89c3da2cc4c28e21b62501fea95e93cd1719 Mon Sep 17 00:00:00 2001 From: aalbarca Date: Sun, 19 Nov 2023 16:30:22 +0000 Subject: [PATCH 15/18] Update CHANGELOG --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 665f717..24ef0de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 2.2.0 - 2023-11-19 + +### What's Changed + +- Updated default Graph version to v18 +- Retrieve and update business profile. Thanks @winkelco +- Allow to reply messages. Thanks @johnflash4real +- Add Interactive buttons. Thanks @derrickobedgiu1 and @ianrothmann +- Retrieve a batch of notifications from Webhook. Thanks @arneee +- Optional headers in interactive list messages. Thanks @horatiua +- Fix non nullable variables when empty ENV variables are defined + ## 2.1.0 - 2023-08-12 ### What's Changed From b8d6cbfc5f0094840561ec201d1a4640981fbce4 Mon Sep 17 00:00:00 2001 From: Alejandro Albarca Date: Sun, 19 Nov 2023 17:38:40 +0100 Subject: [PATCH 16/18] add replying message sample code --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 1c40126..a28abd4 100644 --- a/README.md +++ b/README.md @@ -252,6 +252,21 @@ $whatsapp_cloud_api->sendButton( ); ``` +### Replying messages + +You can reply a previous sent message: + +```php +replyTo('') + ->sendTextMessage( + '34676104574', + 'Hey there! I\'m using WhatsApp Cloud API. Visit https://www.netflie.es' + ); +``` + ## 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. From c8c5971be43107771052eb1d6e2d589bafd9f5b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Feb 2024 17:50:56 +0000 Subject: [PATCH 17/18] Bump actions/dependency-review-action from 3 to 4 Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 3 to 4. - [Release notes](https://github.com/actions/dependency-review-action/releases) - [Commits](https://github.com/actions/dependency-review-action/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/dependency-review-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/dependency-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 4e75197..0d4a013 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,4 +17,4 @@ jobs: - name: 'Checkout Repository' uses: actions/checkout@v4 - name: 'Dependency Review' - uses: actions/dependency-review-action@v3 + uses: actions/dependency-review-action@v4 From b162fbd78d5785f6add80cb6d084f49090eb256b Mon Sep 17 00:00:00 2001 From: derrickobedgiu1 Date: Mon, 11 Mar 2024 14:27:25 +0300 Subject: [PATCH 18/18] Added react to message feature --- src/Message/ReactionMessage.php | 36 +++++++++++++++++++ .../MessageRequest/RequestReactionMessage.php | 27 ++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 src/Message/ReactionMessage.php create mode 100644 src/Request/MessageRequest/RequestReactionMessage.php diff --git a/src/Message/ReactionMessage.php b/src/Message/ReactionMessage.php new file mode 100644 index 0000000..2b29ad9 --- /dev/null +++ b/src/Message/ReactionMessage.php @@ -0,0 +1,36 @@ +emoji = $emoji; + $this->message_id = $message_id; + + parent::__construct($to, null); + } + + public function emoji(): string + { + return $this->emoji; + } + + public function message_id(): string + { + return $this->message_id; + } +} diff --git a/src/Request/MessageRequest/RequestReactionMessage.php b/src/Request/MessageRequest/RequestReactionMessage.php new file mode 100644 index 0000000..9bea82c --- /dev/null +++ b/src/Request/MessageRequest/RequestReactionMessage.php @@ -0,0 +1,27 @@ + $this->message->messagingProduct(), + 'recipient_type' => $this->message->recipientType(), + 'to' => $this->message->to(), + 'type' => $this->message->type(), + $this->message->type() => [ + 'message_id' => $this->message->message_id(), + 'emoji' => $this->message->emoji(), + ], + ]; + + return $body; + } +}