From 69a4a0fdec67ce33a2c0a1b4767f0dca9aedfbcf Mon Sep 17 00:00:00 2001 From: Lukas Kaltenbach Date: Tue, 2 Jul 2024 20:45:16 +0200 Subject: [PATCH] [Notifier] Add Sipgate bridge --- .../FrameworkExtension.php | 1 + .../Resources/config/notifier_transports.php | 1 + .../Notifier/Bridge/Sipgate/.gitattributes | 3 + .../Notifier/Bridge/Sipgate/.gitignore | 3 + .../Notifier/Bridge/Sipgate/CHANGELOG.md | 7 ++ .../Component/Notifier/Bridge/Sipgate/LICENSE | 19 ++++ .../Notifier/Bridge/Sipgate/README.md | 24 +++++ .../Bridge/Sipgate/SipgateTransport.php | 92 ++++++++++++++++ .../Sipgate/SipgateTransportFactory.php | 42 ++++++++ .../Tests/SipgateTransportFactoryTest.php | 43 ++++++++ .../Sipgate/Tests/SipgateTransportTest.php | 100 ++++++++++++++++++ .../Notifier/Bridge/Sipgate/composer.json | 32 ++++++ .../Notifier/Bridge/Sipgate/phpunit.xml.dist | 31 ++++++ .../Exception/UnsupportedSchemeException.php | 4 + .../UnsupportedSchemeExceptionTest.php | 2 + src/Symfony/Component/Notifier/Transport.php | 1 + 16 files changed, 405 insertions(+) create mode 100644 src/Symfony/Component/Notifier/Bridge/Sipgate/.gitattributes create mode 100644 src/Symfony/Component/Notifier/Bridge/Sipgate/.gitignore create mode 100644 src/Symfony/Component/Notifier/Bridge/Sipgate/CHANGELOG.md create mode 100644 src/Symfony/Component/Notifier/Bridge/Sipgate/LICENSE create mode 100644 src/Symfony/Component/Notifier/Bridge/Sipgate/README.md create mode 100644 src/Symfony/Component/Notifier/Bridge/Sipgate/SipgateTransport.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Sipgate/SipgateTransportFactory.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Sipgate/Tests/SipgateTransportFactoryTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Sipgate/Tests/SipgateTransportTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Sipgate/composer.json create mode 100644 src/Symfony/Component/Notifier/Bridge/Sipgate/phpunit.xml.dist diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 1114246cca3eb..46ba61aa64984 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -2801,6 +2801,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ NotifierBridge\RingCentral\RingCentralTransportFactory::class => 'notifier.transport_factory.ring-central', NotifierBridge\RocketChat\RocketChatTransportFactory::class => 'notifier.transport_factory.rocket-chat', NotifierBridge\Sendberry\SendberryTransportFactory::class => 'notifier.transport_factory.sendberry', + NotifierBridge\Sipgate\SipgateTransportFactory::class => 'notifier.transport_factory.sipgate', NotifierBridge\SimpleTextin\SimpleTextinTransportFactory::class => 'notifier.transport_factory.simple-textin', NotifierBridge\Sevenio\SevenIoTransportFactory::class => 'notifier.transport_factory.sevenio', NotifierBridge\Sinch\SinchTransportFactory::class => 'notifier.transport_factory.sinch', diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php index 5ddc9ae240ea1..98ff2d50c2f58 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php @@ -93,6 +93,7 @@ 'ring-central' => Bridge\RingCentral\RingCentralTransportFactory::class, 'sendberry' => Bridge\Sendberry\SendberryTransportFactory::class, 'sevenio' => Bridge\Sevenio\SevenIoTransportFactory::class, + 'sipgate' => Bridge\Sipgate\SipgateTransportFactory::class, 'simple-textin' => Bridge\SimpleTextin\SimpleTextinTransportFactory::class, 'sinch' => Bridge\Sinch\SinchTransportFactory::class, 'sms-biuras' => Bridge\SmsBiuras\SmsBiurasTransportFactory::class, diff --git a/src/Symfony/Component/Notifier/Bridge/Sipgate/.gitattributes b/src/Symfony/Component/Notifier/Bridge/Sipgate/.gitattributes new file mode 100644 index 0000000000000..14c3c35940427 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sipgate/.gitattributes @@ -0,0 +1,3 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.git* export-ignore diff --git a/src/Symfony/Component/Notifier/Bridge/Sipgate/.gitignore b/src/Symfony/Component/Notifier/Bridge/Sipgate/.gitignore new file mode 100644 index 0000000000000..c49a5d8df5c65 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sipgate/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Notifier/Bridge/Sipgate/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/Sipgate/CHANGELOG.md new file mode 100644 index 0000000000000..00149ea5ac6f5 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sipgate/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +7.2 +--- + + * Add the bridge diff --git a/src/Symfony/Component/Notifier/Bridge/Sipgate/LICENSE b/src/Symfony/Component/Notifier/Bridge/Sipgate/LICENSE new file mode 100644 index 0000000000000..e374a5c8339d3 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sipgate/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2024-present 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/Sipgate/README.md b/src/Symfony/Component/Notifier/Bridge/Sipgate/README.md new file mode 100644 index 0000000000000..a0259b3962b11 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sipgate/README.md @@ -0,0 +1,24 @@ +Sipgate Notifier +================ + +Provides [Sipgate](https://www.sipgate.de) integration for Symfony Notifier. + +DSN example +----------- + +``` +SIPGATE_DSN=sipgate://TOKEN_ID:TOKEN@default?senderId=SENDER_ID +``` + +where: + - `TOKEN_ID` is your Sipgate API Token ID + - `TOKEN` is your Sipgate API TOKEN + - `SENDER_ID` is your Sipgate device ID (e.g. s1) + +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) \ No newline at end of file diff --git a/src/Symfony/Component/Notifier/Bridge/Sipgate/SipgateTransport.php b/src/Symfony/Component/Notifier/Bridge/Sipgate/SipgateTransport.php new file mode 100644 index 0000000000000..ea619bf003eaa --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sipgate/SipgateTransport.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\Sipgate; + +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\TransportExceptionInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @author Lukas Kaltenbach + */ +final class SipgateTransport extends AbstractTransport +{ + protected const HOST = 'api.sipgate.com'; + + public function __construct( + private string $tokenId, + #[\SensitiveParameter] private string $token, + private ?string $senderId = null, + ?HttpClientInterface $client = null, + ?EventDispatcherInterface $dispatcher = null, + ) { + parent::__construct($client, $dispatcher); + } + + public function __toString(): string + { + return sprintf('sipgate://%s?senderId=%s', $this->getEndpoint(), $this->senderId); + } + + 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/v2/sessions/sms', $this->getEndpoint()); + + $options = []; + $options['smsId'] = $this->senderId; + $options['message'] = $message->getSubject(); + $options['recipient'] = $message->getPhone(); + + $response = $this->client->request('POST', $endpoint, [ + 'headers' => [ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ], + 'auth_basic' => [$this->tokenId, $this->token], + 'body' => json_encode($options), + ]); + + try { + $statusCode = $response->getStatusCode(); + } catch (TransportExceptionInterface $e) { + throw new TransportException('Could not reach the remote Sipgate server.', $response, 0, $e); + } + + if (204 === $statusCode) { + $sentMessage = new SentMessage($message, (string) $this); + + return $sentMessage; + } elseif (401 === $statusCode) { + throw new TransportException(sprintf('Unable to send SMS with Sipgate: Error code %d - tokenId or token is wrong.', $statusCode), $response); + } elseif (402 === $statusCode) { + throw new TransportException(sprintf('Unable to send SMS with Sipgate: Error code %d - insufficient funds.', $statusCode), $response); + } elseif (403 === $statusCode) { + throw new TransportException(sprintf('Unable to send SMS with Sipgate: Error code %d - no permisssion to use sms feature or password must be reset or senderId is wrong.', $statusCode), $response); + } + throw new TransportException(sprintf('Unable to send SMS with Sipgate: Error code %d.', $statusCode), $response); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Sipgate/SipgateTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/Sipgate/SipgateTransportFactory.php new file mode 100644 index 0000000000000..2304d43e59aae --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sipgate/SipgateTransportFactory.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Sipgate; + +use Symfony\Component\Notifier\Exception\UnsupportedSchemeException; +use Symfony\Component\Notifier\Transport\AbstractTransportFactory; +use Symfony\Component\Notifier\Transport\Dsn; + +/** + * @author Lukas Kaltenbach + */ +final class SipgateTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): SipgateTransport + { + if ('sipgate' !== $dsn->getScheme()) { + throw new UnsupportedSchemeException($dsn, 'sipgate', $this->getSupportedSchemes()); + } + + $tokenId = $this->getUser($dsn); + $token = $this->getPassword($dsn); + $senderId = $dsn->getRequiredOption('senderId'); + $host = 'default' === $dsn->getHost() ? null : $dsn->getHost(); + $port = $dsn->getPort(); + + return (new SipgateTransport($tokenId, $token, $senderId, $this->client, $this->dispatcher))->setHost($host)->setPort($port); + } + + protected function getSupportedSchemes(): array + { + return ['sipgate']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Sipgate/Tests/SipgateTransportFactoryTest.php b/src/Symfony/Component/Notifier/Bridge/Sipgate/Tests/SipgateTransportFactoryTest.php new file mode 100644 index 0000000000000..5656e6f5d78fe --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sipgate/Tests/SipgateTransportFactoryTest.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Sipgate\Tests; + +use Symfony\Component\Notifier\Bridge\Sipgate\SipgateTransportFactory; +use Symfony\Component\Notifier\Test\TransportFactoryTestCase; + +class SipgateTransportFactoryTest extends TransportFactoryTestCase +{ + public function createFactory(): SipgateTransportFactory + { + return new SipgateTransportFactory(); + } + + public static function createProvider(): iterable + { + yield [ + 'sipgate://host.test?senderId=s1', + 'sipgate://tokenId:token@host.test?senderId=s1', + ]; + } + + public static function supportsProvider(): iterable + { + yield [true, 'sipgate://tokenId:token@host.test?senderId=s1']; + yield [false, 'somethingElse://tokenId:token@host.test?senderId=s1']; + } + + public static function unsupportedSchemeProvider(): iterable + { + yield ['somethingElse://tokenId:token@host.test?senderId=s1']; + yield ['somethingElse://tokenId:token@host.test']; // missing senderId + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Sipgate/Tests/SipgateTransportTest.php b/src/Symfony/Component/Notifier/Bridge/Sipgate/Tests/SipgateTransportTest.php new file mode 100644 index 0000000000000..b21cd0e25eb02 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sipgate/Tests/SipgateTransportTest.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Sipgate\Tests; + +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\MockResponse; +use Symfony\Component\Notifier\Bridge\Sipgate\SipgateTransport; +use Symfony\Component\Notifier\Exception\TransportException; +use Symfony\Component\Notifier\Message\ChatMessage; +use Symfony\Component\Notifier\Message\SentMessage; +use Symfony\Component\Notifier\Message\SmsMessage; +use Symfony\Component\Notifier\Test\TransportTestCase; +use Symfony\Component\Notifier\Tests\Transport\DummyMessage; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; + +class SipgateTransportTest extends TransportTestCase +{ + public static function createTransport(?HttpClientInterface $client = null): SipgateTransport + { + return new SipgateTransport('tokenid', 'token', 's1', $client ?? new MockHttpClient()); + } + + public static function toStringProvider(): iterable + { + yield ['sipgate://api.sipgate.com?senderId=s1', self::createTransport()]; + } + + public static function supportedMessagesProvider(): iterable + { + yield [new SmsMessage('+49123456789', 'Hallo!')]; + } + + public static function unsupportedMessagesProvider(): iterable + { + yield [new ChatMessage('Hallo!')]; + yield [new DummyMessage()]; + } + + public function testSendSuccessfully() + { + $response = new MockResponse('', ['http_code' => 204]); + + $client = new MockHttpClient($response); + $transport = $this->createTransport($client); + + $sentMessage = $transport->send(new SmsMessage('+49123456789', 'Hallo!')); + $this->assertInstanceOf(SentMessage::class, $sentMessage); + } + + /** + * @dataProvider errorProvider + */ + public function testExceptionIsThrownWhenSendFailed(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 = $this->createTransport($client); + + $this->expectException(TransportException::class); + $this->expectExceptionMessage($expectedExceptionMessage); + + $transport->send(new SmsMessage('+49123456789', 'Hallo!')); + } + + public static function errorProvider(): iterable + { + yield [ + 401, + '', + 'Unable to send SMS with Sipgate: Error code 401 - tokenId or token is wrong.', + ]; + yield [ + 402, + '', + 'Unable to send SMS with Sipgate: Error code 402 - insufficient funds.', + ]; + yield [ + 403, + '', + 'Unable to send SMS with Sipgate: Error code 403 - no permisssion to use sms feature or password must be reset or senderId is wrong.', + ]; + yield [ + 415, + '', + 'Unable to send SMS with Sipgate: Error code 415.', + ]; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Sipgate/composer.json b/src/Symfony/Component/Notifier/Bridge/Sipgate/composer.json new file mode 100644 index 0000000000000..bba4c1bb1b652 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sipgate/composer.json @@ -0,0 +1,32 @@ +{ + "name": "symfony/sipgate-notifier", + "type": "symfony-notifier-bridge", + "description": "Symfony Sipgate Notifier Bridge", + "keywords": ["sms", "sipgate", "notifier"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Lukas Kaltenbach", + "email": "lk@wikanet.de" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.2", + "symfony/http-client": "^6.4|^7.0", + "symfony/notifier": "^7.2" + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Notifier\\Bridge\\Sipgate\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/src/Symfony/Component/Notifier/Bridge/Sipgate/phpunit.xml.dist b/src/Symfony/Component/Notifier/Bridge/Sipgate/phpunit.xml.dist new file mode 100644 index 0000000000000..4a90d6aecfdc2 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sipgate/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 a9930d452a9e2..8d1908808ce39 100644 --- a/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php +++ b/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php @@ -228,6 +228,10 @@ class UnsupportedSchemeException extends LogicException 'class' => Bridge\Sevenio\SevenIoTransportFactory::class, 'package' => 'symfony/sevenio-notifier', ], + 'sipgate' => [ + 'class' => Bridge\Sipgate\SipgateTransportFactory::class, + 'package' => 'symfony/sipgate-notifier', + ], 'simpletextin' => [ 'class' => Bridge\SimpleTextin\SimpleTextinTransportFactory::class, 'package' => 'symfony/simple-textin-notifier', diff --git a/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php b/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php index 8b11e62c41d21..9d64426017189 100644 --- a/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php +++ b/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php @@ -79,6 +79,7 @@ public static function setUpBeforeClass(): void Bridge\RocketChat\RocketChatTransportFactory::class => false, Bridge\Sendberry\SendberryTransportFactory::class => false, Bridge\Sevenio\SevenIoTransportFactory::class => false, + Bridge\Sipgate\SipgateTransportFactory::class => false, Bridge\SimpleTextin\SimpleTextinTransportFactory::class => false, Bridge\Sinch\SinchTransportFactory::class => false, Bridge\Slack\SlackTransportFactory::class => false, @@ -164,6 +165,7 @@ public static function messageWhereSchemeIsPartOfSchemeToPackageMapProvider(): \ yield ['rocketchat', 'symfony/rocket-chat-notifier']; yield ['sendberry', 'symfony/sendberry-notifier']; yield ['sevenio', 'symfony/sevenio-notifier']; + yield ['sipgate', 'symfony/sipgate-notifier']; yield ['simpletextin', 'symfony/simple-textin-notifier']; yield ['sinch', 'symfony/sinch-notifier']; yield ['slack', 'symfony/slack-notifier']; diff --git a/src/Symfony/Component/Notifier/Transport.php b/src/Symfony/Component/Notifier/Transport.php index 0ed0c465747f6..7938ae6b5718e 100644 --- a/src/Symfony/Component/Notifier/Transport.php +++ b/src/Symfony/Component/Notifier/Transport.php @@ -81,6 +81,7 @@ final class Transport Bridge\RocketChat\RocketChatTransportFactory::class, Bridge\Sendberry\SendberryTransportFactory::class, Bridge\Sevenio\SevenIoTransportFactory::class, + Bridge\Sipgate\SipgateTransportFactory::class, Bridge\SimpleTextin\SimpleTextinTransportFactory::class, Bridge\Sinch\SinchTransportFactory::class, Bridge\Slack\SlackTransportFactory::class,