From e8638ab244778425cb93341ed6a02a31e406aada Mon Sep 17 00:00:00 2001 From: Tomas Date: Wed, 9 Dec 2020 14:07:49 +0200 Subject: [PATCH] [Notifier] Add push channel to notifier --- .../FrameworkExtension.php | 4 + .../Resources/config/notifier.php | 10 ++ .../Resources/config/notifier_transports.php | 5 + .../Bundle/FrameworkBundle/composer.json | 2 +- .../Notifier/Bridge/OneSignal/.gitattributes | 4 + .../Notifier/Bridge/OneSignal/.gitignore | 3 + .../Notifier/Bridge/OneSignal/CHANGELOG.md | 7 + .../Notifier/Bridge/OneSignal/LICENSE | 19 ++ .../Bridge/OneSignal/OneSignalOptions.php | 123 +++++++++++++ .../Bridge/OneSignal/OneSignalTransport.php | 122 +++++++++++++ .../OneSignal/OneSignalTransportFactory.php | 46 +++++ .../Notifier/Bridge/OneSignal/README.md | 25 +++ .../OneSignal/Tests/OneSignalOptionsTest.php | 38 ++++ .../Tests/OneSignalTransportFactoryTest.php | 55 ++++++ .../Tests/OneSignalTransportTest.php | 162 ++++++++++++++++++ .../Notifier/Bridge/OneSignal/composer.json | 33 ++++ .../Bridge/OneSignal/phpunit.xml.dist | 31 ++++ src/Symfony/Component/Notifier/CHANGELOG.md | 1 + .../Notifier/Channel/PushChannel.php | 50 ++++++ .../Exception/UnsupportedSchemeException.php | 4 + .../Notifier/Message/PushMessage.php | 99 +++++++++++ .../PushNotificationInterface.php | 20 +++ .../UnsupportedSchemeExceptionTest.php | 3 + .../Tests/Message/PushMessageTest.php | 66 +++++++ 24 files changed, 931 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Component/Notifier/Bridge/OneSignal/.gitattributes create mode 100644 src/Symfony/Component/Notifier/Bridge/OneSignal/.gitignore create mode 100644 src/Symfony/Component/Notifier/Bridge/OneSignal/CHANGELOG.md create mode 100644 src/Symfony/Component/Notifier/Bridge/OneSignal/LICENSE create mode 100644 src/Symfony/Component/Notifier/Bridge/OneSignal/OneSignalOptions.php create mode 100644 src/Symfony/Component/Notifier/Bridge/OneSignal/OneSignalTransport.php create mode 100644 src/Symfony/Component/Notifier/Bridge/OneSignal/OneSignalTransportFactory.php create mode 100644 src/Symfony/Component/Notifier/Bridge/OneSignal/README.md create mode 100644 src/Symfony/Component/Notifier/Bridge/OneSignal/Tests/OneSignalOptionsTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/OneSignal/Tests/OneSignalTransportFactoryTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/OneSignal/Tests/OneSignalTransportTest.php create mode 100644 src/Symfony/Component/Notifier/Bridge/OneSignal/composer.json create mode 100644 src/Symfony/Component/Notifier/Bridge/OneSignal/phpunit.xml.dist create mode 100644 src/Symfony/Component/Notifier/Channel/PushChannel.php create mode 100644 src/Symfony/Component/Notifier/Message/PushMessage.php create mode 100644 src/Symfony/Component/Notifier/Notification/PushNotificationInterface.php create mode 100644 src/Symfony/Component/Notifier/Tests/Message/PushMessageTest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index c71f53f71b37..e3a789dfb813 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -135,6 +135,7 @@ use Symfony\Component\Notifier\Bridge\Mobyt\MobytTransportFactory; use Symfony\Component\Notifier\Bridge\Nexmo\NexmoTransportFactory; use Symfony\Component\Notifier\Bridge\Octopush\OctopushTransportFactory; +use Symfony\Component\Notifier\Bridge\OneSignal\OneSignalTransportFactory; use Symfony\Component\Notifier\Bridge\OvhCloud\OvhCloudTransportFactory; use Symfony\Component\Notifier\Bridge\RocketChat\RocketChatTransportFactory; use Symfony\Component\Notifier\Bridge\Sendinblue\SendinblueTransportFactory as SendinblueNotifierTransportFactory; @@ -2429,6 +2430,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ $container->getDefinition('notifier.channel.email')->setArgument(0, null); } $container->getDefinition('notifier.channel.sms')->setArgument(0, null); + $container->getDefinition('notifier.channel.push')->setArgument(0, null); } $container->getDefinition('notifier.channel_policy')->setArgument(0, $config['channel_policy']); @@ -2465,6 +2467,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ MobytTransportFactory::class => 'notifier.transport_factory.mobyt', NexmoTransportFactory::class => 'notifier.transport_factory.nexmo', OctopushTransportFactory::class => 'notifier.transport_factory.octopush', + OneSignalTransportFactory::class => 'notifier.transport_factory.onesignal', OvhCloudTransportFactory::class => 'notifier.transport_factory.ovhcloud', RocketChatTransportFactory::class => 'notifier.transport_factory.rocketchat', SendinblueNotifierTransportFactory::class => 'notifier.transport_factory.sendinblue', @@ -2497,6 +2500,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ case 'messagebird': $package = 'message-bird'; break; case 'messagemedia': $package = 'message-media'; break; case 'microsoftteams': $package = 'microsoft-teams'; break; + case 'onesignal': $package = 'one-signal'; break; case 'ovhcloud': $package = 'ovh-cloud'; break; case 'rocketchat': $package = 'rocket-chat'; break; case 'smsbiuras': $package = 'sms-biuras'; break; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier.php index a9c447470b66..73beb2c34669 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier.php @@ -16,12 +16,14 @@ use Symfony\Component\Notifier\Channel\ChannelPolicy; use Symfony\Component\Notifier\Channel\ChatChannel; use Symfony\Component\Notifier\Channel\EmailChannel; +use Symfony\Component\Notifier\Channel\PushChannel; use Symfony\Component\Notifier\Channel\SmsChannel; use Symfony\Component\Notifier\Chatter; use Symfony\Component\Notifier\ChatterInterface; use Symfony\Component\Notifier\EventListener\NotificationLoggerListener; use Symfony\Component\Notifier\EventListener\SendFailedMessageToNotifierListener; use Symfony\Component\Notifier\Message\ChatMessage; +use Symfony\Component\Notifier\Message\PushMessage; use Symfony\Component\Notifier\Message\SmsMessage; use Symfony\Component\Notifier\Messenger\MessageHandler; use Symfony\Component\Notifier\Notifier; @@ -57,6 +59,10 @@ ->args([service('mailer.transports'), service('messenger.default_bus')->ignoreOnInvalid()]) ->tag('notifier.channel', ['channel' => 'email']) + ->set('notifier.channel.push', PushChannel::class) + ->args([service('texter.transports'), service('messenger.default_bus')->ignoreOnInvalid()]) + ->tag('notifier.channel', ['channel' => 'push']) + ->set('notifier.monolog_handler', NotifierHandler::class) ->args([service('notifier')]) @@ -103,6 +109,10 @@ ->args([service('texter.transports')]) ->tag('messenger.message_handler', ['handles' => SmsMessage::class]) + ->set('texter.messenger.push_handler', MessageHandler::class) + ->args([service('texter.transports')]) + ->tag('messenger.message_handler', ['handles' => PushMessage::class]) + ->set('notifier.logger_notification_listener', NotificationLoggerListener::class) ->tag('kernel.event_subscriber') ; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php index 220b25c1fe9e..5aafabfbff21 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php @@ -36,6 +36,7 @@ use Symfony\Component\Notifier\Bridge\Mobyt\MobytTransportFactory; use Symfony\Component\Notifier\Bridge\Nexmo\NexmoTransportFactory; use Symfony\Component\Notifier\Bridge\Octopush\OctopushTransportFactory; +use Symfony\Component\Notifier\Bridge\OneSignal\OneSignalTransportFactory; use Symfony\Component\Notifier\Bridge\OvhCloud\OvhCloudTransportFactory; use Symfony\Component\Notifier\Bridge\RocketChat\RocketChatTransportFactory; use Symfony\Component\Notifier\Bridge\Sendinblue\SendinblueTransportFactory; @@ -230,5 +231,9 @@ ->set('notifier.transport_factory.sms77', Sms77TransportFactory::class) ->parent('notifier.transport_factory.abstract') ->tag('texter.transport_factory') + + ->set('notifier.transport_factory.onesignal', OneSignalTransportFactory::class) + ->parent('notifier.transport_factory.abstract') + ->tag('texter.transport_factory') ; }; diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index b28f7e617929..c797289672f9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -50,7 +50,7 @@ "symfony/mailer": "^5.2|^6.0", "symfony/messenger": "^5.4|^6.0", "symfony/mime": "^4.4|^5.0|^6.0", - "symfony/notifier": "^5.3|^6.0", + "symfony/notifier": "^5.4|^6.0", "symfony/process": "^4.4|^5.0|^6.0", "symfony/rate-limiter": "^5.2|^6.0", "symfony/security-bundle": "^5.4|^6.0", diff --git a/src/Symfony/Component/Notifier/Bridge/OneSignal/.gitattributes b/src/Symfony/Component/Notifier/Bridge/OneSignal/.gitattributes new file mode 100644 index 000000000000..84c7add058fb --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/OneSignal/.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/OneSignal/.gitignore b/src/Symfony/Component/Notifier/Bridge/OneSignal/.gitignore new file mode 100644 index 000000000000..c49a5d8df5c6 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/OneSignal/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Notifier/Bridge/OneSignal/CHANGELOG.md b/src/Symfony/Component/Notifier/Bridge/OneSignal/CHANGELOG.md new file mode 100644 index 000000000000..3a08c7ededfc --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/OneSignal/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +5.4 +--- + + * Add the bridge diff --git a/src/Symfony/Component/Notifier/Bridge/OneSignal/LICENSE b/src/Symfony/Component/Notifier/Bridge/OneSignal/LICENSE new file mode 100644 index 000000000000..efb17f98e7dd --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/OneSignal/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/OneSignal/OneSignalOptions.php b/src/Symfony/Component/Notifier/Bridge/OneSignal/OneSignalOptions.php new file mode 100644 index 000000000000..b0ee07aef245 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/OneSignal/OneSignalOptions.php @@ -0,0 +1,123 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\OneSignal; + +use Symfony\Component\Notifier\Message\MessageOptionsInterface; +use Symfony\Component\Notifier\Notification\Notification; + +/** + * @author Tomas Norkūnas + */ +final class OneSignalOptions implements MessageOptionsInterface +{ + private $options; + + public function __construct(array $options = []) + { + $this->options = $options; + } + + /** + * @return $this + */ + public static function fromNotification(Notification $notification): self + { + $options = new self(); + $options->headings(['en' => $notification->getSubject()]); + $options->contents(['en' => $notification->getContent()]); + + return $options; + } + + /** + * @return $this + */ + public function headings(array $headings): self + { + $this->options['headings'] = $headings; + + return $this; + } + + /** + * @return $this + */ + public function contents(array $contents): self + { + $this->options['contents'] = $contents; + + return $this; + } + + /** + * @return $this + */ + public function url(string $url): self + { + $this->options['url'] = $url; + + return $this; + } + + /** + * @return $this + */ + public function data(array $data): self + { + $this->options['data'] = $data; + + return $this; + } + + /** + * @return $this + */ + public function sendAfter(\DateTimeInterface $datetime): self + { + $this->options['send_after'] = $datetime->format('Y-m-d H:i:sO'); + + return $this; + } + + /** + * @return $this + */ + public function externalId(string $externalId): self + { + $this->options['external_id'] = $externalId; + + return $this; + } + + /** + * @return $this + */ + public function recipient(string $id): self + { + $this->options['recipient_id'] = $id; + + return $this; + } + + public function getRecipientId(): ?string + { + return $this->options['recipient_id'] ?? null; + } + + public function toArray(): array + { + $options = $this->options; + unset($options['recipient_id']); + + return $options; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/OneSignal/OneSignalTransport.php b/src/Symfony/Component/Notifier/Bridge/OneSignal/OneSignalTransport.php new file mode 100644 index 000000000000..c9502dce1f08 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/OneSignal/OneSignalTransport.php @@ -0,0 +1,122 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\OneSignal; + +use Symfony\Component\Notifier\Exception\LogicException; +use Symfony\Component\Notifier\Exception\TransportException; +use Symfony\Component\Notifier\Exception\UnsupportedMessageTypeException; +use Symfony\Component\Notifier\Message\MessageInterface; +use Symfony\Component\Notifier\Message\PushMessage; +use Symfony\Component\Notifier\Message\SentMessage; +use Symfony\Component\Notifier\Transport\AbstractTransport; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @author Tomas Norkūnas + */ +final class OneSignalTransport extends AbstractTransport +{ + protected const HOST = 'onesignal.com'; + + private $appId; + private $apiKey; + private $defaultRecipientId; + + public function __construct(string $appId, string $apiKey, string $defaultRecipientId = null, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null) + { + $this->appId = $appId; + $this->apiKey = $apiKey; + $this->defaultRecipientId = $defaultRecipientId; + + parent::__construct($client, $dispatcher); + } + + public function __toString(): string + { + if (null === $this->defaultRecipientId) { + return sprintf('onesignal://%s@%s', urlencode($this->appId), $this->getEndpoint()); + } + + return sprintf('onesignal://%s@%s?recipientId=%s', urlencode($this->appId), $this->getEndpoint(), $this->defaultRecipientId); + } + + public function supports(MessageInterface $message): bool + { + return $message instanceof PushMessage && (null !== $this->defaultRecipientId || ($message->getOptions() instanceof OneSignalOptions && null !== $message->getOptions()->getRecipientId())); + } + + /** + * @see https://documentation.onesignal.com/reference/create-notification + */ + protected function doSend(MessageInterface $message): SentMessage + { + if (!$message instanceof PushMessage) { + throw new UnsupportedMessageTypeException(__CLASS__, PushMessage::class, $message); + } + + if ($message->getOptions() && !$message->getOptions() instanceof OneSignalOptions) { + throw new LogicException(sprintf('The "%s" transport only supports instances of "%s" for options.', __CLASS__, OneSignalOptions::class)); + } + + if (!($opts = $message->getOptions()) && $notification = $message->getNotification()) { + $opts = OneSignalOptions::fromNotification($notification); + } + + $recipientId = $message->getRecipientId() ?? $this->defaultRecipientId; + + if (null === $recipientId) { + throw new LogicException(sprintf('The "%s" transport should have configured `defaultRecipientId` via DSN or provided with message options.', __CLASS__)); + } + + $options = $opts ? $opts->toArray() : []; + $options['app_id'] = $this->appId; + $options['include_player_ids'] = [$recipientId]; + + if (!isset($options['headings'])) { + $options['headings'] = ['en' => $message->getSubject()]; + } + if (!isset($options['contents'])) { + $options['contents'] = ['en' => $message->getContent()]; + } + + $response = $this->client->request('POST', 'https://'.$this->getEndpoint().'/api/v1/notifications', [ + 'headers' => [ + 'Accept' => 'application/json', + 'Authorization' => 'Basic '.$this->apiKey, + ], + 'json' => $options, + ]); + + try { + $statusCode = $response->getStatusCode(); + } catch (TransportExceptionInterface $e) { + throw new TransportException('Could not reach the remote OneSignal server.', $response, 0, $e); + } + + if (200 !== $statusCode) { + throw new TransportException(sprintf('Unable to send the OneSignal push notification: "%s".', $response->getContent(false)), $response); + } + + $result = $response->toArray(false); + + if (empty($result['id'])) { + throw new TransportException(sprintf('Unable to send the OneSignal push notification: "%s".', $response->getContent(false)), $response); + } + + $sentMessage = new SentMessage($message, (string) $this); + $sentMessage->setMessageId($result['id']); + + return $sentMessage; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/OneSignal/OneSignalTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/OneSignal/OneSignalTransportFactory.php new file mode 100644 index 000000000000..a6d7f9884bf6 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/OneSignal/OneSignalTransportFactory.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\OneSignal; + +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 Tomas Norkūnas + */ +final class OneSignalTransportFactory extends AbstractTransportFactory +{ + /** + * @return OneSignalTransport + */ + public function create(Dsn $dsn): TransportInterface + { + if ('onesignal' !== $dsn->getScheme()) { + throw new UnsupportedSchemeException($dsn, 'onesignal', $this->getSupportedSchemes()); + } + + $appId = $this->getUser($dsn); + $apiKey = $this->getPassword($dsn); + $defaultRecipientId = $dsn->getOption('defaultRecipientId'); + $host = 'default' === $dsn->getHost() ? null : $dsn->getHost(); + $port = $dsn->getPort(); + + return (new OneSignalTransport($appId, $apiKey, $defaultRecipientId, $this->client, $this->dispatcher))->setHost($host)->setPort($port); + } + + protected function getSupportedSchemes(): array + { + return ['onesignal']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/OneSignal/README.md b/src/Symfony/Component/Notifier/Bridge/OneSignal/README.md new file mode 100644 index 000000000000..918a71331d6d --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/OneSignal/README.md @@ -0,0 +1,25 @@ +OneSignal Notifier +================== + +Provides [OneSignal](https://documentation.onesignal.com/reference/create-notification) integration for Symfony Notifier. + +DSN example +----------- + +``` +ONESIGNAL_DSN=onesignal://APP_ID:API_KEY@default?defaultRecipientId=DEFAULT_RECIPIENT_ID +``` + +where: + - `APP_ID` is your OneSignal application id + - `API_KEY` is your OneSignal application auth key + - `DEFAULT_RECIPIENT_ID` is an optional default recipient + + +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/OneSignal/Tests/OneSignalOptionsTest.php b/src/Symfony/Component/Notifier/Bridge/OneSignal/Tests/OneSignalOptionsTest.php new file mode 100644 index 000000000000..8cf3ac093d9c --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/OneSignal/Tests/OneSignalOptionsTest.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\OneSignal\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Notifier\Bridge\OneSignal\OneSignalOptions; + +final class OneSignalOptionsTest extends TestCase +{ + public function testOneSignalOptions() + { + $oneSignalOptions = (new OneSignalOptions()) + ->headings(['en' => 'English Heading', 'fr' => 'French Heading']) + ->contents(['en' => 'English Content', 'fr' => 'French Content']) + ->url('https://example.com') + ->data(['foo' => 'bar']) + ->sendAfter(new \DateTimeImmutable('Thu Sep 24 2015 14:00:00 GMT-0700 (PDT)')) + ->externalId('d637f30d-f709-4bed-9e2c-63637cb91894'); + + $this->assertSame([ + 'headings' => ['en' => 'English Heading', 'fr' => 'French Heading'], + 'contents' => ['en' => 'English Content', 'fr' => 'French Content'], + 'url' => 'https://example.com', + 'data' => ['foo' => 'bar'], + 'send_after' => '2015-09-24 14:00:00-0700', + 'external_id' => 'd637f30d-f709-4bed-9e2c-63637cb91894', + ], $oneSignalOptions->toArray()); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/OneSignal/Tests/OneSignalTransportFactoryTest.php b/src/Symfony/Component/Notifier/Bridge/OneSignal/Tests/OneSignalTransportFactoryTest.php new file mode 100644 index 000000000000..adb66e13c51e --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/OneSignal/Tests/OneSignalTransportFactoryTest.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\OneSignal\Tests; + +use Symfony\Component\Notifier\Bridge\OneSignal\OneSignalTransportFactory; +use Symfony\Component\Notifier\Test\TransportFactoryTestCase; +use Symfony\Component\Notifier\Transport\TransportFactoryInterface; + +/** + * @author Tomas Norkūnas + */ +final class OneSignalTransportFactoryTest extends TransportFactoryTestCase +{ + /** + * @return OneSignalTransportFactory + */ + public function createFactory(): TransportFactoryInterface + { + return new OneSignalTransportFactory(); + } + + public function createProvider(): iterable + { + yield [ + 'onesignal://app_id@host.test', + 'onesignal://app_id:api_key@host.test', + ]; + } + + public function supportsProvider(): iterable + { + yield [true, 'onesignal://token@host']; + yield [false, 'somethingElse://token@host']; + } + + public function incompleteDsnProvider(): iterable + { + yield 'missing app_id' => ['onesignal://:api_key@host.test']; + yield 'missing api_key' => ['onesignal://app_id:@host.test']; + } + + public function unsupportedSchemeProvider(): iterable + { + yield ['somethingElse://token@host']; + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/OneSignal/Tests/OneSignalTransportTest.php b/src/Symfony/Component/Notifier/Bridge/OneSignal/Tests/OneSignalTransportTest.php new file mode 100644 index 000000000000..e942fe0dca78 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/OneSignal/Tests/OneSignalTransportTest.php @@ -0,0 +1,162 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\OneSignal\Tests; + +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\Notifier\Bridge\OneSignal\OneSignalOptions; +use Symfony\Component\Notifier\Bridge\OneSignal\OneSignalTransport; +use Symfony\Component\Notifier\Exception\LogicException; +use Symfony\Component\Notifier\Exception\TransportException; +use Symfony\Component\Notifier\Message\ChatMessage; +use Symfony\Component\Notifier\Message\MessageInterface; +use Symfony\Component\Notifier\Message\PushMessage; +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; + +/** + * @author Tomas Norkūnas + */ +final class OneSignalTransportTest extends TransportTestCase +{ + /** + * @return OneSignalTransport + */ + public function createTransport(HttpClientInterface $client = null, string $recipientId = null): TransportInterface + { + return new OneSignalTransport('9fb175f0-0b32-4e99-ae97-bd228b9eb246', 'api_key', $recipientId, $client ?? $this->createMock(HttpClientInterface::class)); + } + + public function testCanSetCustomHost() + { + $transport = $this->createTransport(); + + $transport->setHost($customHost = self::CUSTOM_HOST); + + $this->assertSame(sprintf('onesignal://9fb175f0-0b32-4e99-ae97-bd228b9eb246@%s', $customHost), (string) $transport); + } + + public function testCanSetCustomHostAndPort() + { + $transport = $this->createTransport(); + + $transport->setHost($customHost = self::CUSTOM_HOST); + $transport->setPort($customPort = self::CUSTOM_PORT); + + $this->assertSame(sprintf('onesignal://9fb175f0-0b32-4e99-ae97-bd228b9eb246@%s:%d', $customHost, $customPort), (string) $transport); + } + + public function toStringProvider(): iterable + { + yield ['onesignal://9fb175f0-0b32-4e99-ae97-bd228b9eb246@onesignal.com', $this->createTransport()]; + yield ['onesignal://9fb175f0-0b32-4e99-ae97-bd228b9eb246@onesignal.com?recipientId=ea345989-d273-4f21-a33b-0c006efc5edb', $this->createTransport(null, 'ea345989-d273-4f21-a33b-0c006efc5edb')]; + } + + public function supportedMessagesProvider(): iterable + { + yield [new PushMessage('Hello', 'World'), $this->createTransport(null, 'ea345989-d273-4f21-a33b-0c006efc5edb')]; + yield [new PushMessage('Hello', 'World', (new OneSignalOptions())->recipient('ea345989-d273-4f21-a33b-0c006efc5edb'))]; + } + + public function unsupportedMessagesProvider(): iterable + { + yield [new SmsMessage('0611223344', 'Hello!')]; + yield [new ChatMessage('Hello!')]; + yield [$this->createMock(MessageInterface::class)]; + } + + public function testUnsupportedWithoutRecipientId() + { + $this->assertFalse($this->createTransport()->supports(new PushMessage('Hello', 'World'))); + } + + public function testSendThrowsWithoutRecipient() + { + $transport = $this->createTransport(); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('The "Symfony\Component\Notifier\Bridge\OneSignal\OneSignalTransport" transport should have configured `defaultRecipientId` via DSN or provided with message options.'); + + $transport->send(new PushMessage('Hello', 'World')); + } + + public function testSendWithErrorResponseThrows() + { + $response = $this->createMock(ResponseInterface::class); + $response->expects($this->exactly(2)) + ->method('getStatusCode') + ->willReturn(400); + $response->expects($this->once()) + ->method('getContent') + ->willReturn(json_encode(['errors' => ['Message Notifications must have English language content']])); + + $client = new MockHttpClient(static function () use ($response): ResponseInterface { + return $response; + }); + + $transport = $this->createTransport($client, 'ea345989-d273-4f21-a33b-0c006efc5edb'); + + $this->expectException(TransportException::class); + $this->expectExceptionMessageMatches('/Message Notifications must have English language content/'); + + $transport->send(new PushMessage('Hello', 'World')); + } + + public function testSendWithErrorResponseThrowsWhenAllUnsubscribed() + { + $response = $this->createMock(ResponseInterface::class); + $response->expects($this->exactly(2)) + ->method('getStatusCode') + ->willReturn(200); + $response->expects($this->once()) + ->method('getContent') + ->willReturn(json_encode(['id' => '', 'recipients' => 0, 'errors' => ['All included players are not subscribed']])); + + $client = new MockHttpClient(static function () use ($response): ResponseInterface { + return $response; + }); + + $transport = $this->createTransport($client, 'ea345989-d273-4f21-a33b-0c006efc5edb'); + + $this->expectException(TransportException::class); + $this->expectExceptionMessageMatches('/All included players are not subscribed/'); + + $transport->send(new PushMessage('Hello', 'World')); + } + + public function testSend() + { + $response = $this->createMock(ResponseInterface::class); + $response->expects($this->exactly(2)) + ->method('getStatusCode') + ->willReturn(200); + $response->expects($this->once()) + ->method('getContent') + ->willReturn(json_encode(['id' => 'b98881cc-1e94-4366-bbd9-db8f3429292b', 'recipients' => 1, 'external_id' => null])); + + $expectedBody = json_encode(['app_id' => '9fb175f0-0b32-4e99-ae97-bd228b9eb246', 'headings' => ['en' => 'Hello'], 'contents' => ['en' => 'World'], 'include_player_ids' => ['ea345989-d273-4f21-a33b-0c006efc5edb']]); + + $client = new MockHttpClient(function (string $method, string $url, array $options = []) use ($response, $expectedBody): ResponseInterface { + $this->assertJsonStringEqualsJsonString($expectedBody, $options['body']); + + return $response; + }); + + $transport = $this->createTransport($client, 'ea345989-d273-4f21-a33b-0c006efc5edb'); + + $sentMessage = $transport->send(new PushMessage('Hello', 'World')); + + $this->assertSame('b98881cc-1e94-4366-bbd9-db8f3429292b', $sentMessage->getMessageId()); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/OneSignal/composer.json b/src/Symfony/Component/Notifier/Bridge/OneSignal/composer.json new file mode 100644 index 000000000000..eb48bce7d85a --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/OneSignal/composer.json @@ -0,0 +1,33 @@ +{ + "name": "symfony/one-signal-notifier", + "type": "symfony-bridge", + "description": "Symfony OneSignal Notifier Bridge", + "keywords": ["onesignal", "notifier"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Tomas Norkūnas", + "email": "norkunas.tom@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2.5", + "symfony/http-client": "^4.4|^5.2", + "symfony/notifier": "^5.4" + }, + "require-dev": { + "symfony/event-dispatcher": "^4.3|^5.0" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\OneSignal\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/src/Symfony/Component/Notifier/Bridge/OneSignal/phpunit.xml.dist b/src/Symfony/Component/Notifier/Bridge/OneSignal/phpunit.xml.dist new file mode 100644 index 000000000000..d988e3ce8d5f --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/OneSignal/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Resources + ./Tests + ./vendor + + + + diff --git a/src/Symfony/Component/Notifier/CHANGELOG.md b/src/Symfony/Component/Notifier/CHANGELOG.md index 0ede307319b0..5e353ec5cc43 100644 --- a/src/Symfony/Component/Notifier/CHANGELOG.md +++ b/src/Symfony/Component/Notifier/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG --- * Add `SentMessageEvent` and `FailedMessageEvent` + * Add `push` channel 5.3 --- diff --git a/src/Symfony/Component/Notifier/Channel/PushChannel.php b/src/Symfony/Component/Notifier/Channel/PushChannel.php new file mode 100644 index 000000000000..e3b95af7859c --- /dev/null +++ b/src/Symfony/Component/Notifier/Channel/PushChannel.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Channel; + +use Symfony\Component\Notifier\Message\PushMessage; +use Symfony\Component\Notifier\Notification\Notification; +use Symfony\Component\Notifier\Notification\PushNotificationInterface; +use Symfony\Component\Notifier\Recipient\RecipientInterface; + +/** + * @author Tomas Norkūnas + */ +class PushChannel extends AbstractChannel +{ + public function notify(Notification $notification, RecipientInterface $recipient, string $transportName = null): void + { + $message = null; + if ($notification instanceof PushNotificationInterface) { + $message = $notification->asPushMessage($recipient, $transportName); + } + + if (null === $message) { + $message = PushMessage::fromNotification($notification); + } + + if (null !== $transportName) { + $message->transport($transportName); + } + + if (null === $this->bus) { + $this->transport->send($message); + } else { + $this->bus->dispatch($message); + } + } + + public function supports(Notification $notification, RecipientInterface $recipient): bool + { + return true; + } +} diff --git a/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php b/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php index f0c9eaa89c3a..f4307594e8ec 100644 --- a/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php +++ b/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php @@ -116,6 +116,10 @@ class UnsupportedSchemeException extends LogicException 'class' => Bridge\Octopush\OctopushTransportFactory::class, 'package' => 'symfony/octopush-notifier', ], + 'onesignal' => [ + 'class' => Bridge\OneSignal\OneSignalTransportFactory::class, + 'package' => 'symfony/one-signal-notifier', + ], 'ovhcloud' => [ 'class' => Bridge\OvhCloud\OvhCloudTransportFactory::class, 'package' => 'symfony/ovh-cloud-notifier', diff --git a/src/Symfony/Component/Notifier/Message/PushMessage.php b/src/Symfony/Component/Notifier/Message/PushMessage.php new file mode 100644 index 000000000000..de6588cae30e --- /dev/null +++ b/src/Symfony/Component/Notifier/Message/PushMessage.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Message; + +use Symfony\Component\Notifier\Notification\Notification; + +/** + * @author Tomas Norkūnas + */ +final class PushMessage implements MessageInterface +{ + private $transport; + private $subject; + private $content; + private $options; + private $notification; + + public function __construct(string $subject, string $content, MessageOptionsInterface $options = null) + { + $this->subject = $subject; + $this->content = $content; + $this->options = $options; + } + + public static function fromNotification(Notification $notification): self + { + $message = new self($notification->getSubject(), $notification->getContent()); + $message->notification = $notification; + + return $message; + } + + public function getRecipientId(): ?string + { + return $this->options ? $this->options->getRecipientId() : null; + } + + public function subject(string $subject): self + { + $this->subject = $subject; + + return $this; + } + + public function getSubject(): string + { + return $this->subject; + } + + public function content(string $content): self + { + $this->content = $content; + + return $this; + } + + public function getContent(): string + { + return $this->content; + } + + public function options(MessageOptionsInterface $options): self + { + $this->options = $options; + + return $this; + } + + public function getOptions(): ?MessageOptionsInterface + { + return $this->options; + } + + public function transport(?string $transport): self + { + $this->transport = $transport; + + return $this; + } + + public function getTransport(): ?string + { + return $this->transport; + } + + public function getNotification(): ?Notification + { + return $this->notification; + } +} diff --git a/src/Symfony/Component/Notifier/Notification/PushNotificationInterface.php b/src/Symfony/Component/Notifier/Notification/PushNotificationInterface.php new file mode 100644 index 000000000000..949dc4893bb7 --- /dev/null +++ b/src/Symfony/Component/Notifier/Notification/PushNotificationInterface.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Notification; + +use Symfony\Component\Notifier\Message\PushMessage; +use Symfony\Component\Notifier\Recipient\RecipientInterface; + +interface PushNotificationInterface +{ + public function asPushMessage(RecipientInterface $recipient, string $transport = null): ?PushMessage; +} diff --git a/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php b/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php index 7cabe954ab92..d630f34c0f1b 100644 --- a/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php +++ b/src/Symfony/Component/Notifier/Tests/Exception/UnsupportedSchemeExceptionTest.php @@ -38,6 +38,7 @@ use Symfony\Component\Notifier\Bridge\Mobyt\MobytTransportFactory; use Symfony\Component\Notifier\Bridge\Nexmo\NexmoTransportFactory; use Symfony\Component\Notifier\Bridge\Octopush\OctopushTransportFactory; +use Symfony\Component\Notifier\Bridge\OneSignal\OneSignalTransportFactory; use Symfony\Component\Notifier\Bridge\OvhCloud\OvhCloudTransportFactory; use Symfony\Component\Notifier\Bridge\RocketChat\RocketChatTransportFactory; use Symfony\Component\Notifier\Bridge\Sendinblue\SendinblueTransportFactory; @@ -91,6 +92,7 @@ public static function setUpBeforeClass(): void MobytTransportFactory::class => false, NexmoTransportFactory::class => false, OctopushTransportFactory::class => false, + OneSignalTransportFactory::class => false, OvhCloudTransportFactory::class => false, RocketChatTransportFactory::class => false, SendinblueTransportFactory::class => false, @@ -150,6 +152,7 @@ public function messageWhereSchemeIsPartOfSchemeToPackageMapProvider(): \Generat yield ['mobyt', 'symfony/mobyt-notifier']; yield ['nexmo', 'symfony/nexmo-notifier']; yield ['octopush', 'symfony/octopush-notifier']; + yield ['onesignal', 'symfony/one-signal-notifier']; yield ['ovhcloud', 'symfony/ovh-cloud-notifier']; yield ['rocketchat', 'symfony/rocket-chat-notifier']; yield ['sendinblue', 'symfony/sendinblue-notifier']; diff --git a/src/Symfony/Component/Notifier/Tests/Message/PushMessageTest.php b/src/Symfony/Component/Notifier/Tests/Message/PushMessageTest.php new file mode 100644 index 000000000000..fc68dc766a2a --- /dev/null +++ b/src/Symfony/Component/Notifier/Tests/Message/PushMessageTest.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Tests\Message; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Notifier\Message\PushMessage; +use Symfony\Component\Notifier\Notification\Notification; + +/** + * @author Tomas Norkūnas + */ +class PushMessageTest extends TestCase +{ + public function testCanBeConstructed() + { + $message = new PushMessage('Hello', 'World'); + + $this->assertSame('Hello', $message->getSubject()); + $this->assertSame('World', $message->getContent()); + } + + public function testSetSubject() + { + $message = new PushMessage('Hello', 'World'); + $message->subject('dlrow olleH'); + + $this->assertSame('dlrow olleH', $message->getSubject()); + } + + public function testSetContent() + { + $message = new PushMessage('Hello', 'World'); + $message->content('dlrow olleH'); + + $this->assertSame('dlrow olleH', $message->getContent()); + } + + public function testSetTransport() + { + $message = new PushMessage('Hello', 'World'); + $message->transport('next_one'); + + $this->assertSame('next_one', $message->getTransport()); + } + + public function testCreateFromNotification() + { + $notification = new Notification('Hello'); + $notification->content('World'); + + $message = PushMessage::fromNotification($notification); + + $this->assertSame('Hello', $message->getSubject()); + $this->assertSame('World', $message->getContent()); + $this->assertSame($notification, $message->getNotification()); + } +}