From c508732e9573e6ce52ad3ebea136620d4c8ffa32 Mon Sep 17 00:00:00 2001 From: Kevin Auivinet Date: Thu, 7 Jan 2021 10:52:04 +0100 Subject: [PATCH] [Notifier] Add notifier for Clickatell --- .../FrameworkExtension.php | 2 + .../Resources/config/notifier_transports.php | 5 + .../Notifier/Bridge/Clickatell/CHANGELOG.md | 7 ++ .../Bridge/Clickatell/ClickatellTransport.php | 92 +++++++++++++++++++ .../Clickatell/ClickatellTransportFactory.php | 44 +++++++++ .../Notifier/Bridge/Clickatell/LICENSE | 19 ++++ .../Notifier/Bridge/Clickatell/README.md | 23 +++++ .../Tests/ClickatellTransportFactoryTest.php | 43 +++++++++ .../Tests/ClickatellTransportTest.php | 65 +++++++++++++ .../Notifier/Bridge/Clickatell/composer.json | 37 ++++++++ .../Bridge/Clickatell/phpunit.xml.dist | 31 +++++++ .../Exception/UnsupportedSchemeException.php | 4 + src/Symfony/Component/Notifier/Transport.php | 2 + 13 files changed, 374 insertions(+) create mode 100644 src/Symfony/Component/Notifier/Bridge/Clickatell/CHANGELOG.md create mode 100644 src/Symfony/Component/Notifier/Bridge/Clickatell/ClickatellTransport.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Clickatell/ClickatellTransportFactory.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Clickatell/LICENSE create mode 100644 src/Symfony/Component/Notifier/Bridge/Clickatell/README.md create mode 100644 src/Symfony/Component/Notifier/Bridge/Clickatell/Tests/ClickatellTransportFactoryTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Clickatell/Tests/ClickatellTransportTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Clickatell/composer.json create mode 100644 src/Symfony/Component/Notifier/Bridge/Clickatell/phpunit.xml.dist diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index f287c8b5f0..5fa763e887 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -105,6 +105,7 @@ use Symfony\Component\Mime\Header\Headers; use Symfony\Component\Mime\MimeTypeGuesserInterface; use Symfony\Component\Mime\MimeTypes; use Symfony\Component\Notifier\Bridge\AllMySms\AllMySmsTransportFactory; +use Symfony\Component\Notifier\Bridge\Clickatell\ClickatellTransportFactory; use Symfony\Component\Notifier\Bridge\Discord\DiscordTransportFactory; use Symfony\Component\Notifier\Bridge\Esendex\EsendexTransportFactory; use Symfony\Component\Notifier\Bridge\Firebase\FirebaseTransportFactory; @@ -2249,6 +2250,7 @@ class FrameworkExtension extends Extension OctopushTransportFactory::class => 'notifier.transport_factory.octopush', MercureTransportFactory::class => 'notifier.transport_factory.mercure', GitterTransportFactory::class => 'notifier.transport_factory.gitter', + ClickatellTransportFactory::class => 'notifier.transport_factory.clickatell', ]; foreach ($classToServices as $class => $service) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php index 3d4fdcf101..f57869b0f2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; use Symfony\Component\Notifier\Bridge\AllMySms\AllMySmsTransportFactory; +use Symfony\Component\Notifier\Bridge\Clickatell\ClickatellTransportFactory; use Symfony\Component\Notifier\Bridge\Discord\DiscordTransportFactory; use Symfony\Component\Notifier\Bridge\Esendex\EsendexTransportFactory; use Symfony\Component\Notifier\Bridge\Firebase\FirebaseTransportFactory; @@ -145,6 +146,10 @@ return static function (ContainerConfigurator $container) { ->parent('notifier.transport_factory.abstract') ->tag('chatter.transport_factory') + ->set('notifier.transport_factory.clickatell', ClickatellTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + ->set('notifier.transport_factory.null', NullTransportFactory::class) ->parent('notifier.transport_factory.abstract') ->tag('chatter.transport_factory') diff --git a/src/Symfony/Component/Notifier/Bridge/Clickatell/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/Clickatell/CHANGELOG.md new file mode 100644 index 0000000000..1f2b652ac2 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Clickatell/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +5.3 +--- + + * Add the bridge diff --git a/src/Symfony/Component/Notifier/Bridge/Clickatell/ClickatellTransport.php b/src/Symfony/Component/Notifier/Bridge/Clickatell/ClickatellTransport.php new file mode 100644 index 0000000000..5584e9d3ba --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Clickatell/ClickatellTransport.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Clickatell; + +use Symfony\Component\Notifier\Exception\TransportException; +use Symfony\Component\Notifier\Exception\UnsupportedMessageTypeException; +use Symfony\Component\Notifier\Message\MessageInterface; +use Symfony\Component\Notifier\Message\SentMessage; +use Symfony\Component\Notifier\Message\SmsMessage; +use Symfony\Component\Notifier\Transport\AbstractTransport; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @author Kevin Auvinet + */ +final class ClickatellTransport extends AbstractTransport +{ + protected const HOST = 'api.clickatell.com'; + + private $authToken; + private $from; + + public function __construct(string $authToken, string $from = null, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null) + { + $this->authToken = $authToken; + $this->from = $from; + + parent::__construct($client, $dispatcher); + } + + public function __toString(): string + { + if (null === $this->from) { + return sprintf('clickatell://%s', $this->getEndpoint()); + } + + return sprintf('clickatell://%s?from=%s', $this->getEndpoint(), $this->from); + } + + public function supports(MessageInterface $message): bool + { + return $message instanceof SmsMessage; + } + + protected function doSend(MessageInterface $message): SentMessage + { + if (!$message instanceof SmsMessage) { + throw new UnsupportedMessageTypeException(__CLASS__, SmsMessage::class, $message); + } + + $endpoint = sprintf('https://%s/rest/message', $this->getEndpoint()); + + $response = $this->client->request('POST', $endpoint, [ + 'headers' => [ + 'Accept' => 'application/json', + 'Authorization' => 'Bearer '.$this->authToken, + 'Content-Type' => 'application/json', + 'X-Version' => 1, + ], + 'json' => [ + 'from' => $this->from ?? '', + 'to' => [$message->getPhone()], + 'text' => $message->getSubject(), + ], + ]); + + if (202 === $response->getStatusCode()) { + $result = $response->toArray(); + $sentMessage = new SentMessage($message, (string) $this); + $sentMessage->setMessageId($result['data']['message'][0]['apiMessageId']); + + return $sentMessage; + } + + $content = $response->toArray(false); + $errorCode = $content['error']['code'] ?? ''; + $errorInfo = $content['error']['description'] ?? ''; + $errorDocumentation = $content['error']['documentation'] ?? ''; + + throw new TransportException(sprintf('Unable to send SMS with Clickatell: Error code %d with message "%s" (%s).', $errorCode, $errorInfo, $errorDocumentation), $response); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Clickatell/ClickatellTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/Clickatell/ClickatellTransportFactory.php new file mode 100644 index 0000000000..f280233c4c --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Clickatell/ClickatellTransportFactory.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Clickatell; + +use Symfony\Component\Notifier\Exception\UnsupportedSchemeException; +use Symfony\Component\Notifier\Transport\AbstractTransportFactory; +use Symfony\Component\Notifier\Transport\Dsn; +use Symfony\Component\Notifier\Transport\TransportInterface; + +/** + * @author Kevin Auvinet + */ +final class ClickatellTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): TransportInterface + { + $scheme = $dsn->getScheme(); + + if ('clickatell' !== $scheme) { + throw new UnsupportedSchemeException($dsn, 'clickatell', $this->getSupportedSchemes()); + } + + $authToken = $this->getUser($dsn); + $from = $dsn->getOption('from'); + $host = 'default' === $dsn->getHost() ? null : $dsn->getHost(); + $port = $dsn->getPort(); + + return (new ClickatellTransport($authToken, $from, $this->client, $this->dispatcher))->setHost($host)->setPort($port); + } + + protected function getSupportedSchemes(): array + { + return ['clickatell']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Clickatell/LICENSE b/src/Symfony/Component/Notifier/Bridge/Clickatell/LICENSE new file mode 100644 index 0000000000..efb17f98e7 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Clickatell/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2021 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/Notifier/Bridge/Clickatell/README.md b/src/Symfony/Component/Notifier/Bridge/Clickatell/README.md new file mode 100644 index 0000000000..ff5eb95c4f --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Clickatell/README.md @@ -0,0 +1,23 @@ +Clickatell Notifier +=================== + +Provides [Clickatell](https://www.clickatell.com) integration for Symfony Notifier. + +DSN example +----------- + +``` +CLICKATELL_DSN=clickatell://ACCESS_TOKEN@default?from=FROM +``` + +where: + - `ACCESS_TOKEN` is your Clickatell auth access token + - `FROM` is the sender + +Resources +--------- + + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/src/Symfony/Component/Notifier/Bridge/Clickatell/Tests/ClickatellTransportFactoryTest.php b/src/Symfony/Component/Notifier/Bridge/Clickatell/Tests/ClickatellTransportFactoryTest.php new file mode 100644 index 0000000000..0ad7cb7ec2 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Clickatell/Tests/ClickatellTransportFactoryTest.php @@ -0,0 +1,43 @@ + ['clickatell://host?from=FROM']; + } + + public function unsupportedSchemeProvider(): iterable + { + yield ['somethingElse://authtoken@default?from=FROM']; + yield ['somethingElse://authtoken@default']; // missing "from" option + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Clickatell/Tests/ClickatellTransportTest.php b/src/Symfony/Component/Notifier/Bridge/Clickatell/Tests/ClickatellTransportTest.php new file mode 100644 index 0000000000..c3d2db0b6f --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Clickatell/Tests/ClickatellTransportTest.php @@ -0,0 +1,65 @@ +createMock(HttpClientInterface::class)); + $transport->setHost('clickatellHost'); + + $this->assertSame('clickatell://clickatellHost?from=fromValue', (string) $transport); + } + + public function testSupports() + { + $transport = new ClickatellTransport('authToken', 'fromValue', $this->createMock(HttpClientInterface::class)); + + $this->assertTrue($transport->supports(new SmsMessage('+33612345678', 'testSmsMessage'))); + $this->assertFalse($transport->supports($this->createMock(MessageInterface::class))); + } + + public function testExceptionIsThrownWhenNonMessageIsSend() + { + $transport = new ClickatellTransport('authToken', 'fromValue', $this->createMock(HttpClientInterface::class)); + + $this->expectException(LogicException::class); + $transport->send($this->createMock(MessageInterface::class)); + } + + public function testExceptionIsThrownWhenHttpSendFailed() + { + $response = $this->createMock(ResponseInterface::class); + $response->expects($this->exactly(2)) + ->method('getStatusCode') + ->willReturn(500); + $response->expects($this->once()) + ->method('getContent') + ->willReturn(json_encode([ + 'error' => [ + 'code' => 105, + 'description' => 'Invalid Account Reference EX0000000', + 'documentation' => 'https://documentation-page', + ], + ])); + + $client = new MockHttpClient($response); + + $transport = new ClickatellTransport('authToken', 'fromValue', $client); + $this->expectException(TransportException::class); + $this->expectExceptionMessage('Unable to send SMS with Clickatell: Error code 105 with message "Invalid Account Reference EX0000000" (https://documentation-page).'); + + $transport->send(new SmsMessage('+33612345678', 'testSmsMessage')); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Clickatell/composer.json b/src/Symfony/Component/Notifier/Bridge/Clickatell/composer.json new file mode 100644 index 0000000000..404589753a --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Clickatell/composer.json @@ -0,0 +1,37 @@ +{ + "name": "symfony/clickatell-notifier", + "type": "symfony-bridge", + "description": "Symfony Clickatell Notifier Bridge", + "keywords": ["sms", "clickatell", "notifier"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + }, + { + "name": "Kevin Auvinet", + "email": "k.auvinet@gmail.com" + } + ], + "require": { + "php": ">=7.2.5", + "symfony/http-client": "^4.3|^5.0", + "symfony/notifier": "^5.3" + }, + "require-dev": { + "symfony/event-dispatcher": "^4.3|^5.0" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Clickatell\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/src/Symfony/Component/Notifier/Bridge/Clickatell/phpunit.xml.dist b/src/Symfony/Component/Notifier/Bridge/Clickatell/phpunit.xml.dist new file mode 100644 index 0000000000..8becf15134 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Clickatell/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Resources + ./Tests + ./vendor + + + + diff --git a/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php b/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php index 574d419a9f..1200ccb31f 100644 --- a/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php +++ b/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php @@ -108,6 +108,10 @@ class UnsupportedSchemeException extends LogicException 'class' => Bridge\Gitter\GitterTransportFactory::class, 'package' => 'symfony/gitter-notifier', ], + 'clickatell' => [ + 'class' => Bridge\Clickatell\ClickatellTransportFactory::class, + 'package' => 'symfony/clickatell-notifier', + ], ]; /** diff --git a/src/Symfony/Component/Notifier/Transport.php b/src/Symfony/Component/Notifier/Transport.php index fd3af9b05c..b2430bbe9a 100644 --- a/src/Symfony/Component/Notifier/Transport.php +++ b/src/Symfony/Component/Notifier/Transport.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Notifier; use Symfony\Component\Notifier\Bridge\AllMySms\AllMySmsTransportFactory; +use Symfony\Component\Notifier\Bridge\Clickatell\ClickatellTransportFactory; use Symfony\Component\Notifier\Bridge\Discord\DiscordTransportFactory; use Symfony\Component\Notifier\Bridge\Esendex\EsendexTransportFactory; use Symfony\Component\Notifier\Bridge\Firebase\FirebaseTransportFactory; @@ -72,6 +73,7 @@ class Transport GatewayApiTransportFactory::class, OctopushTransportFactory::class, GitterTransportFactory::class, + ClickatellTransportFactory::class, ]; private $factories;