Skip to content

Commit f0f1e10

Browse files
committed
Add new Pushy notifier bridge
1 parent fd53091 commit f0f1e10

17 files changed

+631
-0
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2804,6 +2804,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $
28042804
NotifierBridge\PagerDuty\PagerDutyTransportFactory::class => 'notifier.transport_factory.pager-duty',
28052805
NotifierBridge\Plivo\PlivoTransportFactory::class => 'notifier.transport_factory.plivo',
28062806
NotifierBridge\Pushover\PushoverTransportFactory::class => 'notifier.transport_factory.pushover',
2807+
NotifierBridge\Pushy\PushyTransportFactory::class => 'notifier.transport_factory.pushy',
28072808
NotifierBridge\Redlink\RedlinkTransportFactory::class => 'notifier.transport_factory.redlink',
28082809
NotifierBridge\RingCentral\RingCentralTransportFactory::class => 'notifier.transport_factory.ring-central',
28092810
NotifierBridge\RocketChat\RocketChatTransportFactory::class => 'notifier.transport_factory.rocket-chat',

src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier_transports.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
'ovh-cloud' => Bridge\OvhCloud\OvhCloudTransportFactory::class,
8888
'plivo' => Bridge\Plivo\PlivoTransportFactory::class,
8989
'pushover' => Bridge\Pushover\PushoverTransportFactory::class,
90+
'pushy' => Bridge\Pushy\PushyTransportFactory::class,
9091
'redlink' => Bridge\Redlink\RedlinkTransportFactory::class,
9192
'ring-central' => Bridge\RingCentral\RingCentralTransportFactory::class,
9293
'sendberry' => Bridge\Sendberry\SendberryTransportFactory::class,
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/Tests export-ignore
2+
/phpunit.xml.dist export-ignore
3+
/.gitattributes export-ignore
4+
/.gitignore export-ignore
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
vendor/
2+
composer.lock
3+
phpunit.xml
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CHANGELOG
2+
=========
3+
4+
7.1
5+
---
6+
7+
* Add the bridge
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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.
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Message\MessageOptionsInterface;
16+
use Symfony\Component\Notifier\Notification\Notification;
17+
18+
/**
19+
* @author Joseph Bielawski <stloyd@gmail.com>
20+
*
21+
* @see https://pushy.me/docs/api/send-notifications
22+
*/
23+
final class PushyOptions implements MessageOptionsInterface
24+
{
25+
private const INTERRUPTION_LEVEL = ['passive', 'active', 'time-sensitive', 'critical'];
26+
27+
public function __construct(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 => 'critical',
37+
Notification::IMPORTANCE_HIGH => 'time-sensitive',
38+
Notification::IMPORTANCE_MEDIUM => 'active',
39+
Notification::IMPORTANCE_LOW => '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(int $interruptionLevel): static
186+
{
187+
if (!\in_array($interruptionLevel, self::INTERRUPTION_LEVEL, true)) {
188+
throw new InvalidArgumentException(sprintf('Pushy notification priority must be one of "%s".', implode(', ', self::INTERRUPTION_LEVEL)));
189+
}
190+
191+
$this->options['notification']['interruption_level'] = $interruptionLevel;
192+
193+
return $this;
194+
}
195+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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 <stloyd@gmail.com>
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+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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 <stloyd@gmail.com>
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+
}

0 commit comments

Comments
 (0)