From 34e8a966c99507764961bfeef4da697bb777fc5e Mon Sep 17 00:00:00 2001 From: Mathieu Santostefano Date: Sun, 8 Sep 2024 13:40:02 +0200 Subject: [PATCH] Add Sweego Notifier bridge --- .../FrameworkExtension.php | 2 + .../Resources/config/notifier_transports.php | 1 + .../Resources/config/notifier_webhook.php | 4 + .../Notifier/Bridge/Sweego/.gitattributes | 3 + .../Notifier/Bridge/Sweego/.gitignore | 3 + .../Notifier/Bridge/Sweego/CHANGELOG.md | 7 + .../Component/Notifier/Bridge/Sweego/LICENSE | 19 +++ .../Notifier/Bridge/Sweego/README.md | 53 ++++++ .../Notifier/Bridge/Sweego/SweegoOptions.php | 67 ++++++++ .../Bridge/Sweego/SweegoTransport.php | 157 ++++++++++++++++++ .../Bridge/Sweego/SweegoTransportFactory.php | 48 ++++++ .../Tests/SweegoTransportFactoryTest.php | 51 ++++++ .../Sweego/Tests/SweegoTransportTest.php | 71 ++++++++ .../Sweego/Tests/Webhook/Fixtures/sent.json | 18 ++ .../Sweego/Tests/Webhook/Fixtures/sent.php | 8 + .../Tests/Webhook/SweegoRequestParserTest.php | 24 +++ .../Sweego/Webhook/SweegoRequestParser.php | 56 +++++++ .../Notifier/Bridge/Sweego/composer.json | 33 ++++ .../Notifier/Bridge/Sweego/phpunit.xml.dist | 31 ++++ .../Exception/UnsupportedSchemeException.php | 4 + .../UnsupportedSchemeExceptionTest.php | 1 + src/Symfony/Component/Notifier/Transport.php | 1 + 22 files changed, 662 insertions(+) create mode 100644 src/Symfony/Component/Notifier/Bridge/Sweego/.gitattributes create mode 100644 src/Symfony/Component/Notifier/Bridge/Sweego/.gitignore create mode 100644 src/Symfony/Component/Notifier/Bridge/Sweego/CHANGELOG.md create mode 100644 src/Symfony/Component/Notifier/Bridge/Sweego/LICENSE create mode 100644 src/Symfony/Component/Notifier/Bridge/Sweego/README.md create mode 100644 src/Symfony/Component/Notifier/Bridge/Sweego/SweegoOptions.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Sweego/SweegoTransport.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Sweego/SweegoTransportFactory.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Sweego/Tests/SweegoTransportFactoryTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Sweego/Tests/SweegoTransportTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Sweego/Tests/Webhook/Fixtures/sent.json create mode 100644 src/Symfony/Component/Notifier/Bridge/Sweego/Tests/Webhook/Fixtures/sent.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Sweego/Tests/Webhook/SweegoRequestParserTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Sweego/Webhook/SweegoRequestParser.php create mode 100644 src/Symfony/Component/Notifier/Bridge/Sweego/composer.json create mode 100644 src/Symfony/Component/Notifier/Bridge/Sweego/phpunit.xml.dist diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index c4ea185b84beb..a19c5a8636096 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -2881,6 +2881,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ NotifierBridge\SmsSluzba\SmsSluzbaTransportFactory::class => 'notifier.transport_factory.sms-sluzba', NotifierBridge\Smsense\SmsenseTransportFactory::class => 'notifier.transport_factory.smsense', NotifierBridge\SpotHit\SpotHitTransportFactory::class => 'notifier.transport_factory.spot-hit', + NotifierBridge\Sweego\SweegoTransportFactory::class => 'notifier.transport_factory.sweego', NotifierBridge\Telegram\TelegramTransportFactory::class => 'notifier.transport_factory.telegram', NotifierBridge\Telnyx\TelnyxTransportFactory::class => 'notifier.transport_factory.telnyx', NotifierBridge\Termii\TermiiTransportFactory::class => 'notifier.transport_factory.termii', @@ -2948,6 +2949,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ $loader->load('notifier_webhook.php'); $webhookRequestParsers = [ + NotifierBridge\Sweego\Webhook\SweegoRequestParser::class => 'notifier.webhook.request_parser.sweego', NotifierBridge\Twilio\Webhook\TwilioRequestParser::class => 'notifier.webhook.request_parser.twilio', NotifierBridge\Vonage\Webhook\VonageRequestParser::class => 'notifier.webhook.request_parser.vonage', ]; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php index 073eda08b3413..f28007decf81b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php @@ -108,6 +108,7 @@ 'smsense' => Bridge\Smsense\SmsenseTransportFactory::class, 'smsmode' => Bridge\Smsmode\SmsmodeTransportFactory::class, 'spot-hit' => Bridge\SpotHit\SpotHitTransportFactory::class, + 'sweego' => Bridge\Sweego\SweegoTransportFactory::class, 'telnyx' => Bridge\Telnyx\TelnyxTransportFactory::class, 'termii' => Bridge\Termii\TermiiTransportFactory::class, 'turbo-sms' => Bridge\TurboSms\TurboSmsTransportFactory::class, diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_webhook.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_webhook.php index fc541fd999ff5..6447f41394679 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_webhook.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_webhook.php @@ -11,11 +11,15 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; +use Symfony\Component\Notifier\Bridge\Sweego\Webhook\SweegoRequestParser; use Symfony\Component\Notifier\Bridge\Twilio\Webhook\TwilioRequestParser; use Symfony\Component\Notifier\Bridge\Vonage\Webhook\VonageRequestParser; return static function (ContainerConfigurator $container) { $container->services() + ->set('notifier.webhook.request_parser.sweego', SweegoRequestParser::class) + ->alias(SweegoRequestParser::class, 'notifier.webhook.request_parser.sweego') + ->set('notifier.webhook.request_parser.twilio', TwilioRequestParser::class) ->alias(TwilioRequestParser::class, 'notifier.webhook.request_parser.twilio') diff --git a/src/Symfony/Component/Notifier/Bridge/Sweego/.gitattributes b/src/Symfony/Component/Notifier/Bridge/Sweego/.gitattributes new file mode 100644 index 0000000000000..14c3c35940427 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sweego/.gitattributes @@ -0,0 +1,3 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.git* export-ignore diff --git a/src/Symfony/Component/Notifier/Bridge/Sweego/.gitignore b/src/Symfony/Component/Notifier/Bridge/Sweego/.gitignore new file mode 100644 index 0000000000000..c49a5d8df5c65 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sweego/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Notifier/Bridge/Sweego/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/Sweego/CHANGELOG.md new file mode 100644 index 0000000000000..00149ea5ac6f5 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sweego/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +7.2 +--- + + * Add the bridge diff --git a/src/Symfony/Component/Notifier/Bridge/Sweego/LICENSE b/src/Symfony/Component/Notifier/Bridge/Sweego/LICENSE new file mode 100644 index 0000000000000..e374a5c8339d3 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sweego/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/Sweego/README.md b/src/Symfony/Component/Notifier/Bridge/Sweego/README.md new file mode 100644 index 0000000000000..85fb83342d40b --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sweego/README.md @@ -0,0 +1,53 @@ +Sweego Notifier +=============== + +Provides [Sweego](https://www.sweego.io/) integration for Symfony Notifier. + +DSN example +----------- + +``` +SWEEGO_DSN=sweego://API_KEY@default?region=REGION&campaign_type=CAMPAIGN_TYPE&bat=BAT&campaign_id=CAMPAIGN_ID&shorten_urls=SHORTEN_URLS&shorten_with_protocol=SHORTEN_WITH_PROTOCOL +``` + +where: + - `API_KEY` (required) is your Sweego API key + - `REGION` (required) is the region of the phone number (e.g. `FR`, ISO 3166-1 alpha-2 country code) + - `CAMPAIGN_TYPE` (required) is the type of the campaign (e.g. `transac`) + - `BAT` (optional) is the test mode (e.g. `true`) + - `CAMPAIGN_ID` (optional) is the campaign id (e.g. `string`) + - `SHORTEN_URLS` (optional) is the shorten urls option (e.g. `true`) + - `SHORTEN_WITH_PROTOCOL` (optional) is the shorten with protocol option (e.g. `true`) + +Advanced Message options +------------------------ + +```php +use Symfony\Component\Notifier\Message\SmsMessage; +use Symfony\Component\Notifier\Bridge\Sweego\SweegoOptions; + +$sms = new SmsMessage('+1411111111', 'My message'); + +$options = (new SweegoOptions()) + // False by default, set 'bat' to true enable test mode (no sms sent, only for testing purpose) + ->bat(true) + // Optional, used for tracking / filtering purpose on our platform; identity an SMS campaign and allow to see logs / stats only for this campaign + ->campaignId('string') + // True by default, we replace all url in the SMS content by a shortened url version (reduce the characters of the sms) + ->shortenUrls(true) + // True by default, add scheme to shortened url version + ->shortenWithProtocol(true); + +// Add the custom options to the sms message and send the message +$sms->options($options); + +$texter->send($sms); +``` + +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/Sweego/SweegoOptions.php b/src/Symfony/Component/Notifier/Bridge/Sweego/SweegoOptions.php new file mode 100644 index 0000000000000..25f0d431d9e2c --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sweego/SweegoOptions.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Sweego; + +use Symfony\Component\Notifier\Message\MessageOptionsInterface; + +class SweegoOptions implements MessageOptionsInterface +{ + public const REGION = 'region'; + public const BAT = 'bat'; + public const CAMPAIGN_TYPE = 'campaign_type'; + public const CAMPAIGN_ID = 'campaign_id'; + public const SHORTEN_URLS = 'shorten_urls'; + public const SHORTEN_WITH_PROTOCOL = 'shorten_with_protocol'; + + public function __construct( + private array $options = [], + ) { + } + + public function toArray(): array + { + return $this->options; + } + + public function getRecipientId(): ?string + { + return null; + } + + public function bat(bool $bat): self + { + $this->options[self::BAT] = $bat; + + return $this; + } + + public function campaignId(string $campaignId): self + { + $this->options[self::CAMPAIGN_ID] = $campaignId; + + return $this; + } + + public function shortenUrls(bool $shortenUrls): self + { + $this->options[self::SHORTEN_URLS] = $shortenUrls; + + return $this; + } + + public function shortenWithProtocol(bool $shortenWithProtocol): self + { + $this->options[self::SHORTEN_WITH_PROTOCOL] = $shortenWithProtocol; + + return $this; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Sweego/SweegoTransport.php b/src/Symfony/Component/Notifier/Bridge/Sweego/SweegoTransport.php new file mode 100644 index 0000000000000..70bea13002e14 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sweego/SweegoTransport.php @@ -0,0 +1,157 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Sweego; + +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 Mathieu Santostefano + */ +final class SweegoTransport extends AbstractTransport +{ + protected const HOST = 'api.sweego.io'; + + public function __construct( + #[\SensitiveParameter] private readonly string $apiKey, + private readonly string $region, + private readonly string $campaignType, + private readonly ?bool $bat, + private readonly ?string $campaignId, + private readonly ?bool $shortenUrls, + private readonly ?bool $shortenWithProtocol, + ?HttpClientInterface $client = null, + ?EventDispatcherInterface $dispatcher = null, + ) { + parent::__construct($client, $dispatcher); + } + + public function __toString(): string + { + return \sprintf('sweego://%s%s', $this->getEndpoint(), '?'.http_build_query([ + SweegoOptions::REGION => $this->region, + SweegoOptions::CAMPAIGN_TYPE => $this->campaignType, + SweegoOptions::BAT => $this->bat, + SweegoOptions::CAMPAIGN_ID => $this->campaignId, + SweegoOptions::SHORTEN_URLS => $this->shortenUrls, + SweegoOptions::SHORTEN_WITH_PROTOCOL => $this->shortenWithProtocol, + ])); + } + + public function supports(MessageInterface $message): bool + { + return $message instanceof SmsMessage + && (null === $message->getOptions() || $message->getOptions() instanceof SweegoOptions); + } + + protected function doSend(MessageInterface $message): SentMessage + { + if (!$message instanceof SmsMessage) { + throw new UnsupportedMessageTypeException(__CLASS__, SmsMessage::class, $message); + } + + $options = $message->getOptions()?->toArray() ?? []; + + $body = [ + 'recipients' => [ + [ + 'num' => $message->getPhone(), + 'region' => $options[SweegoOptions::REGION] ?? $this->region, + ], + ], + 'message-txt' => $message->getSubject(), + 'channel' => 'sms', + 'provider' => 'sweego', + ]; + + $body = $this->setBat($body, $options); + $body = $this->setCampaignType($body, $options); + $body = $this->setCampaignId($body, $options); + $body = $this->setShortenUrls($body, $options); + $body = $this->setShortenWithProtocol($body, $options); + + $endpoint = \sprintf('https://%s/send', $this->getEndpoint()); + $response = $this->client->request('POST', $endpoint, [ + 'headers' => [ + 'Api-Key' => $this->apiKey, + ], + 'json' => array_filter($body), + ]); + + try { + $statusCode = $response->getStatusCode(); + } catch (TransportExceptionInterface $e) { + throw new TransportException('Could not reach the remote Sweego server.', $response, 0, $e); + } + + if (200 !== $statusCode) { + throw new TransportException('Unable to send the SMS.', $response); + } + + $success = $response->toArray(false); + + $sentMessage = new SentMessage($message, (string) $this); + $sentMessage->setMessageId(array_values($success['swg_uids'])[0]); + + return $sentMessage; + } + + private function setBat(array $body, array $options): array + { + $body['bat'] = (bool) ($options[SweegoOptions::BAT] ?? $this->bat); + + return $body; + } + + private function setCampaignType(array $body, array $options): array + { + $body['campaign-type'] = $this->campaignType; + + if (!\array_key_exists(SweegoOptions::CAMPAIGN_TYPE, $options) && \is_string($options[SweegoOptions::CAMPAIGN_TYPE])) { + $body['campaign-type'] = $options[SweegoOptions::CAMPAIGN_TYPE]; + } + + return $body; + } + + private function setCampaignId(array $body, array $options): array + { + $body['campaign-id'] = $this->campaignId; + + if (!\array_key_exists(SweegoOptions::CAMPAIGN_ID, $options) && \is_string($options[SweegoOptions::CAMPAIGN_ID])) { + $body['campaign-id'] = $options[SweegoOptions::CAMPAIGN_ID]; + } + + return $body; + } + + private function setShortenUrls(array $body, array $options): array + { + $body['shorten_urls'] = (bool) ($options[SweegoOptions::SHORTEN_URLS] ?? $this->shortenUrls); + + return $body; + } + + private function setShortenWithProtocol(array $body, array $options): array + { + $body['shorten_with_protocol'] = (bool) ($options[SweegoOptions::SHORTEN_WITH_PROTOCOL] ?? $this->shortenWithProtocol); + + return $body; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Sweego/SweegoTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/Sweego/SweegoTransportFactory.php new file mode 100644 index 0000000000000..f6fa077f0b289 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sweego/SweegoTransportFactory.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\Sweego; + +use Symfony\Component\Notifier\Exception\UnsupportedSchemeException; +use Symfony\Component\Notifier\Transport\AbstractTransportFactory; +use Symfony\Component\Notifier\Transport\Dsn; + +/** + * @author Mathieu Santostefano + */ +final class SweegoTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): SweegoTransport + { + $scheme = $dsn->getScheme(); + + if ('sweego' !== $scheme) { + throw new UnsupportedSchemeException($dsn, 'sweego', $this->getSupportedSchemes()); + } + + $apiKey = $this->getUser($dsn); + $region = $dsn->getRequiredOption(SweegoOptions::REGION); + $campaignType = $dsn->getRequiredOption(SweegoOptions::CAMPAIGN_TYPE); + $bat = $dsn->getOption(SweegoOptions::BAT); + $campaignId = $dsn->getOption(SweegoOptions::CAMPAIGN_ID); + $shortenUrls = $dsn->getOption(SweegoOptions::SHORTEN_URLS); + $shortenWithProtocol = $dsn->getOption(SweegoOptions::SHORTEN_WITH_PROTOCOL); + $host = 'default' === $dsn->getHost() ? null : $dsn->getHost(); + $port = $dsn->getPort(); + + return (new SweegoTransport($apiKey, $region, $campaignType, $bat, $campaignId, $shortenUrls, $shortenWithProtocol, $this->client, $this->dispatcher))->setHost($host)->setPort($port); + } + + protected function getSupportedSchemes(): array + { + return ['sweego']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Sweego/Tests/SweegoTransportFactoryTest.php b/src/Symfony/Component/Notifier/Bridge/Sweego/Tests/SweegoTransportFactoryTest.php new file mode 100644 index 0000000000000..83f10a0190025 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sweego/Tests/SweegoTransportFactoryTest.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\Sweego\Tests; + +use Symfony\Component\Notifier\Bridge\Sweego\SweegoTransportFactory; +use Symfony\Component\Notifier\Test\AbstractTransportFactoryTestCase; +use Symfony\Component\Notifier\Test\MissingRequiredOptionTestTrait; + +final class SweegoTransportFactoryTest extends AbstractTransportFactoryTestCase +{ + use MissingRequiredOptionTestTrait; + + public function createFactory(): SweegoTransportFactory + { + return new SweegoTransportFactory(); + } + + public static function createProvider(): iterable + { + yield [ + 'sweego://host.test?region=REGION&campaign_type=CAMPAIGN_TYPE', + 'sweego://apiKey@host.test?region=REGION&campaign_type=CAMPAIGN_TYPE', + ]; + } + + public static function missingRequiredOptionProvider(): iterable + { + yield 'missing option: region' => ['sweego://apiKey@default?campaign_type=CAMPAIGN_TYPE']; + yield 'missing option: campaign_type' => ['sweego://apiKey@default?region=REGION']; + } + + public static function supportsProvider(): iterable + { + yield [true, 'sweego://apiKey@default']; + yield [false, 'somethingElse://apiKey@default']; + } + + public static function unsupportedSchemeProvider(): iterable + { + yield ['somethingElse://apiKey@default']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Sweego/Tests/SweegoTransportTest.php b/src/Symfony/Component/Notifier/Bridge/Sweego/Tests/SweegoTransportTest.php new file mode 100644 index 0000000000000..bed8c22fb36ca --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sweego/Tests/SweegoTransportTest.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Sweego\Tests; + +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\Notifier\Bridge\Sweego\SweegoTransport; +use Symfony\Component\Notifier\Exception\UnsupportedMessageTypeException; +use Symfony\Component\Notifier\Message\ChatMessage; +use Symfony\Component\Notifier\Message\MessageInterface; +use Symfony\Component\Notifier\Message\MessageOptionsInterface; +use Symfony\Component\Notifier\Message\SmsMessage; +use Symfony\Component\Notifier\Test\TransportTestCase; +use Symfony\Component\Notifier\Tests\Transport\DummyMessage; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +final class SweegoTransportTest extends TransportTestCase +{ + public static function createTransport(?HttpClientInterface $client = null, string $from = 'from'): SweegoTransport + { + return new SweegoTransport('apiKey', 'REGION', 'CAMPAIGN_TYPE', false, 'CAMPAIGN_ID', true, false, $client ?? new MockHttpClient()); + } + + public static function toStringProvider(): iterable + { + yield ['sweego://api.sweego.io?region=REGION&campaign_type=CAMPAIGN_TYPE&bat=0&campaign_id=CAMPAIGN_ID&shorten_urls=1&shorten_with_protocol=0', self::createTransport()]; + } + + public static function supportedMessagesProvider(): iterable + { + yield [new SmsMessage('0611223344', 'Hello!')]; + } + + public static function unsupportedMessagesProvider(): iterable + { + yield [new ChatMessage('Hello!')]; + yield [new DummyMessage()]; + } + + public function testSupportWithNotSmsMessage() + { + $transport = new SweegoTransport('apiKey', 'REGION', 'CAMPAIGN_TYPE', false, 'CAMPAIGN_ID', true, false); + $message = $this->createMock(MessageInterface::class); + $this->assertFalse($transport->supports($message)); + } + + public function testSupportWithNotSweegoOptions() + { + $transport = new SweegoTransport('apiKey', 'REGION', 'CAMPAIGN_TYPE', false, 'CAMPAIGN_ID', true, false); + $message = new SmsMessage('test', 'test'); + $options = $this->createMock(MessageOptionsInterface::class); + $message->options($options); + $this->assertFalse($transport->supports($message)); + } + + public function testSendWithInvalidMessageType() + { + $this->expectException(UnsupportedMessageTypeException::class); + $transport = new SweegoTransport('apiKey', 'REGION', 'CAMPAIGN_TYPE', false, 'CAMPAIGN_ID', true, false); + $message = $this->createMock(MessageInterface::class); + $transport->send($message); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Sweego/Tests/Webhook/Fixtures/sent.json b/src/Symfony/Component/Notifier/Bridge/Sweego/Tests/Webhook/Fixtures/sent.json new file mode 100644 index 0000000000000..70a9d736b770a --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sweego/Tests/Webhook/Fixtures/sent.json @@ -0,0 +1,18 @@ +{ + "event_type": "sms_sent", + "timestamp": "2024-09-02T17:25:43", + "swg_uid": "03-f237cd16-a013-4e35-a279-c9eaa994e82b", + "event_id": "a5ccc627-6e43-4012-bb29-f1bfe3a3d13e", + "channel": "sms", + "client_id": "de1bbe6f-4103-47b1-8f69-94856aaba3fc", + "country_code": "FR", + "phone_number": "0033612345678", + "sender_id": "38082", + "sms_type": "transactional", + "sms_price": 0.04, + "campaign_id": null, + "test_mode": false, + "send_date": "2024-09-02T15:25:40.577165", + "mobile_network_code": 1, + "mobile_country_code": 208 +} diff --git a/src/Symfony/Component/Notifier/Bridge/Sweego/Tests/Webhook/Fixtures/sent.php b/src/Symfony/Component/Notifier/Bridge/Sweego/Tests/Webhook/Fixtures/sent.php new file mode 100644 index 0000000000000..581c177f35706 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sweego/Tests/Webhook/Fixtures/sent.php @@ -0,0 +1,8 @@ +setRecipientPhone('0033612345678'); + +return $wh; diff --git a/src/Symfony/Component/Notifier/Bridge/Sweego/Tests/Webhook/SweegoRequestParserTest.php b/src/Symfony/Component/Notifier/Bridge/Sweego/Tests/Webhook/SweegoRequestParserTest.php new file mode 100644 index 0000000000000..50d74d158246c --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sweego/Tests/Webhook/SweegoRequestParserTest.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Sweego\Tests\Webhook; + +use Symfony\Component\Notifier\Bridge\Sweego\Webhook\SweegoRequestParser; +use Symfony\Component\Webhook\Client\RequestParserInterface; +use Symfony\Component\Webhook\Test\AbstractRequestParserTestCase; + +class SweegoRequestParserTest extends AbstractRequestParserTestCase +{ + protected function createRequestParser(): RequestParserInterface + { + return new SweegoRequestParser(); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Sweego/Webhook/SweegoRequestParser.php b/src/Symfony/Component/Notifier/Bridge/Sweego/Webhook/SweegoRequestParser.php new file mode 100644 index 0000000000000..e35620e956d28 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sweego/Webhook/SweegoRequestParser.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Sweego\Webhook; + +use Symfony\Component\HttpFoundation\ChainRequestMatcher; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestMatcher\IsJsonRequestMatcher; +use Symfony\Component\HttpFoundation\RequestMatcher\MethodRequestMatcher; +use Symfony\Component\HttpFoundation\RequestMatcherInterface; +use Symfony\Component\RemoteEvent\Event\Sms\SmsEvent; +use Symfony\Component\Webhook\Client\AbstractRequestParser; +use Symfony\Component\Webhook\Exception\RejectWebhookException; + +/** + * @author Mathieu Santostefano + * + * @see https://learn.sweego.io/docs/webhooks/sms_events + */ +final class SweegoRequestParser extends AbstractRequestParser +{ + protected function getRequestMatcher(): RequestMatcherInterface + { + return new ChainRequestMatcher([ + new MethodRequestMatcher('POST'), + new IsJsonRequestMatcher(), + ]); + } + + protected function doParse(Request $request, #[\SensitiveParameter] string $secret): ?SmsEvent + { + $payload = $request->toArray(); + + if (!isset($payload['event_type']) || !isset($payload['swg_uid']) || !isset($payload['phone_number'])) { + throw new RejectWebhookException(406, 'Payload is malformed.'); + } + + $name = match ($payload['event_type']) { + 'sms_sent' => SmsEvent::DELIVERED, + default => throw new RejectWebhookException(406, \sprintf('Unsupported event "%s".', $payload['event'])), + }; + + $event = new SmsEvent($name, $payload['swg_uid'], $payload); + $event->setRecipientPhone($payload['phone_number']); + + return $event; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Sweego/composer.json b/src/Symfony/Component/Notifier/Bridge/Sweego/composer.json new file mode 100644 index 0000000000000..81cbdd8cd9897 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sweego/composer.json @@ -0,0 +1,33 @@ +{ + "name": "symfony/sweego-notifier", + "type": "symfony-notifier-bridge", + "description": "Symfony Sweego Notifier Bridge", + "keywords": ["sms", "sweego", "notifier"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.2", + "symfony/http-client": "^6.4|^7.0", + "symfony/notifier": "^7.2" + }, + "require-dev": { + "symfony/webhook": "^6.4|^7.0" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Sweego\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/src/Symfony/Component/Notifier/Bridge/Sweego/phpunit.xml.dist b/src/Symfony/Component/Notifier/Bridge/Sweego/phpunit.xml.dist new file mode 100644 index 0000000000000..bc57d5334c6e1 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sweego/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 37ac9fdcec834..95c7a04fed30a 100644 --- a/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php +++ b/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php @@ -296,6 +296,10 @@ class UnsupportedSchemeException extends LogicException 'class' => Bridge\SpotHit\SpotHitTransportFactory::class, 'package' => 'symfony/spot-hit-notifier', ], + 'sweego' => [ + 'class' => Bridge\Sweego\SweegoTransportFactory::class, + 'package' => 'symfony/sweego-notifier', + ], 'telegram' => [ 'class' => Bridge\Telegram\TelegramTransportFactory::class, 'package' => 'symfony/telegram-notifier', diff --git a/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php b/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php index d3d3676655602..f772fbe04227b 100644 --- a/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php +++ b/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php @@ -94,6 +94,7 @@ public static function setUpBeforeClass(): void Bridge\Smsmode\SmsmodeTransportFactory::class => false, Bridge\SmsSluzba\SmsSluzbaTransportFactory::class => false, Bridge\SpotHit\SpotHitTransportFactory::class => false, + Bridge\Sweego\SweegoTransportFactory::class => false, Bridge\Telegram\TelegramTransportFactory::class => false, Bridge\Telnyx\TelnyxTransportFactory::class => false, Bridge\Termii\TermiiTransportFactory::class => false, diff --git a/src/Symfony/Component/Notifier/Transport.php b/src/Symfony/Component/Notifier/Transport.php index b4df0729f40d0..a023ed6654443 100644 --- a/src/Symfony/Component/Notifier/Transport.php +++ b/src/Symfony/Component/Notifier/Transport.php @@ -96,6 +96,7 @@ final class Transport Bridge\Smsmode\SmsmodeTransportFactory::class, Bridge\SmsSluzba\SmsSluzbaTransportFactory::class, Bridge\SpotHit\SpotHitTransportFactory::class, + Bridge\Sweego\SweegoTransportFactory::class, Bridge\Telegram\TelegramTransportFactory::class, Bridge\Telnyx\TelnyxTransportFactory::class, Bridge\Termii\TermiiTransportFactory::class,