Skip to content

Commit 6141656

Browse files
committed
feature #51315 [Notifier][Webhook] Add Vonage support (smnandre)
This PR was squashed before being merged into the 6.4 branch. Discussion ---------- [Notifier][Webhook] Add Vonage support | Q | A | ------------- | --- | Branch? | 6.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Ticket | | License | MIT | Doc PR | Add support for `RemoteEvent` & `Webhook` to Vonage Notifier Bridge Event statuses and payloads come from Vonage [documentation](https://developer.vonage.com/en/api/messages-olympus#inbound-message-req-body). Commits ------- 490adc1 [Notifier][Webhook] Add Vonage support
2 parents bacbe8d + 490adc1 commit 6141656

File tree

10 files changed

+257
-0
lines changed

10 files changed

+257
-0
lines changed

src/Symfony/Component/Notifier/Bridge/Vonage/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
6.4
5+
---
6+
7+
* Add support for `RemoteEvent` and `Webhook`
8+
49
6.2
510
---
611

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"message_uuid": "aaaaaaaa-bbbb-cccc-dddd-0123456789ab",
3+
"to": "447700900000",
4+
"from": "447700900001",
5+
"timestamp": {},
6+
"status": "delivered",
7+
"usage": {
8+
"currency": "EUR",
9+
"price": "0.0333"
10+
},
11+
"client_ref": "string",
12+
"channel": "sms",
13+
"destination": {
14+
"network_code": "12345"
15+
},
16+
"sms": {
17+
"count_total": "2"
18+
}
19+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
use Symfony\Component\RemoteEvent\Event\Sms\SmsEvent;
4+
5+
$wh = new SmsEvent(SmsEvent::DELIVERED, 'aaaaaaaa-bbbb-cccc-dddd-0123456789ab', json_decode(file_get_contents(str_replace('.php', '.json', __FILE__)), true, flags: \JSON_THROW_ON_ERROR));
6+
$wh->setRecipientPhone('447700900000');
7+
8+
return $wh;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"message_uuid": "aaaaaaaa-bbbb-cccc-dddd-0123456789ab",
3+
"to": "447700900000",
4+
"from": "447700900001",
5+
"timestamp": {},
6+
"status": "rejected",
7+
"error": {
8+
"type": "https://developer.nexmo.com/api-errors/messages-olympus#1000",
9+
"title": 1000,
10+
"detail": "Throttled - You have exceeded the submission capacity allowed on this account. Please wait and retry",
11+
"instance": "bf0ca0bf927b3b52e3cb03217e1a1ddf"
12+
},
13+
"usage": {
14+
"currency": "EUR",
15+
"price": "0.0333"
16+
},
17+
"client_ref": "string",
18+
"channel": "sms",
19+
"destination": {
20+
"network_code": "12345"
21+
},
22+
"sms": {
23+
"count_total": "2"
24+
}
25+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
use Symfony\Component\RemoteEvent\Event\Sms\SmsEvent;
4+
5+
$wh = new SmsEvent(SmsEvent::FAILED, 'aaaaaaaa-bbbb-cccc-dddd-0123456789ab', json_decode(file_get_contents(str_replace('.php', '.json', __FILE__)), true, flags: \JSON_THROW_ON_ERROR));
6+
$wh->setRecipientPhone('447700900000');
7+
8+
return $wh;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"message_uuid": "aaaaaaaa-bbbb-cccc-dddd-0123456789ab",
3+
"to": "447700900000",
4+
"from": "447700900001",
5+
"timestamp": {},
6+
"status": "undeliverable",
7+
"error": {
8+
"type": "https://developer.nexmo.com/api-errors/messages-olympus#1260",
9+
"title": 1260,
10+
"detail": "Destination unreachable - The message could not be delivered to the phone number. If using Viber Business Messages your account might not be enabled for this country.",
11+
"instance": "bf0ca0bf927b3b52e3cb03217e1a1ddf"
12+
},
13+
"usage": {
14+
"currency": "EUR",
15+
"price": "0.0333"
16+
},
17+
"client_ref": "string",
18+
"channel": "sms",
19+
"destination": {
20+
"network_code": "12345"
21+
},
22+
"sms": {
23+
"count_total": "2"
24+
}
25+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
use Symfony\Component\RemoteEvent\Event\Sms\SmsEvent;
4+
5+
$wh = new SmsEvent(SmsEvent::FAILED, 'aaaaaaaa-bbbb-cccc-dddd-0123456789ab', json_decode(file_get_contents(str_replace('.php', '.json', __FILE__)), true, flags: \JSON_THROW_ON_ERROR));
6+
$wh->setRecipientPhone('447700900000');
7+
8+
return $wh;
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Notifier\Bridge\Vonage\Tests\Webhook;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\Notifier\Bridge\Vonage\Webhook\VonageRequestParser;
16+
use Symfony\Component\Webhook\Client\RequestParserInterface;
17+
use Symfony\Component\Webhook\Exception\RejectWebhookException;
18+
use Symfony\Component\Webhook\Test\AbstractRequestParserTestCase;
19+
20+
class VonageRequestParserTest extends AbstractRequestParserTestCase
21+
{
22+
public function testMissingAuthorizationTokenThrows()
23+
{
24+
$request = $this->createRequest('{}');
25+
$request->headers->remove('Authorization');
26+
$parser = $this->createRequestParser();
27+
28+
$this->expectException(RejectWebhookException::class);
29+
$this->expectExceptionMessage('Missing "Authorization" header');
30+
31+
$parser->parse($request, $this->getSecret());
32+
}
33+
34+
public function testInvalidAuthorizationTokenThrows()
35+
{
36+
$request = $this->createRequest('{}');
37+
$request->headers->set('Authorization', 'Invalid Header');
38+
$parser = $this->createRequestParser();
39+
40+
$this->expectException(RejectWebhookException::class);
41+
$this->expectExceptionMessage('Signature is wrong');
42+
43+
$parser->parse($request, $this->getSecret());
44+
}
45+
46+
protected function createRequestParser(): RequestParserInterface
47+
{
48+
return new VonageRequestParser();
49+
}
50+
51+
protected function createRequest(string $payload): Request
52+
{
53+
// JWT Token signed with the secret key
54+
$jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.kK9JnTXZwzNo3BYNXJT57PGLnQk-Xyu7IBhRWFmc4C0';
55+
56+
$request = parent::createRequest($payload);
57+
$request->headers->set('Authorization', 'Bearer '.$jwt);
58+
59+
return $request;
60+
}
61+
62+
protected function getSecret(): string
63+
{
64+
return 'secret-key';
65+
}
66+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Notifier\Bridge\Vonage\Webhook;
13+
14+
use Symfony\Component\HttpFoundation\ChainRequestMatcher;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpFoundation\RequestMatcher\IsJsonRequestMatcher;
17+
use Symfony\Component\HttpFoundation\RequestMatcher\MethodRequestMatcher;
18+
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
19+
use Symfony\Component\RemoteEvent\Event\Sms\SmsEvent;
20+
use Symfony\Component\Webhook\Client\AbstractRequestParser;
21+
use Symfony\Component\Webhook\Exception\RejectWebhookException;
22+
23+
final class VonageRequestParser extends AbstractRequestParser
24+
{
25+
protected function getRequestMatcher(): RequestMatcherInterface
26+
{
27+
return new ChainRequestMatcher([
28+
new MethodRequestMatcher('POST'),
29+
new IsJsonRequestMatcher(),
30+
]);
31+
}
32+
33+
protected function doParse(Request $request, string $secret): ?SmsEvent
34+
{
35+
// Signed webhooks: https://developer.vonage.com/en/getting-started/concepts/webhooks#validating-signed-webhooks
36+
if (!$request->headers->has('Authorization')) {
37+
throw new RejectWebhookException(406, 'Missing "Authorization" header.');
38+
}
39+
$this->validateSignature(substr($request->headers->get('Authorization'), \strlen('Bearer ')), $secret);
40+
41+
// Statuses: https://developer.vonage.com/en/api/messages-olympus#message-status
42+
$payload = $request->toArray();
43+
if (
44+
!isset($payload['status'])
45+
|| !isset($payload['message_uuid'])
46+
|| !isset($payload['to'])
47+
|| !isset($payload['channel'])
48+
) {
49+
throw new RejectWebhookException(406, 'Payload is malformed.');
50+
}
51+
52+
if ('sms' !== $payload['channel']) {
53+
throw new RejectWebhookException(406, sprintf('Unsupported channel "%s".', $payload['channel']));
54+
}
55+
56+
$name = match ($payload['status']) {
57+
'delivered' => SmsEvent::DELIVERED,
58+
'rejected' => SmsEvent::FAILED,
59+
'submitted' => null,
60+
'undeliverable' => SmsEvent::FAILED,
61+
default => throw new RejectWebhookException(406, sprintf('Unsupported event "%s".', $payload['status'])),
62+
};
63+
if (!$name) {
64+
return null;
65+
}
66+
67+
$event = new SmsEvent($name, $payload['message_uuid'], $payload);
68+
$event->setRecipientPhone($payload['to']);
69+
70+
return $event;
71+
}
72+
73+
private function validateSignature(string $jwt, string $secret): void
74+
{
75+
$tokenParts = explode('.', $jwt);
76+
if (3 !== \count($tokenParts)) {
77+
throw new RejectWebhookException(406, 'Signature is wrong.');
78+
}
79+
80+
[$header, $payload, $signature] = $tokenParts;
81+
if ($signature !== $this->base64EncodeUrl(hash_hmac('sha256', $header.'.'.$payload, $secret, true))) {
82+
throw new RejectWebhookException(406, 'Signature is wrong.');
83+
}
84+
}
85+
86+
private function base64EncodeUrl(string $string): string
87+
{
88+
return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($string));
89+
}
90+
}

src/Symfony/Component/Notifier/Bridge/Vonage/composer.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
"symfony/http-client": "^5.4|^6.0|^7.0",
2121
"symfony/notifier": "^6.2.7|^7.0"
2222
},
23+
"require-dev": {
24+
"symfony/webhook": "^6.4|^7.0"
25+
},
2326
"autoload": {
2427
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Vonage\\": "" },
2528
"exclude-from-classmap": [

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy