Skip to content

Commit 0bb7e83

Browse files
authored
initial commit of functional ntfy notifier
1 parent 4f6296a commit 0bb7e83

File tree

6 files changed

+371
-0
lines changed

6 files changed

+371
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/vendor/
2+
3+
.idea
4+
.DS_Store

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
Ntfy Notifier
2+
================
3+
4+
Provides [Ntfy](https://github.com/binwiederhier/ntfy) integration for Symfony Notifier.
5+
6+
DSN example
7+
-----------
8+
9+
```
10+
NTFY_DSN=ntfy://[USER:PASSWORD@]NTFY_URL/TOPIC?[scheme=[https]]
11+
```
12+
13+
where:
14+
15+
- `NTFY_URL` is the ntfy server which you are using
16+
- if `default` is provided, this will default to the public ntfy server hosted on [ntfy.sh](https://ntfy.sh/).
17+
- _note_: you can provide specific ports here if the selfhosted ntfy server is running on a non-standard web port.
18+
- example: `NTFY_DSN=ntfy://foo.bar:8080/myntfytopic`
19+
- `TOPIC` is the topic on this ntfy server.
20+
21+
- Depending on whether the server is configured to support access control:
22+
- `USER` is the username to access the given topic on the ntfy server which you are using
23+
- `PASSWORD` is the username to access the given topic on the ntfy server which you are using
24+
25+
Optional configuration:
26+
- `scheme` should be adjusted to the appropriate value for the in the ntfy server (defaults to `https` if not set)
27+
- example: `http` should be used if the ntfy server is listening on the insecure HTTP protocol
28+

composer.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "dacodedbeat/ntfy-notifier",
3+
"type": "symfony-notifier-bridge",
4+
"description": "Symfony Ntfy Notifier Bridge",
5+
"keywords": [
6+
"ntfy",
7+
"notifier"
8+
],
9+
"license": "MIT",
10+
"authors": [
11+
{
12+
"name": "Arun Philip",
13+
"email": "[email protected]"
14+
}
15+
],
16+
"require": {
17+
"php": ">=7.2.5",
18+
"ext-json": "*",
19+
"symfony/http-client": "^4.3|^5.0|^6.0",
20+
"symfony/notifier": "^5.3|^6.0",
21+
"symfony/service-contracts": "^1.10|^2|^3"
22+
},
23+
"autoload": {
24+
"psr-4": {
25+
"Ntfy\\Symfony\\Component\\Notifier\\Bridge\\Ntfy\\": "src/"
26+
}
27+
},
28+
"minimum-stability": "dev"
29+
}

src/NtfyOptions.php

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
namespace Ntfy\Symfony\Component\Notifier\Bridge\Ntfy;
4+
5+
use Symfony\Component\Notifier\Message\MessageOptionsInterface;
6+
7+
final class NtfyOptions implements MessageOptionsInterface
8+
{
9+
private $options;
10+
11+
public function __construct(array $options = [])
12+
{
13+
$this->options = $options;
14+
}
15+
16+
/**
17+
* Passed directly to Request Headers
18+
*/
19+
public function toArray(): array
20+
{
21+
$options = array_merge(['Content-Type' => 'text/plain'], $this->options);
22+
if (isset($options['Tags'])) {
23+
$options['Tags'] = implode(',', $options['Tags']);
24+
}
25+
26+
return $options;
27+
}
28+
29+
public function getRecipientId(): ?string
30+
{
31+
return null;
32+
}
33+
34+
/**
35+
* @see https://ntfy.sh/docs/publish/#scheduled-delivery
36+
*/
37+
public function scheduleMessage(\DateTimeInterface $dateTime): self
38+
{
39+
$this->options['Delay'] = $dateTime->getTimestamp();
40+
41+
return $this;
42+
}
43+
44+
/**
45+
* @see https://ntfy.sh/docs/publish/#tags-emojis
46+
*/
47+
public function tags(array $tags): self
48+
{
49+
$this->options['Tags'] = $tags;
50+
51+
return $this;
52+
}
53+
54+
/**
55+
* @see https://ntfy.sh/docs/publish/#click-action
56+
*/
57+
public function click(string $url): self
58+
{
59+
$this->options['Click'] = $url;
60+
61+
return $this;
62+
}
63+
64+
/**
65+
* @see https://ntfy.sh/docs/publish/#attach-file-from-a-url
66+
*/
67+
public function attachFromUrl(string $url): self
68+
{
69+
$this->options['Attach'] = $url;
70+
71+
return $this;
72+
}
73+
74+
/**
75+
* @see https://ntfy.sh/docs/publish/#e-mail-notifications
76+
*/
77+
public function email(string $email): self
78+
{
79+
$this->options['Email'] = $email;
80+
81+
return $this;
82+
}
83+
84+
/**
85+
* @see https://ntfy.sh/docs/publish/#message-caching
86+
*/
87+
public function disableCache(): self
88+
{
89+
$this->options['Cache'] = 'no';
90+
91+
return $this;
92+
}
93+
94+
/**
95+
* @see https://ntfy.sh/docs/publish/#disable-firebase
96+
*/
97+
public function disableFirebase(): self
98+
{
99+
$this->options['Firebase'] = 'no';
100+
101+
return $this;
102+
}
103+
}

src/NtfyTransport.php

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
<?php
2+
3+
namespace Ntfy\Symfony\Component\Notifier\Bridge\Ntfy;
4+
5+
use Symfony\Component\Notifier\Exception\LogicException;
6+
use Symfony\Component\Notifier\Exception\TransportException;
7+
use Symfony\Component\Notifier\Exception\UnsupportedMessageTypeException;
8+
use Symfony\Component\Notifier\Message\MessageInterface;
9+
use Symfony\Component\Notifier\Message\PushMessage;
10+
use Symfony\Component\Notifier\Message\SentMessage;
11+
use Symfony\Component\Notifier\Notification\Notification;
12+
use Symfony\Component\Notifier\Transport\AbstractTransport;
13+
use Symfony\Component\Notifier\Transport\Dsn;
14+
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
15+
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
16+
use Symfony\Contracts\HttpClient\HttpClientInterface;
17+
18+
final class NtfyTransport extends AbstractTransport
19+
{
20+
protected const HOST = 'ntfy.sh';
21+
22+
private $dsn;
23+
private $topic;
24+
private $user;
25+
private $password;
26+
private $scheme;
27+
28+
public function __construct(Dsn $dsn, string $topic, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null)
29+
{
30+
$this->dsn = $dsn;
31+
$this->topic = $topic;
32+
33+
parent::__construct($client, $dispatcher);
34+
}
35+
36+
private static function getNtfyHeadersFromMessage(PushMessage $message): array
37+
{
38+
$notification = $message->getNotification();
39+
$ntfyHeaders = [];
40+
41+
$ntfyHeaders['Title'] = $notification->getSubject() ?? $message->getSubject();
42+
43+
$priority = $notification->getImportance() ?? Notification::IMPORTANCE_LOW;
44+
if ($priority === Notification::IMPORTANCE_MEDIUM) {
45+
$priority = 'default';
46+
}
47+
$ntfyHeaders['Priority'] = $priority;
48+
49+
if ($notification !== null && !empty($notification->getEmoji())) {
50+
$ntfyHeaders['Tags'] = [$notification->getEmoji()];
51+
}
52+
53+
return $ntfyHeaders;
54+
}
55+
56+
public function setUser(?string $user): self
57+
{
58+
$this->user = $user;
59+
60+
return $this;
61+
}
62+
63+
public function setPassword(?string $password): self
64+
{
65+
$this->password = $password;
66+
67+
return $this;
68+
}
69+
70+
public function setScheme(?string $scheme): self
71+
{
72+
$this->scheme = $scheme;
73+
74+
return $this;
75+
}
76+
77+
public function __toString(): string
78+
{
79+
return $this->dsn->getOriginalDsn();
80+
}
81+
82+
public function supports(MessageInterface $message): bool
83+
{
84+
return $message instanceof PushMessage &&
85+
(null === $message->getOptions() || $message->getOptions() instanceof NtfyOptions);
86+
}
87+
88+
protected function doSend(MessageInterface $message): SentMessage
89+
{
90+
if (!$message instanceof PushMessage) {
91+
throw new UnsupportedMessageTypeException(
92+
__CLASS__,
93+
PushMessage::class,
94+
$message
95+
);
96+
}
97+
98+
if (($options = $message->getOptions()) && !$options instanceof NtfyOptions) {
99+
throw new LogicException(
100+
sprintf('The "%s" transport only supports instances of "%s" for options.',
101+
__CLASS__,
102+
NtfyOptions::class
103+
)
104+
);
105+
}
106+
107+
if (null === $options) {
108+
$options = new NtfyOptions();
109+
}
110+
111+
$endpoint = $this->scheme . '://' . $this->getEndpoint();
112+
113+
$messageContent = $message->getContent();
114+
115+
$notificationHeaders = self::getNtfyHeadersFromMessage($message);
116+
$ntfyMessageOptionHeaders = (array)$options;
117+
118+
if (isset($notificationHeaders['Tags'])) { // handle emoji from Notification
119+
$tags = $notificationHeaders['Tags'];
120+
if (isset($ntfyMessageOptionHeaders['Tags'])) {
121+
$tags .= ',' . $ntfyMessageOptionHeaders['Tags'];
122+
}
123+
$ntfyMessageOptionHeaders['Tags'] = $tags;
124+
unset($notificationHeaders['Tags']);
125+
}
126+
127+
$messageOptions = [
128+
'headers' => array_merge($notificationHeaders, $ntfyMessageOptionHeaders),
129+
'body' => $messageContent,
130+
];
131+
132+
if (!empty($this->user) && !empty($this->password)) { // if DSN is configured to have user/pass, set it here
133+
$messageOptions['auth_basic'] = [$this->user, $this->password];
134+
}
135+
136+
// send off the message
137+
$response = $this->client->request('POST', $endpoint, $messageOptions);
138+
139+
try {
140+
$statusCode = $response->getStatusCode();
141+
} catch (TransportExceptionInterface $e) {
142+
throw new TransportException('Could not reach the ntfy server.', $response, 0, $e);
143+
}
144+
145+
if (200 !== $statusCode) {
146+
$errorMessage = $response->getContent(false);
147+
148+
throw new TransportException('Unable to post the ntfy message: ' . $errorMessage, $response);
149+
}
150+
151+
$success = $response->toArray(false);
152+
153+
$sentMessage = new SentMessage($message, (string)$this);
154+
$sentMessage->setMessageId($success['id']);
155+
156+
return $sentMessage;
157+
}
158+
159+
protected function getEndpoint(): string
160+
{
161+
$baseUri = parent::getEndpoint();
162+
163+
return $baseUri . $this->topic;
164+
}
165+
}

src/NtfyTransportFactory.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
namespace Ntfy\Symfony\Component\Notifier\Bridge\Ntfy;
4+
5+
use Symfony\Component\Notifier\Exception\UnsupportedSchemeException;
6+
use Symfony\Component\Notifier\Transport\AbstractTransportFactory;
7+
use Symfony\Component\Notifier\Transport\Dsn;
8+
use Symfony\Component\Notifier\Transport\TransportInterface;
9+
10+
final class NtfyTransportFactory extends AbstractTransportFactory
11+
{
12+
/**
13+
* @return NtfyTransport
14+
*/
15+
public function create(Dsn $dsn): TransportInterface
16+
{
17+
if ('ntfy' !== $dsn->getScheme()) {
18+
throw new UnsupportedSchemeException($dsn, 'ntfy', $this->getSupportedSchemes());
19+
}
20+
21+
$host = 'default' === $dsn->getHost() ? null : $dsn->getHost();
22+
$topic = $dsn->getPath();
23+
$transport = (new NtfyTransport($dsn, $topic))->setHost($host);
24+
if (!empty($port = $dsn->getPort())) {
25+
$transport->setPort($port);
26+
}
27+
28+
if (!empty($user = $dsn->getUser()) && !empty($password = $dsn->getPassword())) {
29+
$transport->setUser($user);
30+
$transport->setPassword($password);
31+
}
32+
33+
$transport->setScheme($dsn->getOption('scheme', 'https'));
34+
35+
return $transport;
36+
}
37+
38+
protected function getSupportedSchemes(): array
39+
{
40+
return ['ntfy'];
41+
}
42+
}

0 commit comments

Comments
 (0)