├── CHANGELOG.md ├── Enum └── InterruptionLevel.php ├── LICENSE ├── PushyOptions.php ├── PushyTransport.php ├── PushyTransportFactory.php ├── README.md └── composer.json /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 7.1 5 | --- 6 | 7 | * Add the bridge 8 | -------------------------------------------------------------------------------- /Enum/InterruptionLevel.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Notifier\Bridge\Pushy\Enum; 13 | 14 | /** 15 | * @author Joseph Bielawski 16 | */ 17 | enum InterruptionLevel: string 18 | { 19 | case ACTIVE = 'active'; 20 | case CRITICAL = 'critical'; 21 | case PASSIVE = 'passive'; 22 | case TIME_SENSITIVE = 'time-sensitive'; 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024-present Fabien Potencier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /PushyOptions.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Notifier\Bridge\Pushy; 13 | 14 | use Symfony\Component\Notifier\Bridge\Pushy\Enum\InterruptionLevel; 15 | use Symfony\Component\Notifier\Exception\InvalidArgumentException; 16 | use Symfony\Component\Notifier\Message\MessageOptionsInterface; 17 | use Symfony\Component\Notifier\Notification\Notification; 18 | 19 | /** 20 | * @author Joseph Bielawski 21 | * 22 | * @see https://pushy.me/docs/api/send-notifications 23 | */ 24 | final class PushyOptions implements MessageOptionsInterface 25 | { 26 | public function __construct( 27 | private array $options = [], 28 | ) { 29 | } 30 | 31 | public static function fromNotification(Notification $notification): self 32 | { 33 | $options = new self(); 34 | $options->interruptionLevel( 35 | match ($notification->getImportance()) { 36 | Notification::IMPORTANCE_URGENT => InterruptionLevel::CRITICAL, 37 | Notification::IMPORTANCE_HIGH => InterruptionLevel::TIME_SENSITIVE, 38 | Notification::IMPORTANCE_MEDIUM => InterruptionLevel::ACTIVE, 39 | Notification::IMPORTANCE_LOW => InterruptionLevel::PASSIVE, 40 | } 41 | ); 42 | 43 | return $options; 44 | } 45 | 46 | public function toArray(): array 47 | { 48 | return $this->options; 49 | } 50 | 51 | public function getRecipientId(): ?string 52 | { 53 | return $this->options['to'] ?? null; 54 | } 55 | 56 | /** 57 | * @see https://pushy.me/docs/api/send-notifications#request-schema 58 | * 59 | * @param string|string[] $to 60 | * 61 | * @return $this 62 | */ 63 | public function to(string|array $to): static 64 | { 65 | $this->options['to'] = $to; 66 | 67 | return $this; 68 | } 69 | 70 | /** 71 | * @see https://pushy.me/docs/api/send-notifications#request-schema 72 | * 73 | * @return $this 74 | */ 75 | public function contentAvailable(bool $bool): static 76 | { 77 | $this->options['content_available'] = $bool; 78 | 79 | return $this; 80 | } 81 | 82 | /** 83 | * @see https://pushy.me/docs/api/send-notifications#request-schema 84 | * 85 | * @return $this 86 | */ 87 | public function mutableContent(bool $bool): static 88 | { 89 | $this->options['mutable_content'] = $bool; 90 | 91 | return $this; 92 | } 93 | 94 | /** 95 | * @see https://pushy.me/docs/api/send-notifications#request-schema 96 | * 97 | * @return $this 98 | */ 99 | public function ttl(int $seconds): static 100 | { 101 | if ($seconds > (86400 * 365)) { 102 | throw new InvalidArgumentException('Pushy notification time to live cannot exceed 365 days.'); 103 | } 104 | 105 | $this->options['time_to_live'] = $seconds; 106 | 107 | return $this; 108 | } 109 | 110 | /** 111 | * @see https://pushy.me/docs/api/send-notifications#request-schema 112 | * 113 | * @return $this 114 | */ 115 | public function schedule(int $seconds): static 116 | { 117 | if (false === \DateTime::createFromFormat('U', $seconds)) { 118 | throw new InvalidArgumentException('Pushy notification schedule time must be correct Unix timestamp.'); 119 | } 120 | 121 | if (\DateTime::createFromFormat('U', $seconds) >= new \DateTime('+1 year')) { 122 | throw new InvalidArgumentException('Pushy notification schedule time cannot exceed 1 year.'); 123 | } 124 | 125 | $this->options['schedule'] = $seconds; 126 | 127 | return $this; 128 | } 129 | 130 | /** 131 | * @see https://pushy.me/docs/api/send-notifications#request-schema 132 | * 133 | * @return $this 134 | */ 135 | public function collapseKey(string $collapseKey): static 136 | { 137 | if (32 < \strlen($collapseKey)) { 138 | throw new InvalidArgumentException('Pushy notification collapse key cannot be longer than 32 characters.'); 139 | } 140 | 141 | $this->options['collapse_key'] = $collapseKey; 142 | 143 | return $this; 144 | } 145 | 146 | /** 147 | * @return $this 148 | */ 149 | public function body(string $body): static 150 | { 151 | $this->options['notification']['body'] = $body; 152 | 153 | return $this; 154 | } 155 | 156 | /** 157 | * @see https://pushy.me/docs/api/send-notifications#request-schema 158 | * 159 | * @return $this 160 | */ 161 | public function badge(int $badge): static 162 | { 163 | $this->options['notification']['badge'] = $badge; 164 | 165 | return $this; 166 | } 167 | 168 | /** 169 | * @see https://pushy.me/docs/api/send-notifications#request-schema 170 | * 171 | * @return $this 172 | */ 173 | public function threadId(int $threadId): static 174 | { 175 | $this->options['notification']['thread_id'] = $threadId; 176 | 177 | return $this; 178 | } 179 | 180 | /** 181 | * @see https://pushy.me/docs/api/send-notifications#request-schema 182 | * 183 | * @return $this 184 | */ 185 | public function interruptionLevel(InterruptionLevel $interruptionLevel): static 186 | { 187 | $this->options['notification']['interruption_level'] = $interruptionLevel->value; 188 | 189 | return $this; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /PushyTransport.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Notifier\Bridge\Pushy; 13 | 14 | use Symfony\Component\Notifier\Exception\InvalidArgumentException; 15 | use Symfony\Component\Notifier\Exception\TransportException; 16 | use Symfony\Component\Notifier\Exception\UnsupportedMessageTypeException; 17 | use Symfony\Component\Notifier\Message\MessageInterface; 18 | use Symfony\Component\Notifier\Message\PushMessage; 19 | use Symfony\Component\Notifier\Message\SentMessage; 20 | use Symfony\Component\Notifier\Transport\AbstractTransport; 21 | use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; 22 | use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; 23 | use Symfony\Contracts\HttpClient\HttpClientInterface; 24 | 25 | /** 26 | * @author Joseph Bielawski 27 | */ 28 | final class PushyTransport extends AbstractTransport 29 | { 30 | protected const HOST = 'api.pushy.me'; 31 | 32 | public function __construct( 33 | #[\SensitiveParameter] private readonly string $apiKey, 34 | ?HttpClientInterface $client = null, 35 | ?EventDispatcherInterface $dispatcher = null, 36 | ) { 37 | parent::__construct($client, $dispatcher); 38 | } 39 | 40 | public function supports(MessageInterface $message): bool 41 | { 42 | return $message instanceof PushMessage && (null === $message->getOptions() || $message->getOptions() instanceof PushyOptions); 43 | } 44 | 45 | public function __toString(): string 46 | { 47 | return \sprintf('pushy://%s', $this->getEndpoint()); 48 | } 49 | 50 | protected function doSend(MessageInterface $message): SentMessage 51 | { 52 | if (!$message instanceof PushMessage) { 53 | throw new UnsupportedMessageTypeException(__CLASS__, PushMessage::class, $message); 54 | } 55 | 56 | $options = $message->getOptions()?->toArray() ?? []; 57 | $options['data'] = $message->getContent(); 58 | $options['notification']['title'] = $message->getSubject(); 59 | $options['to'] ??= $message->getRecipientId(); 60 | 61 | if (!$options['to']) { 62 | throw new InvalidArgumentException(\sprintf('The "%s" transport required the "to" option to be set.', __CLASS__)); 63 | } 64 | 65 | $endpoint = \sprintf('https://%s?api_key=%s', $this->getEndpoint(), $this->apiKey); 66 | $response = $this->client->request('POST', $endpoint, [ 67 | 'headers' => [ 68 | 'Accept' => 'application/json', 69 | 'Content-Type' => 'application/json', 70 | ], 71 | 'json' => array_filter($options), 72 | ]); 73 | 74 | try { 75 | $statusCode = $response->getStatusCode(); 76 | } catch (TransportExceptionInterface $e) { 77 | throw new TransportException('Could not reach the remote Pushy server.', $response, 0, $e); 78 | } 79 | 80 | if (200 !== $statusCode) { 81 | throw new TransportException(\sprintf('Unable to send the Pushy push notification: "%s".', $response->getContent(false)), $response); 82 | } 83 | 84 | $result = $response->toArray(false); 85 | 86 | if (!isset($result['id'])) { 87 | throw new TransportException(\sprintf('Unable to find the message ID within the Pushy response: "%s".', $response->getContent(false)), $response); 88 | } 89 | 90 | $sentMessage = new SentMessage($message, (string) $this); 91 | $sentMessage->setMessageId($result['id']); 92 | 93 | return $sentMessage; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /PushyTransportFactory.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Notifier\Bridge\Pushy; 13 | 14 | use Symfony\Component\Notifier\Exception\UnsupportedSchemeException; 15 | use Symfony\Component\Notifier\Transport\AbstractTransportFactory; 16 | use Symfony\Component\Notifier\Transport\Dsn; 17 | use Symfony\Component\Notifier\Transport\TransportInterface; 18 | 19 | /** 20 | * @author Joseph Bielawski 21 | */ 22 | final class PushyTransportFactory extends AbstractTransportFactory 23 | { 24 | public function create(Dsn $dsn): TransportInterface 25 | { 26 | if ('pushy' !== $dsn->getScheme()) { 27 | throw new UnsupportedSchemeException($dsn, 'pushy', $this->getSupportedSchemes()); 28 | } 29 | 30 | $apiKey = $dsn->getUser(); 31 | $host = 'default' === $dsn->getHost() ? null : $dsn->getHost(); 32 | $port = $dsn->getPort(); 33 | 34 | return (new PushyTransport($apiKey, $this->client, $this->dispatcher))->setHost($host)->setPort($port); 35 | } 36 | 37 | protected function getSupportedSchemes(): array 38 | { 39 | return ['pushy']; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Pushy Notifier 2 | ============== 3 | 4 | Provides [Pushy](https://pushy.me/) integration for Symfony Notifier. 5 | 6 | DSN example 7 | ----------- 8 | 9 | ``` 10 | PUSHY_DSN=pushy://API_KEY@default 11 | ``` 12 | 13 | where: 14 | 15 | - `API_KEY` is your application's API key, viewable in Pushy dashboard 16 | 17 | Resources 18 | --------- 19 | 20 | * [Contributing](https://symfony.com/doc/current/contributing/index.html) 21 | * [Report issues](https://github.com/symfony/symfony/issues) and 22 | [send Pull Requests](https://github.com/symfony/symfony/pulls) 23 | in the [main Symfony repository](https://github.com/symfony/symfony) 24 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony/pushy-notifier", 3 | "type": "symfony-notifier-bridge", 4 | "description": "Symfony Pushy Notifier Bridge", 5 | "keywords": [ 6 | "pushy", 7 | "notifier" 8 | ], 9 | "homepage": "https://symfony.com", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Joseph Bielawski", 14 | "email": "stloyd@gmail.com", 15 | "homepage": "https://github.com/stloyd" 16 | }, 17 | { 18 | "name": "Symfony Community", 19 | "homepage": "https://symfony.com/contributors" 20 | } 21 | ], 22 | "require": { 23 | "php": ">=8.2", 24 | "symfony/http-client": "^6.4|^7.0", 25 | "symfony/notifier": "^7.2" 26 | }, 27 | "require-dev": { 28 | "symfony/event-dispatcher": "^6.4|^7.0" 29 | }, 30 | "autoload": { 31 | "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Pushy\\": "" }, 32 | "exclude-from-classmap": [ 33 | "/Tests/" 34 | ] 35 | }, 36 | "minimum-stability": "dev" 37 | } 38 | --------------------------------------------------------------------------------