8000 feature #51315 [Notifier][Webhook] Add Vonage support (smnandre) · symfony/symfony@6141656 · GitHub
[go: up one dir, main page]

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)
0