diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 76b16900524f2..dd4eeaa643c6b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -127,6 +127,7 @@ use Symfony\Component\Notifier\Bridge\Mattermost\MattermostTransportFactory; use Symfony\Component\Notifier\Bridge\Mercure\MercureTransportFactory; use Symfony\Component\Notifier\Bridge\MessageBird\MessageBirdTransport; +use Symfony\Component\Notifier\Bridge\MessageMedia\MessageMediaTransportFactory; use Symfony\Component\Notifier\Bridge\MicrosoftTeams\MicrosoftTeamsTransportFactory; use Symfony\Component\Notifier\Bridge\Mobyt\MobytTransportFactory; use Symfony\Component\Notifier\Bridge\Nexmo\NexmoTransportFactory; @@ -2431,6 +2432,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ MattermostTransportFactory::class => 'notifier.transport_factory.mattermost', MercureTransportFactory::class => 'notifier.transport_factory.mercure', MessageBirdTransport::class => 'notifier.transport_factory.messagebird', + MessageMediaTransportFactory::class => 'notifier.transport_factory.messagemedia', MicrosoftTeamsTransportFactory::class => 'notifier.transport_factory.microsoftteams', MobytTransportFactory::class => 'notifier.transport_factory.mobyt', NexmoTransportFactory::class => 'notifier.transport_factory.nexmo', @@ -2460,6 +2462,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ case 'lightsms': $package = 'light-sms'; break; case 'linkedin': $package = 'linked-in'; break; case 'messagebird': $package = 'message-bird'; break; + case 'messagemedia': $package = 'message-media'; break; case 'microsoftteams': $package = 'microsoft-teams'; break; case 'ovhcloud': $package = 'ovh-cloud'; break; case 'rocketchat': $package = 'rocket-chat'; break; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php index 8dcd292315edb..1451fa7289c9f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php @@ -29,6 +29,7 @@ use Symfony\Component\Notifier\Bridge\Mattermost\MattermostTransportFactory; use Symfony\Component\Notifier\Bridge\Mercure\MercureTransportFactory; use Symfony\Component\Notifier\Bridge\MessageBird\MessageBirdTransportFactory; +use Symfony\Component\Notifier\Bridge\MessageMedia\MessageMediaTransportFactory; use Symfony\Component\Notifier\Bridge\MicrosoftTeams\MicrosoftTeamsTransportFactory; use Symfony\Component\Notifier\Bridge\Mobyt\MobytTransportFactory; use Symfony\Component\Notifier\Bridge\Nexmo\NexmoTransportFactory; @@ -191,6 +192,10 @@ ->parent('notifier.transport_factory.abstract') ->tag('texter.transport_factory') + ->set('notifier.transport_factory.messagemedia', MessageMediaTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') + ->set('notifier.transport_factory.telnyx', TelnyxTransportFactory::class) ->parent('notifier.transport_factory.abstract') ->tag('texter.transport_factory') diff --git a/src/Symfony/Component/Notifier/Bridge/MessageMedia/.gitattributes b/src/Symfony/Component/Notifier/Bridge/MessageMedia/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MessageMedia/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Component/Notifier/Bridge/MessageMedia/.gitignore b/src/Symfony/Component/Notifier/Bridge/MessageMedia/.gitignore new file mode 100644 index 0000000000000..c49a5d8df5c65 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MessageMedia/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Notifier/Bridge/MessageMedia/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/MessageMedia/CHANGELOG.md new file mode 100644 index 0000000000000..3a08c7ededfcd --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MessageMedia/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +5.4 +--- + + * Add the bridge diff --git a/src/Symfony/Component/Notifier/Bridge/MessageMedia/LICENSE b/src/Symfony/Component/Notifier/Bridge/MessageMedia/LICENSE new file mode 100644 index 0000000000000..efb17f98e7dd3 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MessageMedia/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/MessageMedia/MessageMediaTransport.php b/src/Symfony/Component/Notifier/Bridge/MessageMedia/MessageMediaTransport.php new file mode 100644 index 0000000000000..6421b464ff16b --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MessageMedia/MessageMediaTransport.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\MessageMedia; + +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\Exception\DecodingExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @author Adrian Nguyen + */ +final class MessageMediaTransport extends AbstractTransport +{ + protected const HOST = 'api.messagemedia.com'; + + private $apiKey; + private $apiSecret; + private $from; + + public function __construct(string $apiKey, string $apiSecret, string $from = null, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null) + { + $this->apiKey = $apiKey; + $this->apiSecret = $apiSecret; + $this->from = $from; + + parent::__construct($client, $dispatcher); + } + + public function __toString(): string + { + if (null !== $this->from) { + return sprintf('messagemedia://%s?from=%s', $this->getEndpoint(), $this->from); + } + + return sprintf('messagemedia://%s', $this->getEndpoint()); + } + + 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/v1/messages', $this->getEndpoint()); + $response = $this->client->request( + 'POST', + $endpoint, + [ + 'auth_basic' => $this->apiKey.':'.$this->apiSecret, + 'json' => [ + 'messages' => [ + [ + 'destination_number' => $message->getPhone(), + 'source_number' => $this->from, + 'content' => $message->getSubject(), + ], + ], + ], + ] + ); + + if (202 === $response->getStatusCode()) { + $result = $response->toArray(false)['messages'][0]; + $sentMessage = new SentMessage($message, (string) $this); + $sentMessage->setMessageId($result['message_id']); + + return $sentMessage; + } + + try { + $error = $response->toArray(false); + + $errorMessage = $error['details'][0] ?? ($error['message'] ?? 'Unknown reason'); + } catch (DecodingExceptionInterface | TransportExceptionInterface $e) { + $errorMessage = 'Unknown reason'; + } + + throw new TransportException(sprintf('Unable to send the SMS: "%s".', $errorMessage), $response); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MessageMedia/MessageMediaTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/MessageMedia/MessageMediaTransportFactory.php new file mode 100644 index 0000000000000..4f55f64e8ba5e --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MessageMedia/MessageMediaTransportFactory.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\MessageMedia; + +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 Adrian Nguyen + */ +final class MessageMediaTransportFactory extends AbstractTransportFactory +{ + /** + * @return MessageMediaTransport + */ + public function create(Dsn $dsn): TransportInterface + { + $scheme = $dsn->getScheme(); + + if ('messagemedia' !== $scheme) { + throw new UnsupportedSchemeException($dsn, 'messagemedia', $this->getSupportedSchemes()); + } + + $apiKey = $this->getUser($dsn); + $apiSecret = $this->getPassword($dsn); + $from = $dsn->getOption('from'); + $host = 'default' === $dsn->getHost() ? null : $dsn->getHost(); + $port = $dsn->getPort(); + + return (new MessageMediaTransport($apiKey, $apiSecret, $from, $this->client, $this->dispatcher))->setHost($host)->setPort($port); + } + + protected function getSupportedSchemes(): array + { + return ['messagemedia']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MessageMedia/README.md b/src/Symfony/Component/Notifier/Bridge/MessageMedia/README.md new file mode 100644 index 0000000000000..d29bb1b8d80ae --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MessageMedia/README.md @@ -0,0 +1,25 @@ +MessageMedia Notifier +================= + +Provides [MessageMedia](https://messagemedia.com/) integration for Symfony Notifier. + +DSN example +----------- + +``` +MESSAGEMEDIA_DSN=messagemedia://API_KEY:API_SECRET@default?from=FROM +``` + +where: + - `API_KEY` is your API key + - `API_SECRET` is your API secret + - `FROM` is your registered sender ID (optional). Accepted values: 3-15 letters, could be alpha tag, shortcode or international phone number. +When phone number starts with a `+` sign, it needs to be url encoded in the DSN + +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/MessageMedia/Tests/MessageMediaTransportFactoryTest.php b/src/Symfony/Component/Notifier/Bridge/MessageMedia/Tests/MessageMediaTransportFactoryTest.php new file mode 100644 index 0000000000000..2dbb5ce4b03b1 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MessageMedia/Tests/MessageMediaTransportFactoryTest.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\MessageMedia\Tests; + +use Symfony\Component\Notifier\Bridge\MessageMedia\MessageMediaTransportFactory; +use Symfony\Component\Notifier\Test\TransportFactoryTestCase; +use Symfony\Component\Notifier\Transport\TransportFactoryInterface; + +final class MessageMediaTransportFactoryTest extends TransportFactoryTestCase +{ + /** + * @return MessageMediaTransportFactory + */ + public function createFactory(): TransportFactoryInterface + { + return new MessageMediaTransportFactory(); + } + + public function createProvider(): iterable + { + yield [ + 'messagemedia://host.test', + 'messagemedia://apiKey:apiSecret@host.test', + ]; + + yield [ + 'messagemedia://host.test?from=TEST', + 'messagemedia://apiKey:apiSecret@host.test?from=TEST', + ]; + } + + public function supportsProvider(): iterable + { + yield [true, 'messagemedia://apiKey:apiSecret@default']; + yield [false, 'somethingElse://apiKey:apiSecret@default']; + } + + public function unsupportedSchemeProvider(): iterable + { + yield ['somethingElse://apiKey:apiSecret@default']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MessageMedia/Tests/MessageMediaTransportTest.php b/src/Symfony/Component/Notifier/Bridge/MessageMedia/Tests/MessageMediaTransportTest.php new file mode 100644 index 0000000000000..147078e64aa5c --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MessageMedia/Tests/MessageMediaTransportTest.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\MessageMedia\Tests; + +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\Notifier\Bridge\MessageMedia\MessageMediaTransport; +use Symfony\Component\Notifier\Exception\TransportException; +use Symfony\Component\Notifier\Exception\TransportExceptionInterface; +use Symfony\Component\Notifier\Message\ChatMessage; +use Symfony\Component\Notifier\Message\MessageInterface; +use Symfony\Component\Notifier\Message\SmsMessage; +use Symfony\Component\Notifier\Test\TransportTestCase; +use Symfony\Component\Notifier\Transport\TransportInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; + +final class MessageMediaTransportTest extends TransportTestCase +{ + /** + * @return MessageMediaTransport + */ + public function createTransport(HttpClientInterface $client = null, string $from = null): TransportInterface + { + return new MessageMediaTransport('apiKey', 'apiSecret', $from, $client ?? $this->createMock(HttpClientInterface::class)); + } + + public function toStringProvider(): iterable + { + yield ['messagemedia://api.messagemedia.com', $this->createTransport()]; + yield ['messagemedia://api.messagemedia.com?from=TEST', $this->createTransport(null, 'TEST')]; + } + + public function supportedMessagesProvider(): iterable + { + yield [new SmsMessage('0491570156', 'Hello!')]; + } + + public function unsupportedMessagesProvider(): iterable + { + yield [new ChatMessage('Hello!')]; + yield [$this->createMock(MessageInterface::class)]; + } + + /** + * @dataProvider exceptionIsThrownWhenHttpSendFailedProvider + * + * @throws TransportExceptionInterface + */ + public function testExceptionIsThrownWhenHttpSendFailed(int $statusCode, string $content, string $expectedExceptionMessage) + { + $response = $this->createMock(ResponseInterface::class); + $response->method('getStatusCode') + ->willReturn($statusCode); + $response->method('getContent') + ->willReturn($content); + + $client = new MockHttpClient($response); + + $transport = new MessageMediaTransport('apiKey', 'apiSecret', null, $client); + $this->expectException(TransportException::class); + $this->expectExceptionMessage($expectedExceptionMessage); + + $transport->send(new SmsMessage('+61491570156', 'Hello!')); + } + + public function exceptionIsThrownWhenHttpSendFailedProvider(): iterable + { + yield [503, '', 'Unable to send the SMS: "Unknown reason".']; + yield [500, '{"details": ["Something went wrong."]}', 'Unable to send the SMS: "Something went wrong.".']; + yield [403, '{"message": "Forbidden."}', 'Unable to send the SMS: "Forbidden.']; + yield [401, '{"Unauthenticated"}', 'Unable to send the SMS: "Unknown reason".']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/MessageMedia/composer.json b/src/Symfony/Component/Notifier/Bridge/MessageMedia/composer.json new file mode 100644 index 0000000000000..a1fc216a12a49 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MessageMedia/composer.json @@ -0,0 +1,30 @@ +{ + "name": "symfony/message-media-notifier", + "type": "symfony-bridge", + "description": "Symfony MessageMedia Notifier Bridge", + "keywords": ["sms", "messagemedia", "notifier"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Adrian Nguyen", + "email": "vuphuong87@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2.5", + "symfony/http-client": "^4.4|^5.2|^6.0", + "symfony/notifier": "^5.3|^6.0" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\MessageMedia\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/src/Symfony/Component/Notifier/Bridge/MessageMedia/phpunit.xml.dist b/src/Symfony/Component/Notifier/Bridge/MessageMedia/phpunit.xml.dist new file mode 100644 index 0000000000000..7fd1fbc068814 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/MessageMedia/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 abde8fb90d56c..908ea9588d227 100644 --- a/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php +++ b/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php @@ -92,6 +92,10 @@ class UnsupportedSchemeException extends LogicException 'class' => Bridge\MessageBird\MessageBirdTransportFactory::class, 'package' => 'symfony/message-bird-notifier', ], + 'messagemedia' => [ + 'class' => Bridge\MessageMedia\MessageMediaTransportFactory::class, + 'package' => 'symfony/message-media-notifier', + ], 'microsoftteams' => [ 'class' => Bridge\MicrosoftTeams\MicrosoftTeamsTransportFactory::class, 'package' => 'symfony/microsoft-teams-notifier', diff --git a/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php b/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php index 87310b64562b1..81e0e49cd94ce 100644 --- a/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php +++ b/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php @@ -31,6 +31,7 @@ use Symfony\Component\Notifier\Bridge\Mattermost\MattermostTransportFactory; use Symfony\Component\Notifier\Bridge\Mercure\MercureTransportFactory; use Symfony\Component\Notifier\Bridge\MessageBird\MessageBirdTransportFactory; +use Symfony\Component\Notifier\Bridge\MessageMedia\MessageMediaTransportFactory; use Symfony\Component\Notifier\Bridge\MicrosoftTeams\MicrosoftTeamsTransportFactory; use Symfony\Component\Notifier\Bridge\Mobyt\MobytTransportFactory; use Symfony\Component\Notifier\Bridge\Nexmo\NexmoTransportFactory; @@ -77,6 +78,7 @@ public static function setUpBeforeClass(): void MattermostTransportFactory::class => false, MercureTransportFactory::class => false, MessageBirdTransportFactory::class => false, + MessageMediaTransportFactory::class => false, MicrosoftTeamsTransportFactory::class => false, MobytTransportFactory::class => false, NexmoTransportFactory::class => false, @@ -129,6 +131,7 @@ public function messageWhereSchemeIsPartOfSchemeToPackageMapProvider(): \Generat yield ['mattermost', 'symfony/mattermost-notifier']; yield ['mercure', 'symfony/mercure-notifier']; yield ['messagebird', 'symfony/message-bird-notifier']; + yield ['messagemedia', 'symfony/message-media-notifier']; yield ['microsoftteams', 'symfony/microsoft-teams-notifier']; yield ['mobyt', 'symfony/mobyt-notifier']; yield ['nexmo', 'symfony/nexmo-notifier']; diff --git a/src/Symfony/Component/Notifier/Transport.php b/src/Symfony/Component/Notifier/Transport.php index 868fb5fa372a5..ecb7420e18378 100644 --- a/src/Symfony/Component/Notifier/Transport.php +++ b/src/Symfony/Component/Notifier/Transport.php @@ -24,6 +24,7 @@ use Symfony\Component\Notifier\Bridge\LightSms\LightSmsTransportFactory; use Symfony\Component\Notifier\Bridge\Mattermost\MattermostTransportFactory; use Symfony\Component\Notifier\Bridge\MessageBird\MessageBirdTransportFactory; +use Symfony\Component\Notifier\Bridge\MessageMedia\MessageMediaTransportFactory; use Symfony\Component\Notifier\Bridge\MicrosoftTeams\MicrosoftTeamsTransport; use Symfony\Component\Notifier\Bridge\Mobyt\MobytTransportFactory; use Symfony\Component\Notifier\Bridge\Nexmo\NexmoTransportFactory; @@ -69,6 +70,7 @@ class Transport LightSmsTransportFactory::class, MattermostTransportFactory::class, MessageBirdTransportFactory::class, + MessageMediaTransportFactory::class, MicrosoftTeamsTransport::class, MobytTransportFactory::class, NexmoTransportFactory::class,