Compare commits

...

670 Commits

Author SHA1 Message Date
Philipp Heckel
ddf5d49895 Update install instructions, bump version 2022-04-24 22:09:58 -04:00
Philipp Heckel
899d895f29 Add ARMv6 install instructions 2022-04-24 21:53:08 -04:00
Philipp Heckel
27588b8a48 Makefile for ARMv6 2022-04-24 21:41:40 -04:00
Philipp Heckel
c3f4adb777 Revert urfave/cli, see https://github.com/urfave/cli/issues/1373 2022-04-24 21:20:49 -04:00
Philipp Heckel
3633503549 Bump version, update deps 2022-04-24 20:32:17 -04:00
Philipp Heckel
5494bcce88 Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web into main 2022-04-24 20:19:03 -04:00
Tiago Esperança Triques
b824a1f17f Translated using Weblate (Portuguese (Brazil))
Currently translated at 23.6% (36 of 152 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/pt_BR/
2022-04-25 02:18:20 +02:00
Philipp Heckel
5a6b6dacc0 Changelog 2022-04-24 20:01:14 -04:00
Tiago Esperança Triques
0c6e9d4fca Added translation using Weblate (Portuguese (Brazil)) 2022-04-24 23:44:03 +02:00
Philipp Heckel
0d0ca188bf Fix changelog 2022-04-23 20:48:44 -04:00
Philipp Heckel
9c91ae2744 Make sure clear= values are checked 2022-04-23 15:23:18 -04:00
Philipp Heckel
882f027f6c Changelog 2022-04-23 13:45:26 -04:00
Philipp Heckel
b805d49cfd Disallow HEAD/GET requests with body 2022-04-23 13:40:26 -04:00
Philipp Heckel
12f85cceb1 Add clear=true|false support to actions 2022-04-22 23:07:35 -04:00
Philipp Heckel
8f4a1db1f0 Changelog, add tests 2022-04-22 14:51:44 -04:00
Philipp Heckel
58bde32bfb Fix docs 2022-04-22 13:53:32 -04:00
Philipp Heckel
26b3aa27ae Add proper screenshot 2022-04-21 20:35:25 -04:00
Philipp Heckel
26ebd23bfd Add user actions to web app 2022-04-21 16:33:49 -04:00
Philipp Heckel
12d347976c Docs docs docs 2022-04-21 13:57:42 -04:00
Philipp Heckel
a779434bab More docs 2022-04-21 09:58:28 -04:00
Philipp Heckel
c5ec3b48b4 Ahhh 2022-04-20 19:15:15 -04:00
Philipp Heckel
712c292183 More docs 2022-04-20 16:31:25 -04:00
Philipp Heckel
8900df27c9 Docs, still WIP 2022-04-19 23:26:46 -04:00
Philipp Heckel
0eb511c714 Merge branch 'main' into actions 2022-04-19 19:37:35 -04:00
Philipp Heckel
d48eec5e66 Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web into main 2022-04-19 19:37:19 -04:00
Philipp Heckel
3a7fd7a620 Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web into actions 2022-04-19 19:37:13 -04:00
Philipp Heckel
85043b34a4 Merge branch 'main' into actions 2022-04-19 19:33:25 -04:00
Philipp Heckel
2df0e98749 Added Russian to changelog + web app 2022-04-19 19:31:50 -04:00
Philipp Heckel
3c3b2477af Docs (WIP), Firebase 2022-04-19 19:22:19 -04:00
Philipp Heckel
5a9b2122c2 Make simple actions parsing work 2022-04-19 09:14:32 -04:00
Ilya Mikheev
37e72e078d Translated using Weblate (Russian)
Currently translated at 100.0% (152 of 152 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ru/
2022-04-19 09:12:56 +02:00
Aleksej Muratov
a2dafc11f2 Translated using Weblate (Russian)
Currently translated at 100.0% (152 of 152 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ru/
2022-04-19 09:12:56 +02:00
Philipp Heckel
55869f551e Add ID 2022-04-17 14:29:43 -04:00
Philipp Heckel
967cde1fb5 JSON format 2022-04-16 20:12:01 -04:00
Philipp Heckel
26efd481e3 WIP Actions 2022-04-16 16:17:58 -04:00
Philipp Heckel
06fd7327de Docs for DND override 2022-04-15 15:39:58 -04:00
Philipp Heckel
a08d57ca0f Changelog 2022-04-15 12:19:48 -04:00
Philipp Heckel
c60c51871d Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web into main 2022-04-15 12:16:24 -04:00
Erik S
04c4150283 Translated using Weblate (Russian)
Currently translated at 42.7% (65 of 152 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ru/
2022-04-15 18:16:18 +02:00
109247019824
1feb038385 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (152 of 152 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/bg/
2022-04-15 18:16:17 +02:00
Erik S
61a5b0dbe9 Added translation using Weblate (Russian) 2022-04-14 09:02:20 +02:00
Philipp Heckel
690cd683f0 Changelog 2022-04-13 21:38:56 -04:00
Philipp Heckel
c87c81f663 Add WebSockets support to docs/config 2022-04-13 14:06:20 -04:00
Philipp Heckel
8190d5b1f4 Added docs for "Share to topic" and "ntfy:// links" 2022-04-12 20:10:05 -04:00
Philipp Heckel
75c11371e6 Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web into main 2022-04-11 22:00:14 -04:00
Rogelio Dominguez
ffa0bf05cd Translated using Weblate (Spanish)
Currently translated at 100.0% (152 of 152 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/es/
2022-04-12 04:00:09 +02:00
Philipp Heckel
8e1c57af25 Added Norwegian 2022-04-11 20:18:18 -04:00
Shoshin Akamine
c62916a43c Translated using Weblate (Japanese)
Currently translated at 100.0% (152 of 152 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ja/
2022-04-12 01:56:34 +02:00
Linerly
f5145ffaae Translated using Weblate (Indonesian)
Currently translated at 100.0% (152 of 152 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/id/
2022-04-12 01:56:34 +02:00
109247019824
0a6aba1ac7 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (152 of 152 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/bg/
2022-04-12 01:56:33 +02:00
Allan Nordhøy
5d30246c35 Translated using Weblate (Norwegian Bokmål)
Currently translated at 76.3% (116 of 152 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/nb_NO/
2022-04-12 01:56:33 +02:00
Oğuz Ersen
e9386ecfe3 Translated using Weblate (Turkish)
Currently translated at 100.0% (152 of 152 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/tr/
2022-04-12 01:56:31 +02:00
Christian Meis
04f5d4acb7 Translated using Weblate (German)
Currently translated at 100.0% (152 of 152 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2022-04-12 01:56:31 +02:00
Philipp Heckel
841c08fcb6 Adding Spanish translation 2022-04-10 15:21:13 -04:00
Philipp Heckel
2d7c354723 Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web into main 2022-04-10 15:16:11 -04:00
Rogelio Dominguez
6fec79055e Translated using Weblate (Spanish)
Currently translated at 100.0% (137 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/es/
2022-04-10 21:16:06 +02:00
109247019824
f61a8f82a7 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (137 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/bg/
2022-04-10 21:16:05 +02:00
Philipp Heckel
136883fd94 Additional descriptions for settings (#203), URL validation (#204) 2022-04-10 15:13:12 -04:00
Philipp Heckel
9c3f5929c7 Changelog 2022-04-09 15:12:03 -04:00
Philipp Heckel
39bd1fe164 Added Japanese + Indonesian to web app 2022-04-09 10:54:09 -04:00
Philipp Heckel
67ea467501 Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web into main 2022-04-09 10:47:47 -04:00
Shoshin Akamine
ed946195e2 Translated using Weblate (Japanese)
Currently translated at 100.0% (137 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ja/
2022-04-09 16:47:41 +02:00
Linerly
84bf95fa85 Translated using Weblate (Indonesian)
Currently translated at 100.0% (137 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/id/
2022-04-09 16:47:39 +02:00
109247019824
cf9ba9b1f9 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (137 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/bg/
2022-04-09 16:47:38 +02:00
Oğuz Ersen
1a18ce9e21 Translated using Weblate (Turkish)
Currently translated at 100.0% (137 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/tr/
2022-04-09 16:47:37 +02:00
Philipp Heckel
044b717f86 Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web into main 2022-04-09 10:46:00 -04:00
Shoshin Akamine
8777718afc Added translation using Weblate (Japanese) 2022-04-09 09:12:08 +02:00
Linerly
8e3910c76d Added translation using Weblate (Indonesian) 2022-04-09 04:39:31 +02:00
Philipp Heckel
448444eccf Show snack bar error message when publishing fails, closes #205 2022-04-08 20:24:11 -04:00
Philipp Heckel
65cd380527 Service URL 2022-04-08 19:31:50 -04:00
J. Lavoie
71a49ac1a6 Translated using Weblate (French)
Currently translated at 39.4% (54 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/fr/
2022-04-09 01:30:55 +02:00
Rogelio Dominguez
1fba62276c Translated using Weblate (Spanish)
Currently translated at 13.1% (18 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/es/
2022-04-09 01:30:55 +02:00
Christian Meis
29f265be30 Translated using Weblate (German)
Currently translated at 98.5% (135 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2022-04-09 00:10:19 +02:00
Christian Meis
4c9011f391 Translated using Weblate (German)
Currently translated at 98.5% (135 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2022-04-09 00:01:15 +02:00
Christian Meis
155475422e Translated using Weblate (German)
Currently translated at 98.5% (135 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2022-04-08 23:53:10 +02:00
Christian Meis
32353e0f02 Translated using Weblate (German)
Currently translated at 98.5% (135 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2022-04-08 23:51:11 +02:00
Christian Meis
69159b9aae Translated using Weblate (German)
Currently translated at 98.5% (135 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2022-04-08 23:43:22 +02:00
J. Lavoie
b47d0ac240 Translated using Weblate (German)
Currently translated at 98.5% (135 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2022-04-08 23:43:22 +02:00
J. Lavoie
d14af78403 Translated using Weblate (French)
Currently translated at 16.0% (22 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/fr/
2022-04-08 23:29:05 +02:00
Rogelio Dominguez
9cb08036ef Translated using Weblate (Spanish)
Currently translated at 10.9% (15 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/es/
2022-04-08 23:29:04 +02:00
109247019824
e0da6b1302 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (137 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/bg/
2022-04-08 23:29:04 +02:00
Christian Meis
fcb1f938b9 Translated using Weblate (German)
Currently translated at 98.5% (135 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2022-04-08 23:29:04 +02:00
J. Lavoie
9c094c1cc3 Added translation using Weblate (French) 2022-04-08 23:25:41 +02:00
Rogelio Dominguez
69c6f24d97 Added translation using Weblate (Spanish) 2022-04-08 21:57:48 +02:00
Philipp Heckel
e8b020ff45 Replace placeholders 2022-04-08 15:26:14 -04:00
Philipp Heckel
2ec9a7307e Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web into main 2022-04-08 15:21:54 -04:00
Philipp Heckel
738ee5cf35 Suggested fixes for delay string, widen priority dropdown, add German and Turkish 2022-04-08 15:21:22 -04:00
109247019824
8144d39e29 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (137 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/bg/
2022-04-08 21:21:05 +02:00
Oğuz Ersen
788d5e9f9b Translated using Weblate (Turkish)
Currently translated at 100.0% (137 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/tr/
2022-04-08 21:21:05 +02:00
Philipp Heckel
d399d2fe1c Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web into main 2022-04-08 14:56:15 -04:00
109247019824
615b09a774 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (137 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/bg/
2022-04-08 20:56:03 +02:00
Oğuz Ersen
7a5e8cc44b Translated using Weblate (Turkish)
Currently translated at 7.2% (10 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/tr/
2022-04-08 20:56:03 +02:00
Christian Meis
291b49488b Translated using Weblate (German)
Currently translated at 97.8% (134 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2022-04-08 20:56:02 +02:00
Philipp Heckel
aa58242551 Update language array to match finished languages 2022-04-08 12:54:53 -04:00
Philipp Heckel
b08ea2c416 Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web into main 2022-04-08 12:47:28 -04:00
109247019824
98f02f78db Translated using Weblate (Bulgarian)
Currently translated at 100.0% (137 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/bg/
2022-04-08 18:47:19 +02:00
Philipp Heckel
d2f933e15f Fix English language strings, as per #203 2022-04-08 12:45:41 -04:00
109247019824
d672969840 Added translation using Weblate (Bulgarian) 2022-04-08 17:00:21 +02:00
Allan Nordhøy
8c4f0c1253 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (1 of 1 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/nb_NO/
2022-04-08 16:48:46 +02:00
Allan Nordhøy
18c88e567c Added translation using Weblate (Norwegian Bokmål) 2022-04-08 16:48:46 +02:00
Oğuz Ersen
2c5505852e Translated using Weblate (Turkish)
Currently translated at 100.0% (1 of 1 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/tr/
2022-04-08 16:48:46 +02:00
Oğuz Ersen
bc8f245064 Added translation using Weblate (Turkish) 2022-04-08 16:48:46 +02:00
Philipp Heckel
30726144b8 Finish web app translation 2022-04-08 10:44:35 -04:00
Philipp Heckel
893701c07b Extracting translation strings 2022-04-07 21:46:33 -04:00
Philipp Heckel
0ec9a4c89b Changelog 2022-04-07 20:33:15 -04:00
Philipp Heckel
96fb7e2296 Working language switcher 2022-04-07 20:31:24 -04:00
Philipp Heckel
750e390b5d WIP: Translation of web app 2022-04-07 19:11:51 -04:00
Philipp Heckel
7a8cfb5f66 Changelog 2022-04-07 15:41:44 -04:00
Philipp Heckel
d761ce929c Merge branch 'armv6' into main 2022-04-07 13:35:13 -04:00
Philipp Heckel
29969582e9 Update go.sum 2022-04-06 20:50:36 -04:00
Philipp Heckel
749e334396 Bump version 2022-04-06 20:33:19 -04:00
Philipp Heckel
78a681f277 Fix UTF-8 issues in publish message dialog 2022-04-06 20:04:27 -04:00
Philipp Heckel
e22ec2c505 ARMv6 2022-04-06 19:45:30 -04:00
Philipp Heckel
3f96fad7ce Remove now unused splitTopicUrl function 2022-04-06 13:27:32 -04:00
Philipp Heckel
83bb9951b0 Split baseUrl and topic 2022-04-05 23:33:07 -04:00
Philipp Heckel
4a5f34801a Do not hide notification behind message bar 2022-04-05 22:57:57 -04:00
Philipp Heckel
2cd7839da3 Docblock 2022-04-05 19:55:43 -04:00
Philipp Heckel
35ddcb27f0 Good enough emoji picker 2022-04-05 19:40:34 -04:00
Philipp Heckel
328aca48ab Filter emojis that don't render in Chrome on Desktop 2022-04-04 20:44:40 -04:00
Philipp Heckel
4eba641ec3 Emoji picker 2022-04-04 19:56:21 -04:00
Philipp Heckel
f2d4af04e3 Emoji picker 2022-04-04 10:04:01 -04:00
Philipp Heckel
d44ee2bbf6 Rename Icon->AttachmentIcon 2022-04-04 08:40:54 -04:00
Philipp Heckel
6f07944442 Publish message button 2022-04-03 22:58:44 -04:00
Philipp Heckel
7716b1e81e Push drop zone down to dialog 2022-04-03 22:42:56 -04:00
Philipp Heckel
8914809775 Remove showOpen 2022-04-03 22:28:41 -04:00
Philipp Heckel
dcb5531038 Merge branch 'main' into custom-messages 2022-04-03 22:17:45 -04:00
Philipp Heckel
0c666f96b1 Added Apprise to release notes 2022-04-03 22:17:29 -04:00
Philipp Heckel
d9c3c20350 Publish message button 2022-04-03 22:11:26 -04:00
Philipp Heckel
73349cd423 Add test 2022-04-03 20:19:43 -04:00
Philipp Heckel
b3667a916b Merge branch 'main' into custom-messages 2022-04-03 19:51:46 -04:00
Philipp Heckel
6791c7395b Almost there 2022-04-03 19:51:32 -04:00
Philipp Heckel
aba7e86cbc Attachment behavior fix for Firefox 2022-04-03 12:39:52 -04:00
Philipp Heckel
12f973f61b Changelog 2022-04-02 17:16:46 -04:00
Philipp Heckel
f98743dd9b Continued work on send dialog and drag and drop 2022-04-02 17:06:26 -04:00
Philipp Heckel
2c8b258ae7 Publish another checkbox 2022-04-01 11:34:53 -04:00
Philipp Heckel
00520a7a38 Merge branch 'main' into custom-messages 2022-04-01 08:54:29 -04:00
Philipp Heckel
611894fd05 Release notes, add EXPOSE 80 to Dockerfile 2022-04-01 08:49:15 -04:00
Philipp Heckel
aabae53e5d File upload 2022-04-01 08:41:45 -04:00
Philipp C. Heckel
85cf7bb687 Merge pull request #194 from RasHas/patch-1
Update install docs
2022-03-31 18:17:07 -04:00
Philipp C. Heckel
44b9358c60 Update install.md 2022-03-31 18:16:56 -04:00
RasHas
ee6188d100 Update install docs
Add docker-compose example
2022-03-31 23:11:39 +03:00
Philipp Heckel
2bdae49425 Make Attach URL prettier 2022-03-31 12:03:36 -04:00
Philipp Heckel
9814a9f792 Merge branch 'main' into custom-messages 2022-03-31 10:06:55 -04:00
Philipp Heckel
5aedfd3898 Changelog 2022-03-30 20:17:11 -04:00
Philipp Heckel
59ec2de8bd Fix race in test 2022-03-30 14:37:42 -04:00
Philipp Heckel
d154d3936d Bump version, release notes 2022-03-30 14:26:31 -04:00
Philipp Heckel
5125aac91c Remove upx for arm64/armv7, more translation credits 2022-03-30 14:23:57 -04:00
Philipp Heckel
7ff34364a3 Editable attachment filename 2022-03-30 14:11:18 -04:00
Philipp Heckel
3d0d70dc17 Merge branch 'main' into custom-messages 2022-03-30 10:01:40 -04:00
Philipp Heckel
62512b7a1a Change deprecation warning 2022-03-30 10:01:16 -04:00
Philipp Heckel
c5a1344e8a WIP: Make attachment filename editabe 2022-03-30 09:57:22 -04:00
Philipp Heckel
402b05a27b Merge branch 'main' into custom-messages 2022-03-29 19:37:06 -04:00
Philipp Heckel
b67d9fc85d Added missing 'delay' and 'email' params to publish as json 2022-03-29 15:40:26 -04:00
Philipp Heckel
3e121f5d3c Continued work on the send dialog 2022-03-29 15:22:26 -04:00
Philipp Heckel
b6426f0417 Merge branch 'main' into custom-messages 2022-03-29 11:54:50 -04:00
Philipp Heckel
59b341dfb8 Fix color of home page 2022-03-29 11:47:56 -04:00
Philipp Heckel
e2834a7c4d Changelog 2022-03-29 10:44:12 -04:00
Philipp Heckel
e0b3068a5e Merge branch 'main' into custom-messages 2022-03-28 23:10:54 -04:00
Philipp Heckel
2280031a80 Release notes, translations 2022-03-28 23:10:44 -04:00
Philipp Heckel
8f2851e20a Release notes, translations 2022-03-28 23:10:05 -04:00
Philipp Heckel
2eeb7d63a0 SendDialog, cont'd 2022-03-28 22:54:27 -04:00
Philipp Heckel
b20df55b88 Merge branch 'main' into custom-messages 2022-03-28 14:14:20 -04:00
Philipp Heckel
de1b97bbce Merge branch 'main' of github.com:binwiederhier/ntfy into main 2022-03-28 14:10:24 -04:00
Philipp Heckel
3b4a4108e5 Release log 2022-03-28 14:10:14 -04:00
Philipp C. Heckel
dc1c0ddd4e Update README.md 2022-03-28 11:07:05 -04:00
Philipp Heckel
182e21a9c3 Fix pruning bug in web app (closes #186), release notes, remove local storage migration 2022-03-27 09:20:25 -04:00
Philipp Heckel
187c19f3b2 Continued work on publishing from the web app 2022-03-27 09:10:47 -04:00
Philipp Heckel
d5eff0cd34 Merge branch 'main' into custom-messages 2022-03-26 14:17:28 -04:00
Philipp Heckel
d4fe2052c7 Release notes 2022-03-26 13:33:16 -04:00
Philipp Heckel
2e92be0f23 Remove other fields 2022-03-26 09:32:13 -04:00
Philipp Heckel
94b0e6f690 Merge branch 'main' into custom-messages 2022-03-25 21:43:45 -04:00
Philipp Heckel
202051bbbf Merge branch 'main' of github.com:binwiederhier/ntfy into main 2022-03-25 21:35:56 -04:00
Philipp Heckel
a693975526 Email docs and release notes 2022-03-25 21:35:40 -04:00
Philipp C. Heckel
4cd4e890fe Update README.md 2022-03-25 18:56:22 -04:00
Philipp C. Heckel
5dc8031ec9 Update README.md 2022-03-25 18:54:40 -04:00
Philipp Heckel
03ad5dcff6 Add Allow-Origin: *, because YOLO 2022-03-25 17:17:24 -04:00
Philipp Heckel
5f508e1839 Tiny docs changes 2022-03-25 13:51:04 -04:00
Philipp C. Heckel
c5642799df Merge pull request #178 from nickexyz/docs-noderedupdate
Add Node-RED pictures and change ntfy URL to ntfy.sh
2022-03-25 13:32:05 -04:00
Philipp Heckel
5a99fe8ba2 Try it a better way 2022-03-24 19:27:07 -04:00
Philipp Heckel
ee0f448d86 Another try 2022-03-24 19:25:41 -04:00
Philipp Heckel
a222f64ee4 Hopefully fix the build 2022-03-24 19:23:30 -04:00
Philipp Heckel
140daec0d3 Fix date formatting issue in example 2022-03-24 13:17:04 -04:00
Philipp Heckel
b409c89d3b Do not allow comma in topic name in publish via GET endpoint (no ticket) 2022-03-23 14:29:55 -04:00
Philipp Heckel
806893962c Changelog 2022-03-23 11:02:12 -04:00
Philipp Heckel
14d3c5e93e Changelog 2022-03-22 20:31:43 -04:00
Philipp Heckel
37e14b13a4 Update deps 2022-03-22 19:54:20 -04:00
Philipp Heckel
d7fa51be2c Changelog 2022-03-22 15:26:15 -04:00
Philipp Heckel
a3e28e71aa Bump version 2022-03-21 23:23:46 -04:00
Philipp Heckel
35cef8386c Release docs 2022-03-21 23:13:45 -04:00
Philipp Heckel
38072c9cdd Release log 2022-03-20 19:55:32 -04:00
Philipp Heckel
13d741b89e Changelog 2022-03-20 15:12:06 -04:00
Philipp Heckel
cc90a1af15 WIP: custom messages 2022-03-20 13:52:07 -04:00
nickexyz
21fc1245eb Update examples.md 2022-03-19 00:20:56 +00:00
Niclas Andersson
2511ba7627 Add Node-RED pictures and change ntfy URL to ntfy.sh 2022-03-19 01:16:02 +01:00
Philipp Heckel
23547f4504 New lines 2022-03-18 19:39:37 -04:00
Philipp Heckel
e6f19d050f Updated examples, release notes 2022-03-18 17:34:14 -04:00
Philipp C. Heckel
3ec8084450 Merge pull request #176 from nickexyz/docs-update
Add Node-RED and Gatus examples.
2022-03-18 17:27:57 -04:00
Philipp Heckel
2edb722c0e Refinement 2022-03-18 17:18:52 -04:00
Philipp Heckel
1f75498dca More docs 2022-03-18 17:00:08 -04:00
Niclas Andersson
ab19c4d688 Add Node-RED and Gatus examples. 2022-03-18 21:58:33 +01:00
Philipp Heckel
15265d9ef3 Merge branch 'main' into develop-docs 2022-03-18 15:08:29 -04:00
Philipp Heckel
2839a7228f Merge branch 'main' of github.com:binwiederhier/ntfy into HEAD 2022-03-18 15:00:01 -04:00
Philipp Heckel
c2036975fa Lots of development instructions, Makefile things 2022-03-18 13:53:52 -04:00
Joe Harrison
7aa0f87376 Merge pull request #1 from Joeharrison94/Joeharrison94-patch-1
Update to publish.md to add PowerShell examples
2022-03-18 09:18:01 +00:00
Joe Harrison
df372d1a7e Update to publish.md to add PowerShell examples 2022-03-18 09:17:19 +00:00
Philipp Heckel
6cd31502e7 Merge branch 'main' of github.com:binwiederhier/ntfy into develop-docs 2022-03-17 16:04:15 -04:00
Philipp C. Heckel
bade88079f Merge pull request #173 from nickexyz/arr_docs
Add examples for *arr notification scripts
2022-03-17 16:03:42 -04:00
Niclas Andersson
20ab05afc8 Add examples for *arr notification scripts 2022-03-17 19:20:48 +01:00
Philipp Heckel
5b10f51af1 WIP: Develop docs 2022-03-16 22:33:23 -04:00
Philipp Heckel
470d11f442 Fix install instructions 2022-03-16 21:40:56 -04:00
Philipp Heckel
4952f0fbd2 Tiny fixes to release notes 2022-03-16 21:05:51 -04:00
Philipp Heckel
0a3292566c Bump version 2022-03-16 19:20:36 -04:00
Philipp Heckel
f4e8ebc053 Remove urfave/cli workaround 2022-03-16 14:50:00 -04:00
Philipp Heckel
ad77bde8c8 Update release log 2022-03-16 14:25:44 -04:00
Philipp Heckel
5241b29cc6 Merge branch 'main' of github.com:binwiederhier/ntfy into main 2022-03-16 14:17:19 -04:00
Philipp Heckel
8fcc40942f Publish as JSON 2022-03-16 14:16:54 -04:00
Philipp Heckel
37d4d5d647 PUT/POST as JSON, relates to #133 2022-03-15 16:00:59 -04:00
Philipp C. Heckel
b67b9e83ff Merge pull request #170 from sandebert/patch-1
Fixed typo in url
2022-03-15 11:15:20 -04:00
Fredrik Sandebert
4c3dcec19e Fixed typo in url 2022-03-15 16:09:20 +01:00
Philipp Heckel
53375ff559 Examples 2022-03-15 08:27:17 -04:00
Philipp Heckel
53e08988e7 rpm: Do not replace server.yml, closes #166 2022-03-14 17:21:28 -04:00
Philipp Heckel
d0bbda555f Add Android WebSockets deprecation, remove 'ntfy serve' deprecation 2022-03-13 22:16:48 -04:00
Philipp Heckel
207e990798 Fix brittle test 2022-03-13 21:30:14 -04:00
Philipp Heckel
b0a07af28d Changelog 2022-03-13 20:21:43 -04:00
Philipp C. Heckel
1a8bac7ab1 Update README.md 2022-03-13 16:08:38 -04:00
Philipp Heckel
dc03c13642 Update docs for UnifiedPush 2.0 spec 2022-03-13 16:06:40 -04:00
Philipp C. Heckel
739b20583d Update releases.md 2022-03-12 10:33:23 -05:00
Philipp Heckel
10ccbc780b Docs, bump version 2022-03-12 08:37:23 -05:00
Philipp Heckel
f971a36ec0 Merge branch 'main' of github.com:binwiederhier/ntfy into main 2022-03-12 08:15:48 -05:00
Philipp Heckel
3699464947 Remove crypto.subtle requirement 2022-03-12 08:15:30 -05:00
Philipp C. Heckel
3a3d1262ab Merge pull request #156 from ChaseCares/ChaseCares-readme-screenshots
Update README.md
2022-03-11 22:07:33 -05:00
ChaseCares
395a97c0e5 Update README.md
Commit 4a6aca4 changed the directory structure, this pull requests updates screenshot URLs.

Feel free to disregard, I am new to submitting pull requests.
Looks great!
Chase
2022-03-11 19:02:44 -08:00
Philipp Heckel
4a6aca4c07 Fix packaging 2022-03-11 16:06:08 -05:00
Philipp Heckel
08f0d5fd1f Bump version 2022-03-11 16:01:33 -05:00
Philipp Heckel
750be7f07e Fix content type for config.js 2022-03-11 15:56:54 -05:00
Philipp Heckel
70538783d8 Fix one-off migration 2022-03-11 15:32:24 -05:00
Philipp Heckel
09336fa1e4 Comments 2022-03-11 15:17:12 -05:00
Philipp Heckel
c124434429 Migrate topics from old web ui; nicer stack traces 2022-03-11 14:43:54 -05:00
Philipp Heckel
0544a6f00d Feature complete 2022-03-11 11:46:19 -05:00
Philipp Heckel
7b186af765 Docs and screenshots 2022-03-11 10:43:18 -05:00
Philipp Heckel
3f978bc45f Better test messages 2022-03-10 22:58:24 -05:00
Philipp Heckel
488aeb119b Gzip static responses 2022-03-10 21:55:56 -05:00
Philipp Heckel
160c72997f Fix auth base64, fix iPhone things 2022-03-10 18:11:12 -05:00
Philipp Heckel
ccb9da9333 Add error boundary 2022-03-10 15:37:50 -05:00
Philipp Heckel
840cb5b182 Add server-generated /config.js; add error boundary 2022-03-09 23:28:55 -05:00
Philipp Heckel
04ee6b8be2 Embed resources 2022-03-09 15:58:21 -05:00
Philipp Heckel
8c8a1685b2 Fix it 2022-03-08 21:18:15 -05:00
Philipp Heckel
28e6f8a0f6 Autosubscribe (WIP) 2022-03-08 20:26:15 -05:00
Philipp Heckel
d9e5e08af5 No notifications page text 2022-03-08 18:56:28 -05:00
Philipp Heckel
60980df26b Mute button 2022-03-08 16:56:41 -05:00
Philipp Heckel
d3462d2905 Start work on ephemeral topics 2022-03-08 15:19:15 -05:00
Philipp Heckel
0aefcf29ef This is it 2022-03-08 14:29:03 -05:00
Philipp Heckel
55c021796e Attempt to use react router the way it was meant to 2022-03-08 14:13:32 -05:00
Philipp Heckel
4aad98256a Move things around a bit 2022-03-08 11:33:17 -05:00
Philipp Heckel
30b13cbdbc Working infinite scroll 2022-03-08 11:21:11 -05:00
Philipp Heckel
6d140d6a86 Working infinite scroll 2022-03-07 23:07:07 -05:00
Philipp Heckel
9757983046 Prep for infinite scroll 2022-03-07 20:11:58 -05:00
Philipp Heckel
5bed926323 Home page; "all notifications" 2022-03-07 16:36:49 -05:00
Philipp Heckel
1d2f3f72e4 Add "new" badge and title 2022-03-06 22:37:13 -05:00
Philipp Heckel
3a76e4733c Cleanup 2022-03-06 21:39:20 -05:00
Philipp Heckel
a4fbb1b4c5 Home button 2022-03-06 16:35:31 -05:00
Philipp Heckel
94296e7dd8 Licenses 2022-03-06 10:42:05 -05:00
Philipp Heckel
dc7ca6e405 Support sounds 2022-03-06 00:02:27 -05:00
Philipp Heckel
09b128f27a Move more stuff out of App.js 2022-03-05 22:33:34 -05:00
Philipp Heckel
acde2e5b6e Remove indexPage 2022-03-05 22:18:03 -05:00
Philipp Heckel
420e35c33c Use location.origin as default base URL 2022-03-05 22:11:32 -05:00
Philipp Heckel
c5ce51f242 Add --web-root switch 2022-03-05 21:28:25 -05:00
Philipp Heckel
2743c96694 Re-embed fonts 2022-03-05 21:15:40 -05:00
Philipp Heckel
36ccfac787 Fix tests 2022-03-05 20:48:27 -05:00
Philipp Heckel
e27d5719f0 Embed new web UI into server 2022-03-05 20:24:10 -05:00
Philipp Heckel
1a3816c1ff Strip down old web app 2022-03-05 14:48:42 -05:00
Philipp Heckel
52a55f71e6 Support external routes 2022-03-05 08:52:52 -05:00
Philipp Heckel
b5670d9a71 Routing 2022-03-04 16:10:04 -05:00
Philipp Heckel
e7bd3abadc SubscribeDialog use existing user 2022-03-04 12:10:11 -05:00
Philipp Heckel
5878d7e5a6 Conn state listener, click action button 2022-03-04 11:08:32 -05:00
Philipp Heckel
3bce0ad4ae Lightbox backdrop fixes 2022-03-03 20:28:16 -05:00
Philipp Heckel
695e029147 Make connections react on changes of users; this works wonderfully 2022-03-03 20:07:35 -05:00
Philipp Heckel
08846e4cc2 Refactor the db; move to *Manager classes 2022-03-03 16:52:07 -05:00
Philipp Heckel
f9219d2d96 Attachments 2022-03-03 14:51:56 -05:00
Philipp Heckel
7dfb2d50c7 Attachments, WIP 2022-03-02 20:22:53 -05:00
Philipp Heckel
349872bdb3 Switch everything to Dexie.js 2022-03-02 16:16:30 -05:00
Philipp Heckel
39f4613719 Do not store notifications in localStorage anymore 2022-03-01 22:41:49 -05:00
Philipp Heckel
effc1f42eb Switch prefs to dexie 2022-03-01 22:01:51 -05:00
Philipp Heckel
23d275acec Add Dexie for persistence; user management with dexie; this is the way 2022-03-01 21:23:12 -05:00
Philipp Heckel
8036aa2942 Remove mui/styles, Settings page, make minPriority functional, ahh so ugly 2022-03-01 16:22:47 -05:00
Philipp Heckel
f23c7a2dbf Use another server 2022-02-28 16:56:38 -05:00
Philipp Heckel
17e5af654b "No topics" and "No notifications" view 2022-02-28 11:52:50 -05:00
Philipp Heckel
0909354a6c Switch to since=ID 2022-02-27 19:29:17 -05:00
Philipp Heckel
cda9dfa9d0 Merge branch 'main' into ui 2022-02-27 16:10:21 -05:00
Philipp Heckel
018fa816e2 Update docs 2022-02-27 16:02:46 -05:00
Philipp Heckel
efa6d03ba5 Bump version 2022-02-27 15:49:31 -05:00
Philipp Heckel
1ed4ebaf03 Docs, release notes 2022-02-27 15:45:43 -05:00
Philipp Heckel
10c69a722f Merge branch 'main' of github.com:binwiederhier/ntfy into main 2022-02-27 14:58:18 -05:00
Philipp Heckel
324500d0b3 Deprecation notice 2022-02-27 14:57:44 -05:00
Philipp Heckel
4cd30c35ce Rename cache to messageCache 2022-02-27 14:47:28 -05:00
Philipp Heckel
e79dbf4d00 Docs 2022-02-27 14:40:44 -05:00
Philipp Heckel
e29a18a076 Add another scheduled message to since ID test 2022-02-27 14:31:22 -05:00
Philipp Heckel
f17df1e926 Combine entirely 2022-02-27 14:25:26 -05:00
Philipp Heckel
c21737d546 Combine tests and all that 2022-02-27 14:21:34 -05:00
Philipp Heckel
6dc4e441e4 Fix tests; remove memory implementation entirely 2022-02-27 14:05:13 -05:00
Philipp Heckel
7d93b0596b Almost there; Replace memCache with :memory: SQLite cache 2022-02-27 09:38:46 -05:00
Philipp Heckel
8b32cfaaff Implement since=ID logic in mem cache; add tests; still failing 2022-02-26 20:19:28 -05:00
Philipp Heckel
18b91cf250 Merge branch 'since-id' into ui 2022-02-26 16:01:31 -05:00
Philipp Heckel
4af9c07577 WIP: Since ID, works 2022-02-26 15:57:10 -05:00
Philipp Heckel
fb90ab480a Action bar fixes 2022-02-26 14:36:23 -05:00
Philipp Heckel
d705d3c3b1 Fix action bar 2022-02-26 14:22:21 -05:00
Philipp Heckel
ee743a7b01 TODOs 2022-02-26 11:51:45 -05:00
Philipp Heckel
e422c2c479 Poll on page refresh; validate subscribe dialog properly; avoid save-races 2022-02-26 11:45:39 -05:00
Philipp Heckel
aa79fe2861 Desktop notifications 2022-02-26 10:14:43 -05:00
Philipp Heckel
530f55c234 Fully support auth in Web UI; persist users in localStorage (for now); add ugly ?auth=... param 2022-02-25 23:25:04 -05:00
Philipp Heckel
6d343c0f1a Login page of "subscribe dialog", still WIP, but looking nice 2022-02-25 16:07:25 -05:00
Philipp Heckel
1599793de2 WIP: Auth 2022-02-25 13:40:03 -05:00
Philipp Heckel
42016f48ff Move things around 2022-02-25 12:46:22 -05:00
Philipp Heckel
f9e22dcaa9 Allow deleting individual notifications 2022-02-25 10:23:04 -05:00
Philipp Heckel
703f94a25f Refactor to responsive drawer 2022-02-24 20:18:46 -05:00
Philipp Heckel
0958c1d527 Re-add persistence 2022-02-24 15:17:47 -05:00
Philipp Heckel
fef46823eb Dedup without keeping deleted array 2022-02-24 14:53:45 -05:00
Philipp Heckel
48523a2269 Emojis, formatting, clear all 2022-02-24 12:26:07 -05:00
Philipp Heckel
202c4ac4b3 Do not fetch old messages on old connecting to avoid douple rendering 2022-02-24 10:30:58 -05:00
Philipp Heckel
1536201e9a Reconnect on failure, with backoff; Deduping notifications 2022-02-24 09:52:49 -05:00
Philipp Heckel
3fac1c3432 Refactor to make it more like the Android app 2022-02-23 20:30:12 -05:00
Philipp Heckel
415ab57749 Poll on subscribe; test message 2022-02-22 23:22:30 -05:00
Philipp Heckel
c57fac283e Unsubscribe 2022-02-22 22:10:50 -05:00
Philipp C. Heckel
2eff8d6b47 Merge pull request #150 from rogeliodh/patch-1
add watchtower/shoutrrr examples
2022-02-21 17:26:53 -05:00
Rogelio Domínguez Hernández
40be2a9153 add watchtower/shoutrrr examples 2022-02-21 16:21:42 -06:00
Philipp Heckel
4ba23390b5 Settings icon 2022-02-21 16:24:13 -05:00
Philipp Heckel
dd1a85e733 Awful use of localstorage 2022-02-20 20:04:03 -05:00
Philipp Heckel
c6c3caec39 Restructure 2022-02-20 16:55:55 -05:00
Philipp Heckel
8c0f3b2304 Add dialog 2022-02-19 22:26:58 -05:00
Philipp Heckel
c859f866b8 Move to dashboard theme 2022-02-19 19:48:33 -05:00
Philipp Heckel
b497063af4 Make topics clickable, show notifications 2022-02-18 15:47:25 -05:00
Philipp Heckel
1fe598a966 Split stuff 2022-02-18 14:41:01 -05:00
Philipp Heckel
31e7aa24bc Subscription form 2022-02-18 11:07:04 -05:00
Philipp Heckel
4c4e689af4 WIP: React 2022-02-18 09:49:51 -05:00
Philipp C. Heckel
43326be637 Merge pull request #148 from lrabane/cli-auth
Add authentification support for subscribing with CLI
2022-02-17 15:25:18 -05:00
lrabane
7e1a71b694 Add docs for auth support with CLI 2022-02-17 20:38:48 +01:00
lrabane
b89c18e83d Add support for auth in client config 2022-02-17 20:38:33 +01:00
lrabane
f4f5edb230 Add auth support for subscribing 2022-02-17 19:13:21 +01:00
Philipp Heckel
ce9e9f3e0d Add test for IP exempt setting 2022-02-14 19:23:57 -05:00
Philipp Heckel
da4cf04594 Compress binary with upx, closes #137 2022-02-14 19:12:50 -05:00
Philipp Heckel
0677b3bd7e Re-add OPTIONS: 2022-02-14 17:08:26 -05:00
Philipp Heckel
eed233a793 Double requests/sec limit; update docs 2022-02-14 17:07:17 -05:00
Philipp Heckel
2ad0802b65 Rate limit exemption; relates to #144 2022-02-14 16:09:59 -05:00
Philipp Heckel
0df8aa9a5d Replace \\n with \n in X-Message header 2022-02-13 09:23:55 -05:00
Philipp Heckel
d3f71f9d0a Bump version 2022-02-09 19:22:46 -05:00
Philipp Heckel
8187b49599 Fixed ARMv8 Docker build 2022-02-09 19:20:30 -05:00
Philipp Heckel
2188643387 Update docs 2022-02-09 16:15:48 -05:00
Philipp Heckel
344031b575 Reduce bcrypt cost to 10 2022-02-07 22:09:31 -05:00
Philipp Heckel
a320093cb8 Bump version 2022-02-03 22:57:16 -05:00
Philipp Heckel
7fb7ba2fa5 Merge branch 'main' of github.com:binwiederhier/ntfy into main 2022-02-03 22:33:30 -05:00
Philipp Heckel
3902599c52 More tests; enough! 2022-02-03 22:26:22 -05:00
Philipp Heckel
4972407145 More polishing, more docs; the only thing left are tests for access.go 2022-02-03 20:20:50 -05:00
Philipp Heckel
d714af43c9 More docs, more tests, more docs 2022-02-03 20:07:23 -05:00
Philipp Heckel
29c2fc5472 CLI tests for 'ntfy user' 2022-02-03 16:10:15 -05:00
Philipp Heckel
1c9766b8fd More docs 2022-02-03 13:40:19 -05:00
Philipp Heckel
68351230f3 Docs docs docs docs docs 2022-02-02 22:46:48 -05:00
Philipp Heckel
0ad85262c1 More docs; this will never end 2022-02-02 19:26:17 -05:00
Philipp Heckel
1552d8103e More docs docs docs 2022-02-01 23:39:57 -05:00
Philipp Heckel
c3a2331b59 WIP: Docs 2022-02-01 21:07:42 -05:00
Philipp Heckel
5cf92c55c6 Docs and minor improvements to "ntfy access" 2022-02-01 16:40:33 -05:00
Philipp Heckel
e56eb0c178 Allow 'ntfy access --reset' 2022-02-01 12:23:11 -05:00
Philipp Heckel
44bc13eb2c Update deps 2022-02-01 12:11:52 -05:00
Philipp Heckel
a77f89d302 Test Firebase stuff 2022-02-01 08:47:01 -05:00
Philipp Heckel
b1bbbf0103 Simplify again 2022-01-31 20:12:47 -05:00
Philipp Heckel
c2f31b9c9f Move firebase to seperate file 2022-01-31 20:06:48 -05:00
Philipp Heckel
198e2cfd90 Do not forward messages to Firebase if topic is not world-readable 2022-01-31 19:33:22 -05:00
Philipp Heckel
936e95fd9e Rename Topic to TopicPattern in Grant 2022-01-31 11:47:30 -05:00
Philipp Heckel
c56814e7da Add wildcard access control 2022-01-31 11:44:58 -05:00
Philipp Heckel
631ade5430 Support multiple topics in auth 2022-01-27 12:49:05 -05:00
Philipp Heckel
e61a0c2f78 Add /auth endpoint and tests 2022-01-25 23:04:09 -05:00
Philipp Heckel
89957e7058 Docblocking 2022-01-25 22:30:53 -05:00
Philipp Heckel
26dde0f286 Fix test 2022-01-25 22:04:54 -05:00
Philipp Heckel
7d9f687768 Tests 2022-01-25 21:57:28 -05:00
Philipp C. Heckel
0a0fea1c2f Merge pull request #118 from xnumad/patch-1
Docs clarification
2022-01-24 18:35:15 -05:00
xnumad
cb4970be59 Docs clarification 2022-01-25 00:29:25 +01:00
Philipp Heckel
460162737a More auth 2022-01-24 00:54:28 -05:00
Philipp Heckel
393f95aeac Auth CLI, continued 2022-01-23 23:02:39 -05:00
Philipp Heckel
03a4e3e8e9 More CLI for access control 2022-01-23 15:30:30 -05:00
Philipp Heckel
243d549975 Command help 2022-01-23 01:00:38 -05:00
Philipp Heckel
e309775ac1 More auth CLi 2022-01-23 00:54:18 -05:00
Philipp Heckel
f388fd9c90 Move to package 2022-01-23 00:02:16 -05:00
Philipp Heckel
b908f07355 . 2022-01-22 23:07:55 -05:00
Philipp Heckel
1287594505 Simplify tables 2022-01-22 23:01:20 -05:00
Philipp Heckel
86b20e8ccd All the auth things are working now 2022-01-22 14:47:27 -05:00
Philipp Heckel
2181227a6e WIP: Auth in 80 lines of code :-) 2022-01-21 22:22:27 -05:00
Philipp Heckel
aab705f4a4 Fix baseURL check 2022-01-21 14:17:59 -05:00
Philipp Heckel
8af9a97518 Update server.yml docs 2022-01-20 20:40:12 -05:00
Philipp Heckel
9fac75b831 Check base URL format 2022-01-20 13:20:04 -05:00
Philipp Heckel
c83b5c6e73 Update another instance of a broken link; related to #106 2022-01-18 11:59:13 -05:00
Philipp Heckel
a75326ff69 Fixed docs link; closes #106 2022-01-18 11:55:39 -05:00
Philipp Heckel
df6611e8de Store encoding in cache database 2022-01-17 19:14:51 -05:00
Philipp Heckel
7e817f408c Send encoding in Firebase (even though that doesn't make a ton of sense right now) 2022-01-17 13:49:02 -05:00
Philipp Heckel
4ceb058a40 Handle binary messages for UnifiedPush 2022-01-17 13:28:07 -05:00
Philipp Heckel
4710812c24 Bump version 2022-01-16 21:57:15 -05:00
Philipp Heckel
eb37c47ff5 Typo 2022-01-16 21:56:26 -05:00
Philipp Heckel
e80c2c1a57 Merge branch 'main' of github.com:binwiederhier/ntfy into main 2022-01-16 17:55:18 -05:00
Philipp Heckel
75f8607d75 Bail out early if HTTP is spoken to /ws endpoint 2022-01-16 17:54:15 -05:00
Philipp C. Heckel
828a286809 Merge pull request #95 from binwiederhier/websockets
Websockets; working
2022-01-16 00:21:31 -05:00
Philipp Heckel
9b0e7eedb2 Fix data race 2022-01-16 00:07:32 -05:00
Philipp Heckel
df4585af6b Attachment comment 2022-01-15 23:53:40 -05:00
Philipp Heckel
91d40dcc91 Docs 2022-01-15 23:52:21 -05:00
Philipp Heckel
2b6363474e Combine things, move stuff 2022-01-15 23:17:46 -05:00
Philipp Heckel
707c58a120 Do not print ugly WS error; tests 2022-01-15 22:33:35 -05:00
Philipp Heckel
846ee0fb2d Websockets; working 2022-01-15 13:23:35 -05:00
Philipp Heckel
cdc9c0d62c Docs 2022-01-14 21:22:43 -05:00
Philipp Heckel
b079cb99a4 Merge branch 'main' into unix-socket 2022-01-14 20:16:12 -05:00
Philipp Heckel
a75f74b471 Bump version; update docs 2022-01-14 12:23:58 -05:00
Philipp Heckel
e50779664d Remove peaking, addresses #93 2022-01-14 12:13:14 -05:00
Philipp Heckel
51583f5d28 Attachments dir in package 2022-01-13 17:16:04 -05:00
Philipp Heckel
c3170e1eb6 Bump version 2022-01-13 16:14:35 -05:00
Philipp C. Heckel
bc16ef8480 Merge pull request #82 from binwiederhier/attachments
Attachments
2022-01-13 15:47:58 -05:00
Philipp Heckel
6a7b20e4e3 Docs 2022-01-13 15:47:34 -05:00
Philipp Heckel
034c81288c Docs docs docs 2022-01-13 15:17:30 -05:00
Philipp Heckel
762333c28f Docs docs docs 2022-01-13 00:08:26 -05:00
Philipp Heckel
38b28f9bf4 CLI; docs docs docs 2022-01-12 21:24:48 -05:00
Philipp Heckel
aa94410308 Daily traffic limit 2022-01-12 18:52:07 -05:00
Philipp Heckel
c76e55a1c8 Making RateLimiter and FixedLimiter, so they can both work with LimitWriter 2022-01-12 17:03:28 -05:00
Philipp Heckel
f6b9ebb693 Lots of tests 2022-01-12 11:05:04 -05:00
Philipp Heckel
68a324c206 Fail early for too-large attachments 2022-01-11 12:58:11 -05:00
Philipp Heckel
0b0595384e Listen on Unix socket 2022-01-10 16:28:13 -05:00
Philipp Heckel
289a6fdd0f Add attachment expiry option 2022-01-10 15:36:12 -05:00
Philipp Heckel
e8cb9e7fde Better mime type probing 2022-01-10 13:38:51 -05:00
Philipp Heckel
b5183612be Fix attachment pruning logging; .mp4 extension issue 2022-01-09 22:06:31 -05:00
Philipp Heckel
44a9509cd6 Properly handle different attachment use cases 2022-01-08 15:47:08 -05:00
Philipp Heckel
cefe276ce5 Tests for fileCache 2022-01-08 12:14:43 -05:00
Philipp Heckel
e7c19a2bad Expire attachments properly 2022-01-07 15:15:33 +01:00
Philipp Heckel
c45a28e6af Attachments limits; working visitor limit 2022-01-07 14:49:28 +01:00
Philipp Heckel
70aefc2e48 Merge branch 'main' into attachments 2022-01-07 12:33:34 +01:00
Philipp Heckel
014b561b29 Merge branch 'main' of github.com:binwiederhier/ntfy into main 2022-01-06 15:03:20 +01:00
Philipp Heckel
f397456703 fail2ban docs 2022-01-06 15:03:07 +01:00
Philipp Heckel
9171e94e5a Fix file extension detection; fix HTTPS port 2022-01-06 14:45:23 +01:00
Philipp Heckel
5eca20469f Attachment size limit 2022-01-06 01:04:56 +01:00
Philipp C. Heckel
5ea2751423 Merge pull request #86 from cmeis/rpm-rhel
Adjust RPM scriptlets to work on RHEL-flavour OSes, too.
2022-01-05 21:22:48 +01:00
Christian Meis
814690e66b One more correction to RPM scriptlets 2022-01-05 16:00:27 +01:00
Christian Meis
9b2ddabca9 Corrected RPM scriptlets to actually restart the systemd service on a package upgrade. 2022-01-05 15:47:24 +01:00
Christian Meis
8f7b61291f Add quotes 2022-01-05 14:44:02 +01:00
Christian Meis
523e037900 Switch from parentheses to nested if statements for the RPM scriptlets. 2022-01-05 14:43:25 +01:00
Christian Meis
88586c8f86 Adjust RPM scriptlets to work on RHEL-flavour OSes, too. 2022-01-05 13:32:15 +01:00
Philipp Heckel
24eb27d41c Merge branch 'main' into attachments 2022-01-05 00:25:49 +01:00
Philipp Heckel
7a7e7ca359 Add docs for click action 2022-01-05 00:11:36 +01:00
Philipp Heckel
41c1189fee Persist "click" 2022-01-04 23:40:41 +01:00
Philipp Heckel
2e40b895a7 Fix message truncation, relates to #84 2022-01-04 21:09:47 +01:00
Philipp Heckel
76d102f964 Add "truncated" flag to let Android app know 2022-01-04 20:53:32 +01:00
Philipp Heckel
807d2b0d9d Truncate FCM messages if they are too long; This was trickier than expected; relates to #84 2022-01-04 20:43:37 +01:00
Philipp Heckel
b4f71ce01a Merge branch 'main' of github.com:binwiederhier/ntfy into main 2022-01-04 20:00:59 +01:00
Philipp Heckel
722c579db0 Increase FCM priority for ntfy priority high and max, closes #70 2022-01-04 19:59:54 +01:00
Philipp Heckel
2930c4ff62 Preview URL 2022-01-04 19:45:29 +01:00
Philipp Heckel
38788bb2e9 WIP: attachments 2022-01-04 00:55:08 +01:00
Philipp C. Heckel
75bef92417 Update README.md 2022-01-03 11:49:39 -05:00
Philipp Heckel
eb5b86ffe2 WIP: Attachments 2022-01-02 23:56:12 +01:00
Philipp Heckel
09515f26df Update nginx config 2022-01-01 23:08:58 +01:00
Philipp Heckel
8a3ee987a8 Bump version 2022-01-01 22:49:08 +01:00
Philipp Heckel
47b491b6e2 55s keepalive, 65s did not work; unsure why 2022-01-01 22:48:17 +01:00
Philipp Heckel
91ad69dd00 Bump keepalive interval to 65s after testing 2022-01-01 22:21:47 +01:00
Philipp Heckel
521aad7db5 Increase keepalive duration to 55s 2022-01-01 22:11:09 +01:00
Philipp Heckel
fe2988bb38 Reduce Firebase control channel keepalive message 2022-01-01 22:08:55 +01:00
Philipp Heckel
65a53c1100 Bump version 2022-01-01 21:43:13 +01:00
Philipp Heckel
a53f18ca7d Docs for UnifiedPush, update docs for message limit 2022-01-01 17:45:18 +01:00
Philipp Heckel
595ea87465 Switch VARCHAR(N) to TEXT, as they are equivalent in SQLite 2021-12-31 16:19:41 +01:00
Philipp Heckel
7b37141e07 Increase message size limit to 4096 2021-12-31 16:12:53 +01:00
Philipp Heckel
1fd327325f Merge branch 'main' of github.com:binwiederhier/ntfy into main 2021-12-30 01:15:20 +01:00
Philipp Heckel
96ad49f675 Make build-simple work again 2021-12-30 01:15:02 +01:00
Philipp C. Heckel
35b2ca51d8 Merge pull request #74 from ramonsnir/patch-2
Update Docker installation with a Dockerfile example
2021-12-30 00:46:25 +01:00
Ramon Snir
76a28b4e8b Update Docker installation with a Dockerfile example 2021-12-29 18:25:17 -05:00
Philipp Heckel
9752bd7c30 Fix missing SMTP config options in docs 2021-12-29 14:18:38 +01:00
Philipp Heckel
46c0039a16 Bump version 2021-12-28 17:42:31 +01:00
Philipp Heckel
d5497908bb Merge branch 'main' of github.com:binwiederhier/ntfy into main 2021-12-28 17:40:53 +01:00
Philipp Heckel
dac88391c1 Docs docs docs 2021-12-28 17:36:12 +01:00
Philipp Heckel
a46a520bca Fix tests 2021-12-28 01:48:58 +01:00
Philipp Heckel
04719f8dee Flip title and message if message is empty 2021-12-28 01:41:00 +01:00
Philipp Heckel
113053a9e3 Fix encoding issues 2021-12-28 01:26:20 +01:00
Philipp Heckel
7cfe909644 CLI arguments 2021-12-27 22:27:01 +01:00
Philipp Heckel
01a1d981cf fix nil pointer 2021-12-27 22:18:15 +01:00
Philipp Heckel
e7f8fc93e4 Working prefix 2021-12-27 22:06:40 +01:00
Philipp C. Heckel
b45ca6f2c0 Merge pull request #68 from arjan-s/archlinux_instructions
Add Arch Linux installation instructions
2021-12-27 17:46:17 +01:00
Arjan Schrijver
be17294dc2 Add Arch Linux installation instructions 2021-12-27 17:39:42 +01:00
Philipp Heckel
7eaa92cb20 WIP 2021-12-27 16:39:28 +01:00
Philipp Heckel
3001e57bcc WIP: mail publish 2021-12-27 15:48:09 +01:00
Philipp Heckel
43a2acb756 Typo 2021-12-27 00:37:18 +01:00
Philipp Heckel
bcc424f2aa Oops 2021-12-26 14:36:38 +01:00
Philipp Heckel
ec7e58a6a2 Fix santa bug, email subject encoding, closes #65 2021-12-26 14:34:25 +01:00
Philipp C. Heckel
9a0f1f22b8 Merge pull request #64 from binwiederhier/up
WIP: unified push
2021-12-26 14:03:45 +01:00
Philipp Heckel
d6762276f5 Test 2021-12-25 22:07:55 +01:00
Philipp Heckel
41514cd557 Merge branch 'main' into up 2021-12-25 21:49:47 +01:00
Karmanyaah Malhotra
63a29380a9 up testing 2021-12-25 10:26:18 -06:00
Philipp Heckel
eeb378cfdc Change error JSON 2021-12-25 15:21:41 +01:00
Philipp Heckel
7a23779d07 JSON API errors 2021-12-25 15:15:05 +01:00
Philipp Heckel
29628a66a6 Initial 2021-12-25 11:56:02 +01:00
Philipp Heckel
020c058805 Bump version 2021-12-25 11:22:27 +01:00
Philipp Heckel
8a625ef786 Docs, and fixing tests 2021-12-25 10:35:08 +01:00
Philipp Heckel
3bc8ff0104 Docs 2021-12-25 00:57:02 +01:00
Philipp Heckel
11b5ac49c0 Fully working email feature 2021-12-25 00:13:09 +01:00
Philipp Heckel
f553cdb282 Continued e-mail support 2021-12-24 15:01:29 +01:00
Philipp Heckel
6b46eb46e2 A mutex in a test struct ... 2021-12-24 00:10:22 +01:00
Philipp Heckel
7280ae1ebc Email rate limiting + tests 2021-12-24 00:03:04 +01:00
Philipp Heckel
873c57b3d8 Send emails 2021-12-23 21:04:17 +01:00
Philipp Heckel
c8c53eed07 Bump version 2021-12-23 00:18:13 +01:00
Philipp Heckel
6779d9dd1f Add NTFY_RAW 2021-12-23 00:16:28 +01:00
Philipp Heckel
85939618c8 Fix Poll(); yeyy tests 2021-12-22 23:45:19 +01:00
Philipp Heckel
fe5734d9f0 Tests for cliet package 2021-12-22 23:20:43 +01:00
Philipp Heckel
6a7e9071b6 Tests, client tests WIP 2021-12-22 14:17:50 +01:00
Philipp Heckel
68d881291c Docs, LoadConfig, config test 2021-12-22 13:46:17 +01:00
Philipp Heckel
66c749d5f0 Include image file, docs 2021-12-22 10:21:59 +01:00
Philipp Heckel
534fca0d3b Fix data race 2021-12-22 10:04:59 +01:00
Philipp Heckel
b6120cf6d7 Message filtering tests 2021-12-22 09:44:16 +01:00
Philipp Heckel
09bf13bd70 Query filters only apply to message events 2021-12-21 21:29:37 +01:00
Philipp Heckel
9315829bc4 Query filters 2021-12-21 21:22:27 +01:00
Philipp Heckel
85b4abde6c Docs 2021-12-20 20:46:51 -05:00
Philipp Heckel
edb6b0cf06 More docs, still WIP 2021-12-19 23:04:55 -05:00
Philipp Heckel
f24855ca9a Documentation, fix test, return JSON on publish, add --quiet flag for publish 2021-12-19 21:01:49 -05:00
Philipp Heckel
ddd5ce2c21 Start CLI tests 2021-12-19 18:32:16 -05:00
Philipp Heckel
e3dfea1991 Docblocks, a handful of tests, but not enough 2021-12-19 14:27:26 -05:00
Philipp Heckel
fa9d6444f5 Move config files and folders 2021-12-18 22:02:36 -05:00
Philipp Heckel
2c1989beb0 I think we're getting there 2021-12-18 16:12:36 -05:00
Philipp Heckel
f266afa1de WIP CLI 2021-12-18 14:43:27 -05:00
Philipp Heckel
5639cf7a0f Docs 2021-12-17 22:38:29 -05:00
Philipp Heckel
a1f513f6a5 WIP CLI 2021-12-17 09:32:59 -05:00
Philipp Heckel
1e8421e8ce WIP: CLI, relates to #46 2021-12-16 20:33:01 -05:00
Philipp Heckel
4346f55b29 Python examples; that's all; closes #50 2021-12-15 20:37:21 -05:00
Philipp Heckel
92f48fbbea Bump version 2021-12-15 19:24:38 -05:00
Philipp Heckel
200dd25ffa Add limitations section 2021-12-15 19:23:44 -05:00
Philipp Heckel
534b93e142 Webhooks (#55), more tests (#35) and python examples (#50) 2021-12-15 16:12:40 -05:00
Philipp Heckel
02f8a32b46 GET-based send/trigger, relates to #55 2021-12-15 09:41:55 -05:00
Philipp Heckel
9cb48dbb60 Move background tasks to functions 2021-12-15 09:13:16 -05:00
Philipp Heckel
bd09fb4c54 Bump version 2021-12-13 22:35:54 -05:00
Philipp Heckel
63206f8581 Firebase keepalive, supports #56 2021-12-13 22:30:28 -05:00
Philipp Heckel
de0c41ec3b Resize/compress images 2021-12-12 20:37:38 -05:00
Philipp Heckel
eaefb436d6 More docs 2021-12-12 14:26:24 -05:00
Philipp Heckel
5843de5dfc Documentation for the intent stuff 2021-12-12 09:26:35 -05:00
Philipp Heckel
6abda93a14 Bump version 2021-12-11 07:30:48 -05:00
Philipp C. Heckel
281faeff3b Merge pull request #48 from binwiederhier/delay
WIP: Add 'At:'/'In:' headers to support scheduled messages
2021-12-11 00:14:17 -05:00
Philipp Heckel
01d21165e9 Docs docs docs 2021-12-11 00:06:25 -05:00
Philipp Heckel
e8688fed4b Lots more tests 2021-12-10 22:57:01 -05:00
Philipp Heckel
5ef83a7ba0 Test DB migration 2021-12-10 20:28:56 -05:00
Philipp Heckel
06b4d9c83b Natural language 2021-12-10 19:59:51 -05:00
Philipp Heckel
196c86d12b WIP_ Add 'At:'/'Delay:' headers to support scheduled messages 2021-12-10 11:31:42 -05:00
Philipp C. Heckel
b45f95e392 Update README.md 2021-12-09 19:44:59 -05:00
Philipp Heckel
aacdda94e1 chmod 600 for cache file 2021-12-09 14:07:09 -05:00
Philipp Heckel
ee34770610 Merge branch 'main' of github.com:binwiederhier/ntfy into main 2021-12-09 14:04:57 -05:00
Philipp Heckel
b950e9da95 Fix #47 issue with 1.5.2 2021-12-09 14:04:41 -05:00
Philipp C. Heckel
3ce333819f Update README.md 2021-12-09 13:53:47 -05:00
Philipp Heckel
1f60791bad Bump version 2021-12-09 13:12:57 -05:00
Philipp Heckel
f966b2f9d7 Add 'Firebase: no' header, closes #42 2021-12-09 12:15:17 -05:00
Philipp Heckel
d6fbccab55 Add 'Cache: no' header, closes #41 2021-12-09 10:23:17 -05:00
Philipp Heckel
d5be5d3e8c Test schema migration 2021-12-08 23:24:09 -05:00
Philipp Heckel
98c1ab9e86 Finish cache tests 2021-12-08 22:57:31 -05:00
Philipp Heckel
b437a87266 Disallow subscribing to /docs 2021-12-08 22:13:59 -05:00
Philipp Heckel
9a56c24dbe Make ntfy run as ntfy user/group, closes #38 2021-12-08 22:08:44 -05:00
Philipp Heckel
808b63eaa1 Merge branch 'main' of github.com:binwiederhier/ntfy into main 2021-12-08 16:09:00 -05:00
Philipp Heckel
3a009eac9d More unit tests 2021-12-08 16:08:50 -05:00
Philipp C. Heckel
e2c419c021 Update README.md 2021-12-08 15:11:35 -05:00
Philipp Heckel
6fb4bdc001 Docs on "scaling" 2021-12-08 11:53:59 -05:00
Philipp Heckel
802ef17cb4 Fix data races 2021-12-07 16:03:01 -05:00
Philipp Heckel
c9f1b02251 Unnecessary check around range 2021-12-07 15:43:51 -05:00
Philipp Heckel
40fbce07db Test for simple pub sub 2021-12-07 15:39:42 -05:00
Philipp Heckel
aa9764848a Enable codecov.io 2021-12-07 14:50:46 -05:00
Philipp Heckel
fc16b0531a Open and keepalive tests 2021-12-07 14:46:10 -05:00
Philipp Heckel
94b70fbcb9 First server tests 2021-12-07 14:06:33 -05:00
Philipp Heckel
ab3cc47e27 Move python deps to requirements.txt 2021-12-07 13:37:01 -05:00
Philipp Heckel
a3ce12585b Upgrade jinja 2021-12-07 13:27:15 -05:00
Philipp Heckel
829e8f6ea6 pip list 2021-12-07 13:24:50 -05:00
Philipp Heckel
fd71589f60 More test; begin test infra stuff 2021-12-07 12:23:42 -05:00
Philipp Heckel
37fafd09e7 GitHub workflow 2021-12-07 11:50:48 -05:00
Philipp Heckel
be50af0a7a Begin unit tests, relates to #35 2021-12-07 11:45:15 -05:00
Philipp Heckel
da8f90d388 gofmt 2021-12-07 10:38:58 -05:00
Philipp Heckel
f1fac8da75 Proxy docs 2021-12-06 20:05:06 -05:00
Philipp Heckel
faa7de9f37 Drop shadow color 2021-12-06 19:04:45 -05:00
Philipp Heckel
d0d1f9e5c7 Docs for "tuning for scale" 2021-12-06 16:43:06 -05:00
Philipp Heckel
4cc53633d8 Remove check tags thing 2021-12-05 16:31:06 -05:00
Philipp Heckel
4a3a0de198 Bump version 2021-12-05 16:28:12 -05:00
Philipp Heckel
048d17c365 Merge branch 'main' of github.com:binwiederhier/ntfy into docs 2021-12-05 15:57:55 -05:00
Philipp Heckel
0f495b881a Almost read to publish 2021-12-05 15:57:37 -05:00
Philipp C. Heckel
3daa590732 Update LICENSE.GPLv2 2021-12-05 12:52:20 -05:00
Philipp C. Heckel
dadb6419ee Update LICENSE 2021-12-05 12:51:08 -05:00
Philipp Heckel
4fcb3891a2 More docs; omg why is this taking so long 2021-12-05 07:31:05 -05:00
Philipp Heckel
0f57bd147c Docs docs docs 2021-12-04 23:37:50 -05:00
Philipp Heckel
cd80c7ebd8 Strip down main page more 2021-12-04 20:23:29 -05:00
Philipp Heckel
069d66263d Merge branch 'main' into docs 2021-12-04 15:33:04 -05:00
Philipp Heckel
6fbbb0c7b5 Properly handle systemd start/stop during Debian package install, closes #30 2021-12-04 15:32:21 -05:00
Philipp Heckel
90ba341323 Remove print 2021-12-04 14:33:55 -05:00
Philipp Heckel
acb0bdf619 More docs 2021-12-04 08:56:46 -05:00
Philipp Heckel
246061829b Continued docs 2021-12-03 20:38:21 -05:00
Philipp Heckel
7ba268887b Continuation of the docs 2021-12-02 17:27:31 -05:00
Philipp Heckel
29f2196376 Emoji reference and such 2021-12-02 12:04:22 -05:00
Philipp Heckel
62f4d9963e Merge branch 'main' into docs 2021-12-02 08:53:11 -05:00
Philipp Heckel
07a1fe3acb Add TLS/HTTPS 2021-12-02 08:52:48 -05:00
Philipp Heckel
1c8747f06f SSL 2021-12-02 08:33:39 -05:00
Philipp Heckel
ae7bfb2c97 WIP: Docs 2021-12-01 23:08:12 -05:00
Philipp Heckel
1e7ae885b4 Update examples 2021-11-29 15:34:29 -05:00
Philipp Heckel
9d37217eba Bump version 2021-11-29 11:52:35 -05:00
Philipp Heckel
e3debf4315 Count capitalization 2021-11-29 11:48:34 -05:00
Philipp Heckel
ccccae9aad Refine embedfs 2021-11-29 11:10:12 -05:00
Philipp Heckel
28f4e1e55e Reduce emoji.js size 2021-11-29 10:57:18 -05:00
Philipp Heckel
8616be12a2 Emojis in notifications; server caching 2021-11-29 09:34:43 -05:00
Philipp Heckel
052ab7d411 Emoji support in Web UI 2021-11-28 19:58:49 -05:00
Philipp Heckel
6ca63cc0e9 Merge branch 'main' of github.com:binwiederhier/ntfy into main 2021-11-28 19:03:53 -05:00
Philipp Heckel
f0edf0610e Add priorities and tags to web UI 2021-11-28 19:03:15 -05:00
Philipp C. Heckel
ee5b5c6edd Merge pull request #26 from Copephobia/css-fix
Fix topic div being hidden on iOS devices
2021-11-28 15:53:59 -05:00
Copephobia
4663c3b724 Fix topic div being hidden on iOS devices 2021-11-28 15:44:25 -05:00
Philipp Heckel
1193ddc65f Merge branch 'main' of github.com:binwiederhier/ntfy into main 2021-11-28 14:08:01 -05:00
Philipp Heckel
d4330e86ac Add title, priority, tags to cache; add schema migration 2021-11-28 14:07:29 -05:00
Philipp Heckel
1b8ebab5f3 Priorities, titles, tags 2021-11-27 16:12:08 -05:00
Philipp C. Heckel
b6af28de33 Update README.md 2021-11-26 17:09:41 -05:00
Philipp C. Heckel
e327e52766 Update README.md 2021-11-25 08:58:26 -05:00
Philipp Heckel
7b8185c2a7 Readme 2021-11-23 22:00:32 -05:00
Philipp Heckel
71af1af001 New logo and header 2021-11-23 20:22:09 -05:00
Philipp Heckel
9af64bf3dd Favicon 2021-11-23 12:02:16 -05:00
Philipp Heckel
093154fa6c Only build armv7, not armv6 anymore 2021-11-22 16:09:54 -05:00
Philipp Heckel
c247984ca9 Merge branch 'main' of github.com:binwiederhier/ntfy into main 2021-11-22 10:53:03 -05:00
Philipp Heckel
2a05715107 Multi-platform Docker images 2021-11-22 10:46:19 -05:00
Philipp C. Heckel
cb69f18c39 Merge pull request #12 from nathanaelhoun/custom-url-selfhosted
Use custom url for self-hosted
2021-11-22 08:42:56 -05:00
Nathanaël Houn
fde5fda635 Use custom url in case of self-hosted 2021-11-22 14:30:09 +01:00
Philipp Heckel
21990398c6 Bump readme 2021-11-20 20:31:04 -05:00
Philipp Heckel
bbbab8d2ef Properly statically compile, without warnings; netgo,osusergo 2021-11-20 20:27:17 -05:00
Philipp Heckel
ad057c12c0 Statically linking go-sqlite3 2021-11-20 20:18:40 -05:00
Philipp Heckel
e3bc92e158 Update readme 2021-11-20 16:02:05 -05:00
Philipp Heckel
45f94ead8b Merge branch 'main' of github.com:binwiederhier/ntfy into main 2021-11-20 15:58:31 -05:00
Philipp Heckel
a56e1bf36d This time really 2021-11-20 15:55:30 -05:00
Philipp Heckel
fa8a7ce43e ARM builds, hopefully working 2021-11-20 15:43:15 -05:00
Philipp C. Heckel
e4c34a20e6 Update README.md 2021-11-19 20:21:16 -05:00
Philipp Heckel
6a0dd08375 GPLv2 2021-11-19 20:19:18 -05:00
Philipp Heckel
0d7039d034 Readme 2021-11-18 14:45:37 -05:00
Philipp Heckel
8280e5b0ad Do not allow empty messages 2021-11-18 14:21:00 -05:00
Philipp Heckel
ae97fbe025 Fix typo 2021-11-18 13:14:55 -05:00
Philipp Heckel
6d7fec5337 Examples and anchors on website 2021-11-18 09:22:33 -05:00
Philipp Heckel
ba2f6e08cd Anchors in web UI 2021-11-17 20:50:47 -05:00
Philipp Heckel
ffe0c72a5a Bump version 2021-11-16 13:11:31 -05:00
Philipp Heckel
52136030be Subscribe to more than one topic 2021-11-15 07:56:58 -05:00
Philipp Heckel
a481f4c448 Theme color in Chrome Android 2021-11-12 14:43:32 -05:00
Philipp Heckel
9b171dee8b Merge branch 'main' of github.com:binwiederhier/ntfy into main 2021-11-09 14:48:48 -05:00
Philipp Heckel
c0ee174b13 Make publishing asynchronous 2021-11-09 14:48:25 -05:00
Philipp C. Heckel
fff535ca1a Update README.md 2021-11-09 10:52:11 -05:00
Philipp C. Heckel
26390b9ad1 Update README.md 2021-11-09 10:51:21 -05:00
Philipp Heckel
cc752cf797 Screenshots 2021-11-09 10:46:47 -05:00
Philipp Heckel
4d48c5dc34 Examples 2021-11-08 20:15:13 -05:00
Philipp Heckel
b9b53bcdf0 Fix Chrome/Firefox inconsistencies with sorting 2021-11-08 10:35:46 -05:00
Philipp Heckel
a1385f6785 Update readme 2021-11-08 09:48:55 -05:00
Philipp Heckel
d453db89a7 Add since=all; make poll=1 default to since=all 2021-11-08 09:46:31 -05:00
Philipp Heckel
43c9a92748 Detail page in web UI 2021-11-08 09:24:34 -05:00
Philipp Heckel
c01c94c64c Fix content type to add charset 2021-11-07 13:08:03 -05:00
Philipp Heckel
7adb0e4f2f Bump 2021-11-05 13:47:36 -04:00
Philipp Heckel
0170f673bd Fix rate limiting behind proxy, make configurable 2021-11-05 13:46:27 -04:00
Philipp Heckel
86a16e3944 More tweaking 2021-11-04 22:47:29 -04:00
Philipp Heckel
c9124cb5eb Make web ui prettier 2021-11-04 22:32:17 -04:00
Philipp Heckel
644ffa1420 WIP: Web UI improvements 2021-11-04 10:55:34 -04:00
Philipp Heckel
5948f39a53 Add firebase subscriber to topics from cache upon initialization; stricter rate limits 2021-11-03 21:16:07 -04:00
Philipp Heckel
eef85c0955 Merge branch 'main' of github.com:binwiederhier/ntfy into main 2021-11-03 11:46:45 -04:00
Philipp Heckel
60cbf23bcc Fix CGO stuff for sqlite 2021-11-03 11:46:24 -04:00
Philipp C. Heckel
3b5235ed01 Update README.md 2021-11-03 11:38:46 -04:00
Philipp C. Heckel
54366d105f Update README.md 2021-11-03 11:36:09 -04:00
Philipp C. Heckel
5356580fc6 Update README.md 2021-11-03 11:35:53 -04:00
Philipp C. Heckel
6ccadb09dd Update README.md 2021-11-03 11:35:39 -04:00
Philipp Heckel
ecde123f1c readme 2021-11-03 11:34:12 -04:00
Philipp Heckel
56ffa551f3 Merge branch 'main' of github.com:binwiederhier/ntfy into main 2021-11-03 11:33:47 -04:00
Philipp Heckel
30a1ffa7cf Clean up readme 2021-11-03 11:33:34 -04:00
Philipp C. Heckel
17839e634c Update example_desktop_notifications.sh 2021-11-02 21:15:21 -04:00
Philipp Heckel
7b810acfb5 SQLite cache 2021-11-02 21:09:49 -04:00
Philipp Heckel
1c7695c1f3 Merge branch 'main' into cache 2021-11-02 14:24:49 -04:00
Philipp Heckel
0e9fa1c4dc Fix raw endpoint 2021-11-02 14:10:56 -04:00
Philipp Heckel
67922b0ae5 Cache 2021-11-02 14:08:21 -04:00
Philipp Heckel
b775e6dfce Limits 2021-11-01 16:39:40 -04:00
Philipp Heckel
fa7a45902f Subscription limit 2021-11-01 15:21:38 -04:00
Philipp Heckel
5f2bb4f876 Readme 2021-10-30 23:46:50 -04:00
Philipp Heckel
91541f9c69 Add logo and color 2021-10-30 23:46:08 -04:00
Philipp Heckel
9a91312392 Only send Firebase data messages 2021-10-29 15:47:46 -04:00
Philipp Heckel
4b455e0fd8 Config file 2021-10-29 14:03:41 -04:00
Philipp Heckel
b145e693a5 Add firebase support 2021-10-29 13:58:14 -04:00
Philipp Heckel
4677e724ee Re-add "message" as event type 2021-10-29 08:29:27 -04:00
Philipp Heckel
ad17be1683 WIP: Firebase support 2021-10-28 23:50:38 -04:00
Philipp Heckel
28fb63b9ae readme 2021-10-27 14:58:38 -04:00
278 changed files with 77117 additions and 919 deletions

28
.github/workflows/test.yaml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: '1.17.x'
- name: Install node
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Checkout code
uses: actions/checkout@v2
- name: Install dependencies
run: sudo apt update && sudo apt install -y python3-pip curl
- name: Build docs (required for tests)
run: make docs
- name: Build web app (required for tests)
run: make web
- name: Run tests, formatting, vetting and linting
run: make check
- name: Run coverage
run: make coverage
- name: Upload coverage to codecov.io
run: make coverage-upload

6
.gitignore vendored
View File

@@ -1,3 +1,9 @@
dist/
build/
.idea/
server/docs/
server/site/
tools/fbsend/fbsend
playground/
*.iml
node_modules/

View File

@@ -1,20 +1,65 @@
before:
hooks:
- go mod download
- go mod tidy
builds:
- binary: ntfy
-
id: ntfy_amd64
binary: ntfy
env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
goarch:
- amd64
- CGO_ENABLED=1 # required for go-sqlite3
tags: [sqlite_omit_load_extension,osusergo,netgo]
ldflags:
- "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
goos: [linux]
goarch: [amd64]
hooks:
post:
- upx "{{ .Path }}" # apt install upx
-
id: ntfy_armv6
binary: ntfy
env:
- CGO_ENABLED=1 # required for go-sqlite3
- CC=arm-linux-gnueabi-gcc # apt install gcc-arm-linux-gnueabi
tags: [sqlite_omit_load_extension,osusergo,netgo]
ldflags:
- "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
goos: [linux]
goarch: [arm]
goarm: [6]
# No "upx", since it causes random core dumps, see
# https://github.com/binwiederhier/ntfy/issues/191#issuecomment-1083406546
-
id: ntfy_armv7
binary: ntfy
env:
- CGO_ENABLED=1 # required for go-sqlite3
- CC=arm-linux-gnueabi-gcc # apt install gcc-arm-linux-gnueabi
tags: [sqlite_omit_load_extension,osusergo,netgo]
ldflags:
- "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
goos: [linux]
goarch: [arm]
goarm: [7]
# No "upx", since it causes random core dumps, see
# https://github.com/binwiederhier/ntfy/issues/191#issuecomment-1083406546
-
id: ntfy_arm64
binary: ntfy
env:
- CGO_ENABLED=1 # required for go-sqlite3
- CC=aarch64-linux-gnu-gcc # apt install gcc-aarch64-linux-gnu
tags: [sqlite_omit_load_extension,osusergo,netgo]
ldflags:
- "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
goos: [linux]
goarch: [arm64]
# No "upx", since it causes random core dumps, see
# https://github.com/binwiederhier/ntfy/issues/191#issuecomment-1083406546
nfpms:
-
package_name: ntfy
file_name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Arch }}"
homepage: https://heckel.io/ntfy
maintainer: Philipp C. Heckel <philipp.heckel@gmail.com>
description: Simple pub-sub notification service
@@ -24,12 +69,28 @@ nfpms:
- rpm
bindir: /usr/bin
contents:
- src: config/config.yml
dst: /etc/ntfy/config.yml
type: config
- src: config/ntfy.service
- src: server/server.yml
dst: /etc/ntfy/server.yml
type: "config|noreplace"
- src: server/ntfy.service
dst: /lib/systemd/system/ntfy.service
- src: client/client.yml
dst: /etc/ntfy/client.yml
type: "config|noreplace"
- src: client/ntfy-client.service
dst: /lib/systemd/system/ntfy-client.service
- dst: /var/cache/ntfy
type: dir
- dst: /var/cache/ntfy/attachments
type: dir
- dst: /var/lib/ntfy
type: dir
- dst: /usr/share/ntfy/logo.png
src: web/public/static/img/ntfy.png
scripts:
preinstall: "scripts/preinst.sh"
postinstall: "scripts/postinst.sh"
preremove: "scripts/prerm.sh"
postremove: "scripts/postrm.sh"
archives:
-
@@ -37,8 +98,10 @@ archives:
files:
- LICENSE
- README.md
- config/config.yml
- config/ntfy.service
- server/server.yml
- server/ntfy.service
- client/client.yml
- client/ntfy-client.service
replacements:
386: i386
amd64: x86_64
@@ -53,10 +116,36 @@ changelog:
- '^docs:'
- '^test:'
dockers:
- dockerfile: Dockerfile
ids:
- ntfy
- image_templates:
- &amd64_image "binwiederhier/ntfy:{{ .Tag }}-amd64"
use: buildx
dockerfile: Dockerfile
goarch: amd64
build_flag_templates:
- "--platform=linux/amd64"
- image_templates:
- &arm64v8_image "binwiederhier/ntfy:{{ .Tag }}-arm64v8"
use: buildx
dockerfile: Dockerfile
goarch: arm64
build_flag_templates:
- "--platform=linux/arm64/v8"
- image_templates:
- &armv7_image "binwiederhier/ntfy:{{ .Tag }}-armv7"
use: buildx
dockerfile: Dockerfile
goarch: arm
goarm: 7
build_flag_templates:
- "--platform=linux/arm/v7"
docker_manifests:
- name_template: "binwiederhier/ntfy:latest"
image_templates:
- "binwiederhier/ntfy:latest"
- "binwiederhier/ntfy:{{ .Tag }}"
- "binwiederhier/ntfy:v{{ .Major }}.{{ .Minor }}"
- *amd64_image
- *arm64v8_image
- *armv7_image
- name_template: "binwiederhier/ntfy:{{ .Tag }}"
image_templates:
- *amd64_image
- *arm64v8_image
- *armv7_image

View File

@@ -2,4 +2,6 @@ FROM alpine
MAINTAINER Philipp C. Heckel <philipp.heckel@gmail.com>
COPY ntfy /usr/bin
EXPOSE 80/tcp
ENTRYPOINT ["ntfy"]

View File

@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Copyright 2021 Philipp C. Heckel
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

340
LICENSE.GPLv2 Normal file
View File

@@ -0,0 +1,340 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
ntfy
Copyright (C) 2021 Philipp C. Heckel
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

242
Makefile
View File

@@ -1,41 +1,137 @@
GO=$(shell which go)
VERSION := $(shell git describe --tag)
.PHONY:
help:
@echo "Typical commands:"
@echo " make check - Run all tests, vetting/formatting checks and linters"
@echo " make fmt build-snapshot install - Build latest and install to local system"
@echo "Typical commands (more see below):"
@echo " make build - Build web app, documentation and server/client (sloowwww)"
@echo " make server-amd64 - Build server/client binary (amd64, no web app or docs)"
@echo " make install-amd64 - Install ntfy binary to /usr/bin/ntfy (amd64)"
@echo " make web - Build the web app"
@echo " make docs - Build the documentation"
@echo " make check - Run all tests, vetting/formatting checks and linters"
@echo
@echo "Build everything:"
@echo " make build - Build web app, documentation and server/client"
@echo " make clean - Clean build/dist folders"
@echo
@echo "Build server & client (not release version):"
@echo " make server - Build server & client (all architectures)"
@echo " make server-amd64 - Build server & client (amd64 only)"
@echo " make server-armv6 - Build server & client (armv6 only)"
@echo " make server-armv7 - Build server & client (armv7 only)"
@echo " make server-arm64 - Build server & client (arm64 only)"
@echo
@echo "Build web app:"
@echo " make web - Build the web app"
@echo " make web-deps - Install web app dependencies (npm install the universe)"
@echo " make web-build - Actually build the web app"
@echo
@echo "Build documentation:"
@echo " make docs - Build the documentation"
@echo " make docs-deps - Install Python dependencies (pip3 install)"
@echo " make docs-build - Actually build the documentation"
@echo
@echo "Test/check:"
@echo " make test - Run tests"
@echo " make race - Run tests with -race flag"
@echo " make coverage - Run tests and show coverage"
@echo " make coverage-html - Run tests and show coverage (as HTML)"
@echo " make coverage-upload - Upload coverage results to codecov.io"
@echo " make test - Run tests"
@echo " make race - Run tests with -race flag"
@echo " make coverage - Run tests and show coverage"
@echo " make coverage-html - Run tests and show coverage (as HTML)"
@echo " make coverage-upload - Upload coverage results to codecov.io"
@echo
@echo "Lint/format:"
@echo " make fmt - Run 'go fmt'"
@echo " make fmt-check - Run 'go fmt', but don't change anything"
@echo " make vet - Run 'go vet'"
@echo " make lint - Run 'golint'"
@echo " make staticcheck - Run 'staticcheck'"
@echo " make fmt - Run 'go fmt'"
@echo " make fmt-check - Run 'go fmt', but don't change anything"
@echo " make vet - Run 'go vet'"
@echo " make lint - Run 'golint'"
@echo " make staticcheck - Run 'staticcheck'"
@echo
@echo "Build:"
@echo " make build - Build"
@echo " make build-snapshot - Build snapshot"
@echo " make build-simple - Build (using go build, without goreleaser)"
@echo " make clean - Clean build folder"
@echo
@echo "Releasing (requires goreleaser):"
@echo " make release - Create a release"
@echo " make release-snapshot - Create a test release"
@echo "Releasing:"
@echo " make release - Create a release"
@echo " make release-snapshot - Create a test release"
@echo
@echo "Install locally (requires sudo):"
@echo " make install - Copy binary from dist/ to /usr/bin"
@echo " make install-deb - Install .deb from dist/"
@echo " make install-lint - Install golint"
@echo " make install-amd64 - Copy amd64 binary from dist/ to /usr/bin/ntfy"
@echo " make install-armv6 - Copy armv6 binary from dist/ to /usr/bin/ntfy"
@echo " make install-armv7 - Copy armv7 binary from dist/ to /usr/bin/ntfy"
@echo " make install-arm64 - Copy arm64 binary from dist/ to /usr/bin/ntfy"
@echo " make install-deb-amd64 - Install .deb from dist/ (amd64 only)"
@echo " make install-deb-armv6 - Install .deb from dist/ (armv6 only)"
@echo " make install-deb-armv7 - Install .deb from dist/ (armv7 only)"
@echo " make install-deb-arm64 - Install .deb from dist/ (arm64 only)"
# Building everything
clean: .PHONY
rm -rf dist build server/docs server/site
build: web docs server
# Documentation
docs: docs-deps docs-build
docs-deps: .PHONY
pip3 install -r requirements.txt
docs-build: .PHONY
mkdocs build
# Web app
web: web-deps web-build
web-deps:
cd web && npm install
# If this fails for .svg files, optimizes them with svgo
web-build:
cd web \
&& npm run build \
&& mv build/index.html build/app.html \
&& rm -rf ../server/site \
&& mv build ../server/site \
&& rm \
../server/site/config.js \
../server/site/asset-manifest.json
# Main server/client build
server: server-deps
goreleaser build --snapshot --rm-dist --debug
server-amd64: server-deps-static-sites
goreleaser build --snapshot --rm-dist --debug --id ntfy_amd64
server-armv6: server-deps-static-sites server-deps-gcc-armv6-armv7
goreleaser build --snapshot --rm-dist --debug --id ntfy_armv6
server-armv7: server-deps-static-sites server-deps-gcc-armv6-armv7
goreleaser build --snapshot --rm-dist --debug --id ntfy_armv7
server-arm64: server-deps-static-sites server-deps-gcc-arm64
goreleaser build --snapshot --rm-dist --debug --id ntfy_arm64
server-deps: server-deps-static-sites server-deps-all server-deps-gcc
server-deps-gcc: server-deps-gcc-armv6-armv7 server-deps-gcc-arm64
server-deps-static-sites:
mkdir -p server/docs server/site
touch server/docs/index.html server/site/app.html
server-deps-all:
which upx || { echo "ERROR: upx not installed. On Ubuntu, run: apt install upx"; exit 1; }
server-deps-gcc-armv6-armv7:
which arm-linux-gnueabi-gcc || { echo "ERROR: ARMv6/ARMv7 cross compiler not installed. On Ubuntu, run: apt install gcc-arm-linux-gnueabi"; exit 1; }
server-deps-gcc-arm64:
which aarch64-linux-gnu-gcc || { echo "ERROR: ARM64 cross compiler not installed. On Ubuntu, run: apt install gcc-aarch64-linux-gnu"; exit 1; }
# Test/check targets
@@ -43,82 +139,98 @@ help:
check: test fmt-check vet lint staticcheck
test: .PHONY
$(GO) test ./...
go test -v $(shell go list ./... | grep -vE 'ntfy/(test|examples|tools)')
race: .PHONY
$(GO) test -race ./...
go test -race $(shell go list ./... | grep -vE 'ntfy/(test|examples|tools)')
coverage:
mkdir -p build/coverage
$(GO) test -race -coverprofile=build/coverage/coverage.txt -covermode=atomic ./...
$(GO) tool cover -func build/coverage/coverage.txt
go test -race -coverprofile=build/coverage/coverage.txt -covermode=atomic $(shell go list ./... | grep -vE 'ntfy/(test|examples|tools)')
go tool cover -func build/coverage/coverage.txt
coverage-html:
mkdir -p build/coverage
$(GO) test -race -coverprofile=build/coverage/coverage.txt -covermode=atomic ./...
$(GO) tool cover -html build/coverage/coverage.txt
go test -race -coverprofile=build/coverage/coverage.txt -covermode=atomic $(shell go list ./... | grep -vE 'ntfy/(test|examples|tools)')
go tool cover -html build/coverage/coverage.txt
coverage-upload:
cd build/coverage && (curl -s https://codecov.io/bash | bash)
# Lint/formatting targets
fmt:
$(GO) fmt ./...
gofmt -s -w .
fmt-check:
test -z $(shell gofmt -l .)
vet:
$(GO) vet ./...
go vet ./...
lint:
which golint || $(GO) get -u golang.org/x/lint/golint
$(GO) list ./... | grep -v /vendor/ | xargs -L1 golint -set_exit_status
which golint || go install golang.org/x/lint/golint@latest
go list ./... | grep -v /vendor/ | xargs -L1 golint -set_exit_status
staticcheck: .PHONY
rm -rf build/staticcheck
which staticcheck || go get honnef.co/go/tools/cmd/staticcheck
which staticcheck || go install honnef.co/go/tools/cmd/staticcheck@latest
mkdir -p build/staticcheck
ln -s "$(GO)" build/staticcheck/go
ln -s "go" build/staticcheck/go
PATH="$(PWD)/build/staticcheck:$(PATH)" staticcheck ./...
rm -rf build/staticcheck
# Building targets
build: .PHONY
goreleaser build --rm-dist
build-snapshot:
goreleaser build --snapshot --rm-dist
build-simple: clean
mkdir -p dist/ntfy_linux_amd64
$(GO) build \
-o dist/ntfy_linux_amd64/ntfy \
-ldflags \
"-s -w -X main.version=$(VERSION) -X main.commit=$(shell git rev-parse --short HEAD) -X main.date=$(shell date +%s)"
clean: .PHONY
rm -rf dist build
# Releasing targets
release:
goreleaser release --rm-dist
release: clean server-deps release-check-tags docs web check
goreleaser release --rm-dist --debug
release-snapshot:
goreleaser release --snapshot --skip-publish --rm-dist
release-snapshot: clean server-deps docs web check
goreleaser release --snapshot --skip-publish --rm-dist --debug
release-check-tags:
$(eval LATEST_TAG := $(shell git describe --abbrev=0 --tags | cut -c2-))
if ! grep -q $(LATEST_TAG) docs/install.md; then\
echo "ERROR: Must update docs/install.md with latest tag first.";\
exit 1;\
fi
if ! grep -q $(LATEST_TAG) docs/releases.md; then\
echo "ERROR: Must update docs/releases.md with latest tag first.";\
exit 1;\
fi
# Installing targets
install:
sudo rm -f /usr/bin/ntfy
sudo cp -a dist/ntfy_linux_amd64/ntfy /usr/bin/ntfy
install-amd64: remove-binary
sudo cp -a dist/ntfy_amd64_linux_amd64_v1/ntfy /usr/bin/ntfy
install-deb:
install-armv6: remove-binary
sudo cp -a dist/ntfy_armv6_linux_arm_6/ntfy /usr/bin/ntfy
install-armv7: remove-binary
sudo cp -a dist/ntfy_armv7_linux_arm_7/ntfy /usr/bin/ntfy
install-arm64: remove-binary
sudo cp -a dist/ntfy_arm64_linux_arm64/ntfy /usr/bin/ntfy
remove-binary:
sudo rm -f /usr/bin/ntfy
install-amd64-deb: purge-package
sudo dpkg -i dist/ntfy_*_linux_amd64.deb
install-armv6-deb: purge-package
sudo dpkg -i dist/ntfy_*_linux_armv6.deb
install-armv7-deb: purge-package
sudo dpkg -i dist/ntfy_*_linux_armv7.deb
install-arm64-deb: purge-package
sudo dpkg -i dist/ntfy_*_linux_arm64.deb
purge-package:
sudo systemctl stop ntfy || true
sudo apt-get purge ntfy || true
sudo dpkg -i dist/*.deb

167
README.md
View File

@@ -1,119 +1,74 @@
# ntfy
![ntfy](web/public/static/img/ntfy.png)
ntfy (pronounce: *notify*) is a super simple pub-sub notification service. It allows you to send desktop and (soon) phone notifications
via scripts. I run a free version of it on *[ntfy.sh](https://ntfy.sh)*. **No signups or cost.**
# ntfy.sh | Send push notifications to your phone or desktop via PUT/POST
[![Release](https://img.shields.io/github/release/binwiederhier/ntfy.svg?color=success&style=flat-square)](https://github.com/binwiederhier/ntfy/releases/latest)
[![Go Reference](https://pkg.go.dev/badge/heckel.io/ntfy.svg)](https://pkg.go.dev/heckel.io/ntfy)
[![Tests](https://github.com/binwiederhier/ntfy/workflows/test/badge.svg)](https://github.com/binwiederhier/ntfy/actions)
[![Go Report Card](https://goreportcard.com/badge/github.com/binwiederhier/ntfy)](https://goreportcard.com/report/github.com/binwiederhier/ntfy)
[![codecov](https://codecov.io/gh/binwiederhier/ntfy/branch/main/graph/badge.svg?token=A597KQ463G)](https://codecov.io/gh/binwiederhier/ntfy)
[![Discord](https://img.shields.io/discord/874398661709295626?label=Discord)](https://discord.gg/cT7ECsZj9w)
[![Matrix](https://img.shields.io/matrix/ntfy:matrix.org?label=Matrix)](https://matrix.to/#/#ntfy:matrix.org)
[![Healthcheck](https://healthchecks.io/badge/68b65976-b3b0-4102-aec9-980921/kcoEgrLY.svg)](https://ntfy.statuspage.io/)
## Usage
**ntfy** (pronounce: *notify*) is a simple HTTP-based [pub-sub](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) notification service.
It allows you to **send notifications to your phone or desktop via scripts** from any computer, entirely **without signup or cost**.
It's also open source (as you can plainly see) if you want to run your own.
### Subscribe to a topic
Topics are created on the fly by subscribing to them. You can create and subscribe to a topic either in a web UI, or in
your own app by subscribing to an [SSE](https://en.wikipedia.org/wiki/Server-sent_events)/[EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource),
or a JSON or raw feed.
I run a free version of it at **[ntfy.sh](https://ntfy.sh)**, and there's an [open source](https://github.com/binwiederhier/ntfy-android) [Android app](https://play.google.com/store/apps/details?id=io.heckel.ntfy)
too.
Because there is no sign-up, **the topic is essentially a password**, so pick something that's not easily guessable.
<p>
<img src="web/public/static/img/screenshot-curl.png" height="180">
<img src="web/public/static/img/screenshot-web-detail.png" height="180">
<img src="web/public/static/img/screenshot-phone-main.jpg" height="180">
<img src="web/public/static/img/screenshot-phone-detail.jpg" height="180">
<img src="web/public/static/img/screenshot-phone-notification.jpg" height="180">
</p>
Here's how you can create a topic `mytopic`, subscribe to it topic and wait for events. This is using `curl`, but you
can use any library that can do HTTP GETs:
## **[Documentation](https://ntfy.sh/docs/)**
```
# Subscribe to "mytopic" and output one message per line (\n are replaced with a space)
curl -s ntfy.sh/mytopic/raw
# Subscribe to "mytopic" and output one JSON message per line
curl -s ntfy.sh/mytopic/json
# Subscribe to "mytopic" and output an SSE stream (supported via JS/EventSource)
curl -s ntfy.sh/mytopic/sse
```
You can easily script it to execute any command when a message arrives. This sends desktop notifications (just like
the web UI, but without it):
```
while read msg; do
notify-send "$msg"
done < <(stdbuf -i0 -o0 curl -s ntfy.sh/mytopic/raw)
```
### Publish messages
Publishing messages can be done via PUT or POST using. Here's an example using `curl`:
```
curl -d "long process is done" ntfy.sh/mytopic
```
Messages published to a non-existing topic or a topic without subscribers will not be delivered later. There is (currently)
no buffering of any kind. If you're not listening, the message won't be delivered.
## Installation
Please check out the [releases page](https://github.com/binwiederhier/ntfy/releases) for binaries and
deb/rpm packages.
1. Install ntfy using one of the methods described below
2. Then (optionally) edit `/etc/ntfy/config.yml`
3. Then just run it with `ntfy` (or `systemctl start ntfy` when using the deb/rpm).
### Binaries and packages
**Debian/Ubuntu** (*from a repository*)**:**
```bash
curl -sSL https://archive.heckel.io/apt/pubkey.txt | sudo apt-key add -
sudo apt install apt-transport-https
sudo sh -c "echo 'deb [arch=amd64] https://archive.heckel.io/apt debian main' > /etc/apt/sources.list.d/archive.heckel.io.list"
sudo apt update
sudo apt install ntfy
```
**Debian/Ubuntu** (*manual install*)**:**
```bash
sudo apt install tmux
wget https://github.com/binwiederhier/ntfy/releases/download/v0.0.4/ntfy_0.0.4_amd64.deb
dpkg -i ntfy_0.0.4_amd64.deb
```
**Fedora/RHEL/CentOS:**
```bash
rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v0.0.4/ntfy_0.0.4_amd64.rpm
```
**Docker:**
```bash
docker run --rm -it binwiederhier/ntfy
```
**Go:**
```bash
go get -u heckel.io/ntfy
```
**Manual install** (*any x86_64-based Linux*)**:**
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v0.0.4/ntfy_0.0.4_linux_x86_64.tar.gz
sudo tar -C /usr/bin -zxf ntfy_0.0.4_linux_x86_64.tar.gz ntfy
./ntfy
```
## Building
Building ntfy is simple. Here's how you do it:
```
make build-simple
# Builds to dist/ntfy_linux_amd64/ntfy
```
To build releases, I use [GoReleaser](https://goreleaser.com/). If you have that installed, you can run `make build` or
`make build-snapshot`.
## TODO
- add HTTPS
- make limits configurable
- limit max number of subscriptions
[Getting started](https://ntfy.sh/docs/) |
[Android/iOS](https://ntfy.sh/docs/subscribe/phone/) |
[API](https://ntfy.sh/docs/publish/) |
[Install / Self-hosting](https://ntfy.sh/docs/install/) |
[Building](https://ntfy.sh/docs/develop/)
## Contributing
I welcome any and all contributions. Just create a PR or an issue.
I welcome any and all contributions. Just create a PR or an issue. To contribute code, check out
the [build instructions](https://ntfy.sh/docs/develop/) for the server and the Android app.
Or, if you'd like to help translate 🇩🇪 🇺🇸 🇧🇬, you can start immediately in
[Hosted Weblate](https://hosted.weblate.org/projects/ntfy/).
<a href="https://hosted.weblate.org/engage/ntfy/">
<img src="https://hosted.weblate.org/widgets/ntfy/-/multi-blue.svg" alt="Translation status" />
</a>
## Contact me
You can directly contact me **[on Discord](https://discord.gg/cT7ECsZj9w)** or [on Matrix](https://matrix.to/#/#ntfy:matrix.org)
(bridged from Discord), or via the [GitHub issues](https://github.com/binwiederhier/ntfy/issues), or find more contact information
[on my website](https://heckel.io/about).
## License
Made with ❤️ by [Philipp C. Heckel](https://heckel.io), distributed under the [Apache License 2.0](LICENSE).
Made with ❤️ by [Philipp C. Heckel](https://heckel.io).
The project is dual licensed under the [Apache License 2.0](LICENSE) and the [GPLv2 License](LICENSE.GPLv2).
Third party libraries and resources:
* [github.com/urfave/cli/v2](https://github.com/urfave/cli/v2) (MIT) is used to drive the CLI
* [Mixkit sound](https://mixkit.co/free-sound-effects/notification/) (Mixkit Free License) used as notification sound
* [Lato Font](https://www.latofonts.com/) (OFL) is used as a font in the Web UI
* [GoReleaser](https://goreleaser.com/) (MIT) is used to create releases
* [Mixkit sounds](https://mixkit.co/free-sound-effects/notification/) (Mixkit Free License) are used as notification sounds
* [Sounds from notificationsounds.com](https://notificationsounds.com) (Creative Commons Attribution) are used as notification sounds
* [Roboto Font](https://fonts.google.com/specimen/Roboto) (Apache 2.0) is used as a font in everything web
* [React](https://reactjs.org/) (MIT) is used for the web app
* [Material UI components](https://mui.com/) (MIT) are used in the web app
* [MUI dashboard template](https://github.com/mui/material-ui/tree/master/docs/data/material/getting-started/templates/dashboard) (MIT) was used as a basis for the web app
* [Dexie.js](https://github.com/dexie/Dexie.js) (Apache 2.0) is used for web app persistence in IndexedDB
* [GoReleaser](https://goreleaser.com/) (MIT) is used to create releases
* [go-smtp](https://github.com/emersion/go-smtp) (MIT) is used to receive e-mails
* [stretchr/testify](https://github.com/stretchr/testify) (MIT) is used for unit and integration tests
* [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) (MIT) is used to provide the persistent message cache
* [Firebase Admin SDK](https://github.com/firebase/firebase-admin-go) (Apache 2.0) is used to send FCM messages
* [github/gemoji](https://github.com/github/gemoji) (MIT) is used for emoji support (specifically the [emoji.json](https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json) file)
* [Lightbox with vanilla JS](https://yossiabramov.com/blog/vanilla-js-lightbox) as a lightbox on the landing page
* [HTTP middleware for gzip compression](https://gist.github.com/CJEnright/bc2d8b8dc0c1389a9feeddb110f822d7) (MIT) is used for serving static files
* [Regex for auto-linking](https://github.com/bryanwoods/autolink-js) (MIT) is used to highlight links (the library is not used)
* [Statically linking go-sqlite3](https://www.arp242.net/static-go.html)
* [Linked tabs in mkdocs](https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/#linked-tabs)

122
auth/auth.go Normal file
View File

@@ -0,0 +1,122 @@
// Package auth deals with authentication and authorization against topics
package auth
import (
"errors"
"regexp"
)
// Auther is a generic interface to implement password-based authentication and authorization
type Auther interface {
// Authenticate checks username and password and returns a user if correct. The method
// returns in constant-ish time, regardless of whether the user exists or the password is
// correct or incorrect.
Authenticate(username, password string) (*User, error)
// Authorize returns nil if the given user has access to the given topic using the desired
// permission. The user param may be nil to signal an anonymous user.
Authorize(user *User, topic string, perm Permission) error
}
// Manager is an interface representing user and access management
type Manager interface {
// AddUser adds a user with the given username, password and role. The password should be hashed
// before it is stored in a persistence layer.
AddUser(username, password string, role Role) error
// RemoveUser deletes the user with the given username. The function returns nil on success, even
// if the user did not exist in the first place.
RemoveUser(username string) error
// Users returns a list of users. It always also returns the Everyone user ("*").
Users() ([]*User, error)
// User returns the user with the given username if it exists, or ErrNotFound otherwise.
// You may also pass Everyone to retrieve the anonymous user and its Grant list.
User(username string) (*User, error)
// ChangePassword changes a user's password
ChangePassword(username, password string) error
// ChangeRole changes a user's role. When a role is changed from RoleUser to RoleAdmin,
// all existing access control entries (Grant) are removed, since they are no longer needed.
ChangeRole(username string, role Role) error
// AllowAccess adds or updates an entry in th access control list for a specific user. It controls
// read/write access to a topic. The parameter topicPattern may include wildcards (*).
AllowAccess(username string, topicPattern string, read bool, write bool) error
// ResetAccess removes an access control list entry for a specific username/topic, or (if topic is
// empty) for an entire user. The parameter topicPattern may include wildcards (*).
ResetAccess(username string, topicPattern string) error
// DefaultAccess returns the default read/write access if no access control entry matches
DefaultAccess() (read bool, write bool)
}
// User is a struct that represents a user
type User struct {
Name string
Hash string // password hash (bcrypt)
Role Role
Grants []Grant
}
// Grant is a struct that represents an access control entry to a topic
type Grant struct {
TopicPattern string // May include wildcard (*)
AllowRead bool
AllowWrite bool
}
// Permission represents a read or write permission to a topic
type Permission int
// Permissions to a topic
const (
PermissionRead = Permission(1)
PermissionWrite = Permission(2)
)
// Role represents a user's role, either admin or regular user
type Role string
// User roles
const (
RoleAdmin = Role("admin")
RoleUser = Role("user")
RoleAnonymous = Role("anonymous")
)
// Everyone is a special username representing anonymous users
const (
Everyone = "*"
)
var (
allowedUsernameRegex = regexp.MustCompile(`^[-_.@a-zA-Z0-9]+$`) // Does not include Everyone (*)
allowedTopicPatternRegex = regexp.MustCompile(`^[-_*A-Za-z0-9]{1,64}$`) // Adds '*' for wildcards!
)
// AllowedRole returns true if the given role can be used for new users
func AllowedRole(role Role) bool {
return role == RoleUser || role == RoleAdmin
}
// AllowedUsername returns true if the given username is valid
func AllowedUsername(username string) bool {
return allowedUsernameRegex.MatchString(username)
}
// AllowedTopicPattern returns true if the given topic pattern is valid; this includes the wildcard character (*)
func AllowedTopicPattern(username string) bool {
return allowedTopicPatternRegex.MatchString(username)
}
// Error constants used by the package
var (
ErrUnauthenticated = errors.New("unauthenticated")
ErrUnauthorized = errors.New("unauthorized")
ErrInvalidArgument = errors.New("invalid argument")
ErrNotFound = errors.New("not found")
)

399
auth/auth_sqlite.go Normal file
View File

@@ -0,0 +1,399 @@
package auth
import (
"database/sql"
"errors"
"fmt"
_ "github.com/mattn/go-sqlite3" // SQLite driver
"golang.org/x/crypto/bcrypt"
"strings"
)
const (
bcryptCost = 10
intentionalSlowDownHash = "$2a$10$YFCQvqQDwIIwnJM1xkAYOeih0dg17UVGanaTStnrSzC8NCWxcLDwy" // Cost should match bcryptCost
)
// Auther-related queries
const (
createAuthTablesQueries = `
BEGIN;
CREATE TABLE IF NOT EXISTS user (
user TEXT NOT NULL PRIMARY KEY,
pass TEXT NOT NULL,
role TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS access (
user TEXT NOT NULL,
topic TEXT NOT NULL,
read INT NOT NULL,
write INT NOT NULL,
PRIMARY KEY (topic, user)
);
CREATE TABLE IF NOT EXISTS schemaVersion (
id INT PRIMARY KEY,
version INT NOT NULL
);
COMMIT;
`
selectUserQuery = `SELECT pass, role FROM user WHERE user = ?`
selectTopicPermsQuery = `
SELECT read, write
FROM access
WHERE user IN ('*', ?) AND ? LIKE topic
ORDER BY user DESC
`
)
// Manager-related queries
const (
insertUserQuery = `INSERT INTO user (user, pass, role) VALUES (?, ?, ?)`
selectUsernamesQuery = `SELECT user FROM user ORDER BY role, user`
updateUserPassQuery = `UPDATE user SET pass = ? WHERE user = ?`
updateUserRoleQuery = `UPDATE user SET role = ? WHERE user = ?`
deleteUserQuery = `DELETE FROM user WHERE user = ?`
upsertUserAccessQuery = `
INSERT INTO access (user, topic, read, write)
VALUES (?, ?, ?, ?)
ON CONFLICT (user, topic) DO UPDATE SET read=excluded.read, write=excluded.write
`
selectUserAccessQuery = `SELECT topic, read, write FROM access WHERE user = ?`
deleteAllAccessQuery = `DELETE FROM access`
deleteUserAccessQuery = `DELETE FROM access WHERE user = ?`
deleteTopicAccessQuery = `DELETE FROM access WHERE user = ? AND topic = ?`
)
// Schema management queries
const (
currentSchemaVersion = 1
insertSchemaVersion = `INSERT INTO schemaVersion VALUES (1, ?)`
selectSchemaVersionQuery = `SELECT version FROM schemaVersion WHERE id = 1`
)
// SQLiteAuth is an implementation of Auther and Manager. It stores users and access control list
// in a SQLite database.
type SQLiteAuth struct {
db *sql.DB
defaultRead bool
defaultWrite bool
}
var _ Auther = (*SQLiteAuth)(nil)
var _ Manager = (*SQLiteAuth)(nil)
// NewSQLiteAuth creates a new SQLiteAuth instance
func NewSQLiteAuth(filename string, defaultRead, defaultWrite bool) (*SQLiteAuth, error) {
db, err := sql.Open("sqlite3", filename)
if err != nil {
return nil, err
}
if err := setupAuthDB(db); err != nil {
return nil, err
}
return &SQLiteAuth{
db: db,
defaultRead: defaultRead,
defaultWrite: defaultWrite,
}, nil
}
// Authenticate checks username and password and returns a user if correct. The method
// returns in constant-ish time, regardless of whether the user exists or the password is
// correct or incorrect.
func (a *SQLiteAuth) Authenticate(username, password string) (*User, error) {
if username == Everyone {
return nil, ErrUnauthenticated
}
user, err := a.User(username)
if err != nil {
bcrypt.CompareHashAndPassword([]byte(intentionalSlowDownHash),
[]byte("intentional slow-down to avoid timing attacks"))
return nil, ErrUnauthenticated
}
if err := bcrypt.CompareHashAndPassword([]byte(user.Hash), []byte(password)); err != nil {
return nil, ErrUnauthenticated
}
return user, nil
}
// Authorize returns nil if the given user has access to the given topic using the desired
// permission. The user param may be nil to signal an anonymous user.
func (a *SQLiteAuth) Authorize(user *User, topic string, perm Permission) error {
if user != nil && user.Role == RoleAdmin {
return nil // Admin can do everything
}
username := Everyone
if user != nil {
username = user.Name
}
// Select the read/write permissions for this user/topic combo. The query may return two
// rows (one for everyone, and one for the user), but prioritizes the user. The value for
// user.Name may be empty (= everyone).
rows, err := a.db.Query(selectTopicPermsQuery, username, topic)
if err != nil {
return err
}
defer rows.Close()
if !rows.Next() {
return a.resolvePerms(a.defaultRead, a.defaultWrite, perm)
}
var read, write bool
if err := rows.Scan(&read, &write); err != nil {
return err
} else if err := rows.Err(); err != nil {
return err
}
return a.resolvePerms(read, write, perm)
}
func (a *SQLiteAuth) resolvePerms(read, write bool, perm Permission) error {
if perm == PermissionRead && read {
return nil
} else if perm == PermissionWrite && write {
return nil
}
return ErrUnauthorized
}
// AddUser adds a user with the given username, password and role. The password should be hashed
// before it is stored in a persistence layer.
func (a *SQLiteAuth) AddUser(username, password string, role Role) error {
if !AllowedUsername(username) || !AllowedRole(role) {
return ErrInvalidArgument
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
if err != nil {
return err
}
if _, err = a.db.Exec(insertUserQuery, username, hash, role); err != nil {
return err
}
return nil
}
// RemoveUser deletes the user with the given username. The function returns nil on success, even
// if the user did not exist in the first place.
func (a *SQLiteAuth) RemoveUser(username string) error {
if !AllowedUsername(username) {
return ErrInvalidArgument
}
if _, err := a.db.Exec(deleteUserQuery, username); err != nil {
return err
}
if _, err := a.db.Exec(deleteUserAccessQuery, username); err != nil {
return err
}
return nil
}
// Users returns a list of users. It always also returns the Everyone user ("*").
func (a *SQLiteAuth) Users() ([]*User, error) {
rows, err := a.db.Query(selectUsernamesQuery)
if err != nil {
return nil, err
}
defer rows.Close()
usernames := make([]string, 0)
for rows.Next() {
var username string
if err := rows.Scan(&username); err != nil {
return nil, err
} else if err := rows.Err(); err != nil {
return nil, err
}
usernames = append(usernames, username)
}
rows.Close()
users := make([]*User, 0)
for _, username := range usernames {
user, err := a.User(username)
if err != nil {
return nil, err
}
users = append(users, user)
}
everyone, err := a.everyoneUser()
if err != nil {
return nil, err
}
users = append(users, everyone)
return users, nil
}
// User returns the user with the given username if it exists, or ErrNotFound otherwise.
// You may also pass Everyone to retrieve the anonymous user and its Grant list.
func (a *SQLiteAuth) User(username string) (*User, error) {
if username == Everyone {
return a.everyoneUser()
}
rows, err := a.db.Query(selectUserQuery, username)
if err != nil {
return nil, err
}
defer rows.Close()
var hash, role string
if !rows.Next() {
return nil, ErrNotFound
}
if err := rows.Scan(&hash, &role); err != nil {
return nil, err
} else if err := rows.Err(); err != nil {
return nil, err
}
grants, err := a.readGrants(username)
if err != nil {
return nil, err
}
return &User{
Name: username,
Hash: hash,
Role: Role(role),
Grants: grants,
}, nil
}
func (a *SQLiteAuth) everyoneUser() (*User, error) {
grants, err := a.readGrants(Everyone)
if err != nil {
return nil, err
}
return &User{
Name: Everyone,
Hash: "",
Role: RoleAnonymous,
Grants: grants,
}, nil
}
func (a *SQLiteAuth) readGrants(username string) ([]Grant, error) {
rows, err := a.db.Query(selectUserAccessQuery, username)
if err != nil {
return nil, err
}
defer rows.Close()
grants := make([]Grant, 0)
for rows.Next() {
var topic string
var read, write bool
if err := rows.Scan(&topic, &read, &write); err != nil {
return nil, err
} else if err := rows.Err(); err != nil {
return nil, err
}
grants = append(grants, Grant{
TopicPattern: fromSQLWildcard(topic),
AllowRead: read,
AllowWrite: write,
})
}
return grants, nil
}
// ChangePassword changes a user's password
func (a *SQLiteAuth) ChangePassword(username, password string) error {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
if err != nil {
return err
}
if _, err := a.db.Exec(updateUserPassQuery, hash, username); err != nil {
return err
}
return nil
}
// ChangeRole changes a user's role. When a role is changed from RoleUser to RoleAdmin,
// all existing access control entries (Grant) are removed, since they are no longer needed.
func (a *SQLiteAuth) ChangeRole(username string, role Role) error {
if !AllowedUsername(username) || !AllowedRole(role) {
return ErrInvalidArgument
}
if _, err := a.db.Exec(updateUserRoleQuery, string(role), username); err != nil {
return err
}
if role == RoleAdmin {
if _, err := a.db.Exec(deleteUserAccessQuery, username); err != nil {
return err
}
}
return nil
}
// AllowAccess adds or updates an entry in th access control list for a specific user. It controls
// read/write access to a topic. The parameter topicPattern may include wildcards (*).
func (a *SQLiteAuth) AllowAccess(username string, topicPattern string, read bool, write bool) error {
if (!AllowedUsername(username) && username != Everyone) || !AllowedTopicPattern(topicPattern) {
return ErrInvalidArgument
}
if _, err := a.db.Exec(upsertUserAccessQuery, username, toSQLWildcard(topicPattern), read, write); err != nil {
return err
}
return nil
}
// ResetAccess removes an access control list entry for a specific username/topic, or (if topic is
// empty) for an entire user. The parameter topicPattern may include wildcards (*).
func (a *SQLiteAuth) ResetAccess(username string, topicPattern string) error {
if !AllowedUsername(username) && username != Everyone && username != "" {
return ErrInvalidArgument
} else if !AllowedTopicPattern(topicPattern) && topicPattern != "" {
return ErrInvalidArgument
}
if username == "" && topicPattern == "" {
_, err := a.db.Exec(deleteAllAccessQuery, username)
return err
} else if topicPattern == "" {
_, err := a.db.Exec(deleteUserAccessQuery, username)
return err
}
_, err := a.db.Exec(deleteTopicAccessQuery, username, toSQLWildcard(topicPattern))
return err
}
// DefaultAccess returns the default read/write access if no access control entry matches
func (a *SQLiteAuth) DefaultAccess() (read bool, write bool) {
return a.defaultRead, a.defaultWrite
}
func toSQLWildcard(s string) string {
return strings.ReplaceAll(s, "*", "%")
}
func fromSQLWildcard(s string) string {
return strings.ReplaceAll(s, "%", "*")
}
func setupAuthDB(db *sql.DB) error {
// If 'schemaVersion' table does not exist, this must be a new database
rowsSV, err := db.Query(selectSchemaVersionQuery)
if err != nil {
return setupNewAuthDB(db)
}
defer rowsSV.Close()
// If 'schemaVersion' table exists, read version and potentially upgrade
schemaVersion := 0
if !rowsSV.Next() {
return errors.New("cannot determine schema version: database file may be corrupt")
}
if err := rowsSV.Scan(&schemaVersion); err != nil {
return err
}
rowsSV.Close()
// Do migrations
if schemaVersion == currentSchemaVersion {
return nil
}
return fmt.Errorf("unexpected schema version found: %d", schemaVersion)
}
func setupNewAuthDB(db *sql.DB) error {
if _, err := db.Exec(createAuthTablesQueries); err != nil {
return err
}
if _, err := db.Exec(insertSchemaVersion, currentSchemaVersion); err != nil {
return err
}
return nil
}

243
auth/auth_sqlite_test.go Normal file
View File

@@ -0,0 +1,243 @@
package auth_test
import (
"github.com/stretchr/testify/require"
"heckel.io/ntfy/auth"
"path/filepath"
"strings"
"testing"
"time"
)
const minBcryptTimingMillis = int64(50) // Ideally should be >100ms, but this should also run on a Raspberry Pi without massive resources
func TestSQLiteAuth_FullScenario_Default_DenyAll(t *testing.T) {
a := newTestAuth(t, false, false)
require.Nil(t, a.AddUser("phil", "phil", auth.RoleAdmin))
require.Nil(t, a.AddUser("ben", "ben", auth.RoleUser))
require.Nil(t, a.AllowAccess("ben", "mytopic", true, true))
require.Nil(t, a.AllowAccess("ben", "readme", true, false))
require.Nil(t, a.AllowAccess("ben", "writeme", false, true))
require.Nil(t, a.AllowAccess("ben", "everyonewrite", false, false)) // How unfair!
require.Nil(t, a.AllowAccess(auth.Everyone, "announcements", true, false))
require.Nil(t, a.AllowAccess(auth.Everyone, "everyonewrite", true, true))
require.Nil(t, a.AllowAccess(auth.Everyone, "up*", false, true)) // Everyone can write to /up*
phil, err := a.Authenticate("phil", "phil")
require.Nil(t, err)
require.Equal(t, "phil", phil.Name)
require.True(t, strings.HasPrefix(phil.Hash, "$2a$10$"))
require.Equal(t, auth.RoleAdmin, phil.Role)
require.Equal(t, []auth.Grant{}, phil.Grants)
ben, err := a.Authenticate("ben", "ben")
require.Nil(t, err)
require.Equal(t, "ben", ben.Name)
require.True(t, strings.HasPrefix(ben.Hash, "$2a$10$"))
require.Equal(t, auth.RoleUser, ben.Role)
require.Equal(t, []auth.Grant{
{"mytopic", true, true},
{"readme", true, false},
{"writeme", false, true},
{"everyonewrite", false, false},
}, ben.Grants)
notben, err := a.Authenticate("ben", "this is wrong")
require.Nil(t, notben)
require.Equal(t, auth.ErrUnauthenticated, err)
// Admin can do everything
require.Nil(t, a.Authorize(phil, "sometopic", auth.PermissionWrite))
require.Nil(t, a.Authorize(phil, "mytopic", auth.PermissionRead))
require.Nil(t, a.Authorize(phil, "readme", auth.PermissionWrite))
require.Nil(t, a.Authorize(phil, "writeme", auth.PermissionWrite))
require.Nil(t, a.Authorize(phil, "announcements", auth.PermissionWrite))
require.Nil(t, a.Authorize(phil, "everyonewrite", auth.PermissionWrite))
// User cannot do everything
require.Nil(t, a.Authorize(ben, "mytopic", auth.PermissionWrite))
require.Nil(t, a.Authorize(ben, "mytopic", auth.PermissionRead))
require.Nil(t, a.Authorize(ben, "readme", auth.PermissionRead))
require.Equal(t, auth.ErrUnauthorized, a.Authorize(ben, "readme", auth.PermissionWrite))
require.Equal(t, auth.ErrUnauthorized, a.Authorize(ben, "writeme", auth.PermissionRead))
require.Nil(t, a.Authorize(ben, "writeme", auth.PermissionWrite))
require.Nil(t, a.Authorize(ben, "writeme", auth.PermissionWrite))
require.Equal(t, auth.ErrUnauthorized, a.Authorize(ben, "everyonewrite", auth.PermissionRead))
require.Equal(t, auth.ErrUnauthorized, a.Authorize(ben, "everyonewrite", auth.PermissionWrite))
require.Nil(t, a.Authorize(ben, "announcements", auth.PermissionRead))
require.Equal(t, auth.ErrUnauthorized, a.Authorize(ben, "announcements", auth.PermissionWrite))
// Everyone else can do barely anything
require.Equal(t, auth.ErrUnauthorized, a.Authorize(nil, "sometopicnotinthelist", auth.PermissionRead))
require.Equal(t, auth.ErrUnauthorized, a.Authorize(nil, "sometopicnotinthelist", auth.PermissionWrite))
require.Equal(t, auth.ErrUnauthorized, a.Authorize(nil, "mytopic", auth.PermissionRead))
require.Equal(t, auth.ErrUnauthorized, a.Authorize(nil, "mytopic", auth.PermissionWrite))
require.Equal(t, auth.ErrUnauthorized, a.Authorize(nil, "readme", auth.PermissionRead))
require.Equal(t, auth.ErrUnauthorized, a.Authorize(nil, "readme", auth.PermissionWrite))
require.Equal(t, auth.ErrUnauthorized, a.Authorize(nil, "writeme", auth.PermissionRead))
require.Equal(t, auth.ErrUnauthorized, a.Authorize(nil, "writeme", auth.PermissionWrite))
require.Equal(t, auth.ErrUnauthorized, a.Authorize(nil, "announcements", auth.PermissionWrite))
require.Nil(t, a.Authorize(nil, "announcements", auth.PermissionRead))
require.Nil(t, a.Authorize(nil, "everyonewrite", auth.PermissionRead))
require.Nil(t, a.Authorize(nil, "everyonewrite", auth.PermissionWrite))
require.Nil(t, a.Authorize(nil, "up1234", auth.PermissionWrite)) // Wildcard permission
require.Nil(t, a.Authorize(nil, "up5678", auth.PermissionWrite))
}
func TestSQLiteAuth_AddUser_Invalid(t *testing.T) {
a := newTestAuth(t, false, false)
require.Equal(t, auth.ErrInvalidArgument, a.AddUser(" invalid ", "pass", auth.RoleAdmin))
require.Equal(t, auth.ErrInvalidArgument, a.AddUser("validuser", "pass", "invalid-role"))
}
func TestSQLiteAuth_AddUser_Timing(t *testing.T) {
a := newTestAuth(t, false, false)
start := time.Now().UnixMilli()
require.Nil(t, a.AddUser("user", "pass", auth.RoleAdmin))
require.GreaterOrEqual(t, time.Now().UnixMilli()-start, minBcryptTimingMillis)
}
func TestSQLiteAuth_Authenticate_Timing(t *testing.T) {
a := newTestAuth(t, false, false)
require.Nil(t, a.AddUser("user", "pass", auth.RoleAdmin))
// Timing a correct attempt
start := time.Now().UnixMilli()
_, err := a.Authenticate("user", "pass")
require.Nil(t, err)
require.GreaterOrEqual(t, time.Now().UnixMilli()-start, minBcryptTimingMillis)
// Timing an incorrect attempt
start = time.Now().UnixMilli()
_, err = a.Authenticate("user", "INCORRECT")
require.Equal(t, auth.ErrUnauthenticated, err)
require.GreaterOrEqual(t, time.Now().UnixMilli()-start, minBcryptTimingMillis)
// Timing a non-existing user attempt
start = time.Now().UnixMilli()
_, err = a.Authenticate("DOES-NOT-EXIST", "hithere")
require.Equal(t, auth.ErrUnauthenticated, err)
require.GreaterOrEqual(t, time.Now().UnixMilli()-start, minBcryptTimingMillis)
}
func TestSQLiteAuth_UserManagement(t *testing.T) {
a := newTestAuth(t, false, false)
require.Nil(t, a.AddUser("phil", "phil", auth.RoleAdmin))
require.Nil(t, a.AddUser("ben", "ben", auth.RoleUser))
require.Nil(t, a.AllowAccess("ben", "mytopic", true, true))
require.Nil(t, a.AllowAccess("ben", "readme", true, false))
require.Nil(t, a.AllowAccess("ben", "writeme", false, true))
require.Nil(t, a.AllowAccess("ben", "everyonewrite", false, false)) // How unfair!
require.Nil(t, a.AllowAccess(auth.Everyone, "announcements", true, false))
require.Nil(t, a.AllowAccess(auth.Everyone, "everyonewrite", true, true))
// Query user details
phil, err := a.User("phil")
require.Nil(t, err)
require.Equal(t, "phil", phil.Name)
require.True(t, strings.HasPrefix(phil.Hash, "$2a$10$"))
require.Equal(t, auth.RoleAdmin, phil.Role)
require.Equal(t, []auth.Grant{}, phil.Grants)
ben, err := a.User("ben")
require.Nil(t, err)
require.Equal(t, "ben", ben.Name)
require.True(t, strings.HasPrefix(ben.Hash, "$2a$10$"))
require.Equal(t, auth.RoleUser, ben.Role)
require.Equal(t, []auth.Grant{
{"mytopic", true, true},
{"readme", true, false},
{"writeme", false, true},
{"everyonewrite", false, false},
}, ben.Grants)
everyone, err := a.User(auth.Everyone)
require.Nil(t, err)
require.Equal(t, "*", everyone.Name)
require.Equal(t, "", everyone.Hash)
require.Equal(t, auth.RoleAnonymous, everyone.Role)
require.Equal(t, []auth.Grant{
{"announcements", true, false},
{"everyonewrite", true, true},
}, everyone.Grants)
// Ben: Before revoking
require.Nil(t, a.AllowAccess("ben", "mytopic", true, true))
require.Nil(t, a.AllowAccess("ben", "readme", true, false))
require.Nil(t, a.AllowAccess("ben", "writeme", false, true))
require.Nil(t, a.Authorize(ben, "mytopic", auth.PermissionRead))
require.Nil(t, a.Authorize(ben, "mytopic", auth.PermissionWrite))
require.Nil(t, a.Authorize(ben, "readme", auth.PermissionRead))
require.Nil(t, a.Authorize(ben, "writeme", auth.PermissionWrite))
// Revoke access for "ben" to "mytopic", then check again
require.Nil(t, a.ResetAccess("ben", "mytopic"))
require.Equal(t, auth.ErrUnauthorized, a.Authorize(ben, "mytopic", auth.PermissionWrite)) // Revoked
require.Equal(t, auth.ErrUnauthorized, a.Authorize(ben, "mytopic", auth.PermissionRead)) // Revoked
require.Nil(t, a.Authorize(ben, "readme", auth.PermissionRead)) // Unchanged
require.Nil(t, a.Authorize(ben, "writeme", auth.PermissionWrite)) // Unchanged
// Revoke rest of the access
require.Nil(t, a.ResetAccess("ben", ""))
require.Equal(t, auth.ErrUnauthorized, a.Authorize(ben, "readme", auth.PermissionRead)) // Revoked
require.Equal(t, auth.ErrUnauthorized, a.Authorize(ben, "wrtiteme", auth.PermissionWrite)) // Revoked
// User list
users, err := a.Users()
require.Nil(t, err)
require.Equal(t, 3, len(users))
require.Equal(t, "phil", users[0].Name)
require.Equal(t, "ben", users[1].Name)
require.Equal(t, "*", users[2].Name)
// Remove user
require.Nil(t, a.RemoveUser("ben"))
_, err = a.User("ben")
require.Equal(t, auth.ErrNotFound, err)
users, err = a.Users()
require.Nil(t, err)
require.Equal(t, 2, len(users))
require.Equal(t, "phil", users[0].Name)
require.Equal(t, "*", users[1].Name)
}
func TestSQLiteAuth_ChangePassword(t *testing.T) {
a := newTestAuth(t, false, false)
require.Nil(t, a.AddUser("phil", "phil", auth.RoleAdmin))
_, err := a.Authenticate("phil", "phil")
require.Nil(t, err)
require.Nil(t, a.ChangePassword("phil", "newpass"))
_, err = a.Authenticate("phil", "phil")
require.Equal(t, auth.ErrUnauthenticated, err)
_, err = a.Authenticate("phil", "newpass")
require.Nil(t, err)
}
func TestSQLiteAuth_ChangeRole(t *testing.T) {
a := newTestAuth(t, false, false)
require.Nil(t, a.AddUser("ben", "ben", auth.RoleUser))
require.Nil(t, a.AllowAccess("ben", "mytopic", true, true))
require.Nil(t, a.AllowAccess("ben", "readme", true, false))
ben, err := a.User("ben")
require.Nil(t, err)
require.Equal(t, auth.RoleUser, ben.Role)
require.Equal(t, 2, len(ben.Grants))
require.Nil(t, a.ChangeRole("ben", auth.RoleAdmin))
ben, err = a.User("ben")
require.Nil(t, err)
require.Equal(t, auth.RoleAdmin, ben.Role)
require.Equal(t, 0, len(ben.Grants))
}
func newTestAuth(t *testing.T, defaultRead, defaultWrite bool) *auth.SQLiteAuth {
filename := filepath.Join(t.TempDir(), "user.db")
a, err := auth.NewSQLiteAuth(filename, defaultRead, defaultWrite)
require.Nil(t, err)
return a
}

284
client/client.go Normal file
View File

@@ -0,0 +1,284 @@
// Package client provides a ntfy client to publish and subscribe to topics
package client
import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"heckel.io/ntfy/util"
"io"
"log"
"net/http"
"strings"
"sync"
"time"
)
// Event type constants
const (
MessageEvent = "message"
KeepaliveEvent = "keepalive"
OpenEvent = "open"
PollRequestEvent = "poll_request"
)
const (
maxResponseBytes = 4096
)
// Client is the ntfy client that can be used to publish and subscribe to ntfy topics
type Client struct {
Messages chan *Message
config *Config
subscriptions map[string]*subscription
mu sync.Mutex
}
// Message is a struct that represents a ntfy message
type Message struct { // TODO combine with server.message
ID string
Event string
Time int64
Topic string
Message string
Title string
Priority int
Tags []string
Click string
Attachment *Attachment
// Additional fields
TopicURL string
SubscriptionID string
Raw string
}
// Attachment represents a message attachment
type Attachment struct {
Name string `json:"name"`
Type string `json:"type,omitempty"`
Size int64 `json:"size,omitempty"`
Expires int64 `json:"expires,omitempty"`
URL string `json:"url"`
Owner string `json:"-"` // IP address of uploader, used for rate limiting
}
type subscription struct {
ID string
topicURL string
cancel context.CancelFunc
}
// New creates a new Client using a given Config
func New(config *Config) *Client {
return &Client{
Messages: make(chan *Message, 50), // Allow reading a few messages
config: config,
subscriptions: make(map[string]*subscription),
}
}
// Publish sends a message to a specific topic, optionally using options.
// See PublishReader for details.
func (c *Client) Publish(topic, message string, options ...PublishOption) (*Message, error) {
return c.PublishReader(topic, strings.NewReader(message), options...)
}
// PublishReader sends a message to a specific topic, optionally using options.
//
// A topic can be either a full URL (e.g. https://myhost.lan/mytopic), a short URL which is then prepended https://
// (e.g. myhost.lan -> https://myhost.lan), or a short name which is expanded using the default host in the
// config (e.g. mytopic -> https://ntfy.sh/mytopic).
//
// To pass title, priority and tags, check out WithTitle, WithPriority, WithTagsList, WithDelay, WithNoCache,
// WithNoFirebase, and the generic WithHeader.
func (c *Client) PublishReader(topic string, body io.Reader, options ...PublishOption) (*Message, error) {
topicURL := c.expandTopicURL(topic)
req, _ := http.NewRequest("POST", topicURL, body)
for _, option := range options {
if err := option(req); err != nil {
return nil, err
}
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
b, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBytes))
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, errors.New(strings.TrimSpace(string(b)))
}
m, err := toMessage(string(b), topicURL, "")
if err != nil {
return nil, err
}
return m, nil
}
// Poll queries a topic for all (or a limited set) of messages. Unlike Subscribe, this method only polls for
// messages and does not subscribe to messages that arrive after this call.
//
// A topic can be either a full URL (e.g. https://myhost.lan/mytopic), a short URL which is then prepended https://
// (e.g. myhost.lan -> https://myhost.lan), or a short name which is expanded using the default host in the
// config (e.g. mytopic -> https://ntfy.sh/mytopic).
//
// By default, all messages will be returned, but you can change this behavior using a SubscribeOption.
// See WithSince, WithSinceAll, WithSinceUnixTime, WithScheduled, and the generic WithQueryParam.
func (c *Client) Poll(topic string, options ...SubscribeOption) ([]*Message, error) {
ctx := context.Background()
messages := make([]*Message, 0)
msgChan := make(chan *Message)
errChan := make(chan error)
topicURL := c.expandTopicURL(topic)
options = append(options, WithPoll())
go func() {
err := performSubscribeRequest(ctx, msgChan, topicURL, "", options...)
close(msgChan)
errChan <- err
}()
for m := range msgChan {
messages = append(messages, m)
}
return messages, <-errChan
}
// Subscribe subscribes to a topic to listen for newly incoming messages. The method starts a connection in the
// background and returns new messages via the Messages channel.
//
// A topic can be either a full URL (e.g. https://myhost.lan/mytopic), a short URL which is then prepended https://
// (e.g. myhost.lan -> https://myhost.lan), or a short name which is expanded using the default host in the
// config (e.g. mytopic -> https://ntfy.sh/mytopic).
//
// By default, only new messages will be returned, but you can change this behavior using a SubscribeOption.
// See WithSince, WithSinceAll, WithSinceUnixTime, WithScheduled, and the generic WithQueryParam.
//
// The method returns a unique subscriptionID that can be used in Unsubscribe.
//
// Example:
// c := client.New(client.NewConfig())
// subscriptionID := c.Subscribe("mytopic")
// for m := range c.Messages {
// fmt.Printf("New message: %s", m.Message)
// }
func (c *Client) Subscribe(topic string, options ...SubscribeOption) string {
c.mu.Lock()
defer c.mu.Unlock()
subscriptionID := util.RandomString(10)
topicURL := c.expandTopicURL(topic)
ctx, cancel := context.WithCancel(context.Background())
c.subscriptions[subscriptionID] = &subscription{
ID: subscriptionID,
topicURL: topicURL,
cancel: cancel,
}
go handleSubscribeConnLoop(ctx, c.Messages, topicURL, subscriptionID, options...)
return subscriptionID
}
// Unsubscribe unsubscribes from a topic that has been previously subscribed to using the unique
// subscriptionID returned in Subscribe.
func (c *Client) Unsubscribe(subscriptionID string) {
c.mu.Lock()
defer c.mu.Unlock()
sub, ok := c.subscriptions[subscriptionID]
if !ok {
return
}
delete(c.subscriptions, subscriptionID)
sub.cancel()
}
// UnsubscribeAll unsubscribes from a topic that has been previously subscribed with Subscribe.
// If there are multiple subscriptions matching the topic, all of them are unsubscribed from.
//
// A topic can be either a full URL (e.g. https://myhost.lan/mytopic), a short URL which is then prepended https://
// (e.g. myhost.lan -> https://myhost.lan), or a short name which is expanded using the default host in the
// config (e.g. mytopic -> https://ntfy.sh/mytopic).
func (c *Client) UnsubscribeAll(topic string) {
c.mu.Lock()
defer c.mu.Unlock()
topicURL := c.expandTopicURL(topic)
for _, sub := range c.subscriptions {
if sub.topicURL == topicURL {
delete(c.subscriptions, sub.ID)
sub.cancel()
}
}
}
func (c *Client) expandTopicURL(topic string) string {
if strings.HasPrefix(topic, "http://") || strings.HasPrefix(topic, "https://") {
return topic
} else if strings.Contains(topic, "/") {
return fmt.Sprintf("https://%s", topic)
}
return fmt.Sprintf("%s/%s", c.config.DefaultHost, topic)
}
func handleSubscribeConnLoop(ctx context.Context, msgChan chan *Message, topicURL, subcriptionID string, options ...SubscribeOption) {
for {
// TODO The retry logic is crude and may lose messages. It should record the last message like the
// Android client, use since=, and do incremental backoff too
if err := performSubscribeRequest(ctx, msgChan, topicURL, subcriptionID, options...); err != nil {
log.Printf("Connection to %s failed: %s", topicURL, err.Error())
}
select {
case <-ctx.Done():
log.Printf("Connection to %s exited", topicURL)
return
case <-time.After(10 * time.Second): // TODO Add incremental backoff
}
}
}
func performSubscribeRequest(ctx context.Context, msgChan chan *Message, topicURL string, subscriptionID string, options ...SubscribeOption) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/json", topicURL), nil)
if err != nil {
return err
}
for _, option := range options {
if err := option(req); err != nil {
return err
}
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBytes))
if err != nil {
return err
}
return errors.New(strings.TrimSpace(string(b)))
}
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
m, err := toMessage(scanner.Text(), topicURL, subscriptionID)
if err != nil {
return err
}
if m.Event == MessageEvent {
msgChan <- m
}
}
return nil
}
func toMessage(s, topicURL, subscriptionID string) (*Message, error) {
var m *Message
if err := json.NewDecoder(strings.NewReader(s)).Decode(&m); err != nil {
return nil, err
}
m.TopicURL = topicURL
m.SubscriptionID = subscriptionID
m.Raw = s
return m, nil
}

40
client/client.yml Normal file
View File

@@ -0,0 +1,40 @@
# ntfy client config file
# Base URL used to expand short topic names in the "ntfy publish" and "ntfy subscribe" commands.
# If you self-host a ntfy server, you'll likely want to change this.
#
# default-host: https://ntfy.sh
# Subscriptions to topics and their actions. This option is primarily used by the systemd service,
# or if you cann "ntfy subscribe --from-config" directly.
#
# Example:
# subscribe:
# - topic: mytopic
# command: /usr/local/bin/mytopic-triggered.sh
# - topic: myserver.com/anothertopic
# command: 'echo "$message"'
# if:
# priority: high,urgent
# - topic: secret
# command: 'notify-send "$m"'
# user: phill
# password: mypass
#
# Variables:
# Variable Aliases Description
# --------------- --------------------- -----------------------------------
# $NTFY_ID $id Unique message ID
# $NTFY_TIME $time Unix timestamp of the message delivery
# $NTFY_TOPIC $topic Topic name
# $NTFY_MESSAGE $message, $m Message body
# $NTFY_TITLE $title, $t Message title
# $NTFY_PRIORITY $priority, $prio, $p Message priority (1=min, 5=max)
# $NTFY_TAGS $tags, $tag, $ta Message tags (comma separated list)
# $NTFY_RAW $raw Raw JSON message
#
# Filters ('if:'):
# You can filter 'message', 'title', 'priority' (comma-separated list, logical OR)
# and 'tags' (comma-separated list, logical AND). See https://ntfy.sh/docs/subscribe/api/#filter-messages.
#
# subscribe:

110
client/client_test.go Normal file
View File

@@ -0,0 +1,110 @@
package client_test
import (
"fmt"
"github.com/stretchr/testify/require"
"heckel.io/ntfy/client"
"heckel.io/ntfy/test"
"testing"
"time"
)
func TestClient_Publish_Subscribe(t *testing.T) {
s, port := test.StartServer(t)
defer test.StopServer(t, s, port)
c := client.New(newTestConfig(port))
subscriptionID := c.Subscribe("mytopic")
time.Sleep(time.Second)
msg, err := c.Publish("mytopic", "some message")
require.Nil(t, err)
require.Equal(t, "some message", msg.Message)
msg, err = c.Publish("mytopic", "some other message",
client.WithTitle("some title"),
client.WithPriority("high"),
client.WithTags([]string{"tag1", "tag 2"}))
require.Nil(t, err)
require.Equal(t, "some other message", msg.Message)
require.Equal(t, "some title", msg.Title)
require.Equal(t, []string{"tag1", "tag 2"}, msg.Tags)
require.Equal(t, 4, msg.Priority)
msg, err = c.Publish("mytopic", "some delayed message",
client.WithDelay("25 hours"))
require.Nil(t, err)
require.Equal(t, "some delayed message", msg.Message)
require.True(t, time.Now().Add(24*time.Hour).Unix() < msg.Time)
time.Sleep(200 * time.Millisecond)
msg = nextMessage(c)
require.NotNil(t, msg)
require.Equal(t, "some message", msg.Message)
msg = nextMessage(c)
require.NotNil(t, msg)
require.Equal(t, "some other message", msg.Message)
require.Equal(t, "some title", msg.Title)
require.Equal(t, []string{"tag1", "tag 2"}, msg.Tags)
require.Equal(t, 4, msg.Priority)
msg = nextMessage(c)
require.Nil(t, msg)
c.Unsubscribe(subscriptionID)
time.Sleep(200 * time.Millisecond)
msg, err = c.Publish("mytopic", "a message that won't be received")
require.Nil(t, err)
require.Equal(t, "a message that won't be received", msg.Message)
msg = nextMessage(c)
require.Nil(t, msg)
}
func TestClient_Publish_Poll(t *testing.T) {
s, port := test.StartServer(t)
defer test.StopServer(t, s, port)
c := client.New(newTestConfig(port))
msg, err := c.Publish("mytopic", "some message", client.WithNoFirebase(), client.WithTagsList("tag1,tag2"))
require.Nil(t, err)
require.Equal(t, "some message", msg.Message)
require.Equal(t, []string{"tag1", "tag2"}, msg.Tags)
msg, err = c.Publish("mytopic", "this won't be cached", client.WithNoCache())
require.Nil(t, err)
require.Equal(t, "this won't be cached", msg.Message)
msg, err = c.Publish("mytopic", "some delayed message", client.WithDelay("20 min"))
require.Nil(t, err)
require.Equal(t, "some delayed message", msg.Message)
messages, err := c.Poll("mytopic")
require.Nil(t, err)
require.Equal(t, 1, len(messages))
require.Equal(t, "some message", messages[0].Message)
messages, err = c.Poll("mytopic", client.WithScheduled())
require.Nil(t, err)
require.Equal(t, 2, len(messages))
require.Equal(t, "some message", messages[0].Message)
require.Equal(t, "some delayed message", messages[1].Message)
}
func newTestConfig(port int) *client.Config {
c := client.NewConfig()
c.DefaultHost = fmt.Sprintf("http://127.0.0.1:%d", port)
return c
}
func nextMessage(c *client.Client) *client.Message {
select {
case m := <-c.Messages:
return m
default:
return nil
}
}

44
client/config.go Normal file
View File

@@ -0,0 +1,44 @@
package client
import (
"gopkg.in/yaml.v2"
"os"
)
const (
// DefaultBaseURL is the base URL used to expand short topic names
DefaultBaseURL = "https://ntfy.sh"
)
// Config is the config struct for a Client
type Config struct {
DefaultHost string `yaml:"default-host"`
Subscribe []struct {
Topic string `yaml:"topic"`
User string `yaml:"user"`
Password string `yaml:"password"`
Command string `yaml:"command"`
If map[string]string `yaml:"if"`
} `yaml:"subscribe"`
}
// NewConfig creates a new Config struct for a Client
func NewConfig() *Config {
return &Config{
DefaultHost: DefaultBaseURL,
Subscribe: nil,
}
}
// LoadConfig loads the Client config from a yaml file
func LoadConfig(filename string) (*Config, error) {
b, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
c := NewConfig()
if err := yaml.Unmarshal(b, c); err != nil {
return nil, err
}
return c, nil
}

40
client/config_test.go Normal file
View File

@@ -0,0 +1,40 @@
package client_test
import (
"github.com/stretchr/testify/require"
"heckel.io/ntfy/client"
"os"
"path/filepath"
"testing"
)
func TestConfig_Load(t *testing.T) {
filename := filepath.Join(t.TempDir(), "client.yml")
require.Nil(t, os.WriteFile(filename, []byte(`
default-host: http://localhost
subscribe:
- topic: no-command-with-auth
user: phil
password: mypass
- topic: echo-this
command: 'echo "Message received: $message"'
- topic: alerts
command: notify-send -i /usr/share/ntfy/logo.png "Important" "$m"
if:
priority: high,urgent
`), 0600))
conf, err := client.LoadConfig(filename)
require.Nil(t, err)
require.Equal(t, "http://localhost", conf.DefaultHost)
require.Equal(t, 3, len(conf.Subscribe))
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
require.Equal(t, "", conf.Subscribe[0].Command)
require.Equal(t, "phil", conf.Subscribe[0].User)
require.Equal(t, "mypass", conf.Subscribe[0].Password)
require.Equal(t, "echo-this", conf.Subscribe[1].Topic)
require.Equal(t, `echo "Message received: $message"`, conf.Subscribe[1].Command)
require.Equal(t, "alerts", conf.Subscribe[2].Topic)
require.Equal(t, `notify-send -i /usr/share/ntfy/logo.png "Important" "$m"`, conf.Subscribe[2].Command)
require.Equal(t, "high,urgent", conf.Subscribe[2].If["priority"])
}

View File

@@ -0,0 +1,12 @@
[Unit]
Description=ntfy client
After=network.target
[Service]
User=ntfy
Group=ntfy
ExecStart=/usr/bin/ntfy subscribe --config /etc/ntfy/client.yml --from-config
Restart=on-failure
[Install]
WantedBy=multi-user.target

174
client/options.go Normal file
View File

@@ -0,0 +1,174 @@
package client
import (
"fmt"
"heckel.io/ntfy/util"
"net/http"
"strings"
"time"
)
// RequestOption is a generic request option that can be added to Client calls
type RequestOption = func(r *http.Request) error
// PublishOption is an option that can be passed to the Client.Publish call
type PublishOption = RequestOption
// SubscribeOption is an option that can be passed to a Client.Subscribe or Client.Poll call
type SubscribeOption = RequestOption
// WithMessage sets the notification message. This is an alternative way to passing the message body.
func WithMessage(message string) PublishOption {
return WithHeader("X-Message", message)
}
// WithTitle adds a title to a message
func WithTitle(title string) PublishOption {
return WithHeader("X-Title", title)
}
// WithPriority adds a priority to a message. The priority can be either a number (1=min, 5=max),
// or the corresponding names (see util.ParsePriority).
func WithPriority(priority string) PublishOption {
return WithHeader("X-Priority", priority)
}
// WithTagsList adds a list of tags to a message. The tags parameter must be a comma-separated list
// of tags. To use a slice, use WithTags instead
func WithTagsList(tags string) PublishOption {
return WithHeader("X-Tags", tags)
}
// WithTags adds a list of a tags to a message
func WithTags(tags []string) PublishOption {
return WithTagsList(strings.Join(tags, ","))
}
// WithDelay instructs the server to send the message at a later date. The delay parameter can be a
// Unix timestamp, a duration string or a natural langage string. See https://ntfy.sh/docs/publish/#scheduled-delivery
// for details.
func WithDelay(delay string) PublishOption {
return WithHeader("X-Delay", delay)
}
// WithClick makes the notification action open the given URL as opposed to entering the detail view
func WithClick(url string) PublishOption {
return WithHeader("X-Click", url)
}
// WithActions adds custom user actions to the notification. The value can be either a JSON array or the
// simple format definition. See https://ntfy.sh/docs/publish/#action-buttons for details.
func WithActions(value string) PublishOption {
return WithHeader("X-Actions", value)
}
// WithAttach sets a URL that will be used by the client to download an attachment
func WithAttach(attach string) PublishOption {
return WithHeader("X-Attach", attach)
}
// WithFilename sets a filename for the attachment, and/or forces the HTTP body to interpreted as an attachment
func WithFilename(filename string) PublishOption {
return WithHeader("X-Filename", filename)
}
// WithEmail instructs the server to also send the message to the given e-mail address
func WithEmail(email string) PublishOption {
return WithHeader("X-Email", email)
}
// WithBasicAuth adds the Authorization header for basic auth to the request
func WithBasicAuth(user, pass string) PublishOption {
return WithHeader("Authorization", util.BasicAuth(user, pass))
}
// WithNoCache instructs the server not to cache the message server-side
func WithNoCache() PublishOption {
return WithHeader("X-Cache", "no")
}
// WithNoFirebase instructs the server not to forward the message to Firebase
func WithNoFirebase() PublishOption {
return WithHeader("X-Firebase", "no")
}
// WithSince limits the number of messages returned from the server. The parameter since can be a Unix
// timestamp (see WithSinceUnixTime), a duration (WithSinceDuration) the word "all" (see WithSinceAll).
func WithSince(since string) SubscribeOption {
return WithQueryParam("since", since)
}
// WithSinceAll instructs the server to return all messages for the given topic from the server
func WithSinceAll() SubscribeOption {
return WithSince("all")
}
// WithSinceDuration instructs the server to return all messages since the given duration ago
func WithSinceDuration(since time.Duration) SubscribeOption {
return WithSinceUnixTime(time.Now().Add(-1 * since).Unix())
}
// WithSinceUnixTime instructs the server to return only messages newer or equal to the given timestamp
func WithSinceUnixTime(since int64) SubscribeOption {
return WithSince(fmt.Sprintf("%d", since))
}
// WithPoll instructs the server to close the connection after messages have been returned. Don't use this option
// directly. Use Client.Poll instead.
func WithPoll() SubscribeOption {
return WithQueryParam("poll", "1")
}
// WithScheduled instructs the server to also return messages that have not been sent yet, i.e. delayed/scheduled
// messages (see WithDelay). The messages will have a future date.
func WithScheduled() SubscribeOption {
return WithQueryParam("scheduled", "1")
}
// WithFilter is a generic subscribe option meant to be used to filter for certain messages only
func WithFilter(param, value string) SubscribeOption {
return WithQueryParam(param, value)
}
// WithMessageFilter instructs the server to only return messages that match the exact message
func WithMessageFilter(message string) SubscribeOption {
return WithQueryParam("message", message)
}
// WithTitleFilter instructs the server to only return messages with a title that match the exact string
func WithTitleFilter(title string) SubscribeOption {
return WithQueryParam("title", title)
}
// WithPriorityFilter instructs the server to only return messages with the matching priority. Not that messages
// without priority also implicitly match priority 3.
func WithPriorityFilter(priority int) SubscribeOption {
return WithQueryParam("priority", fmt.Sprintf("%d", priority))
}
// WithTagsFilter instructs the server to only return messages that contain all of the given tags
func WithTagsFilter(tags []string) SubscribeOption {
return WithQueryParam("tags", strings.Join(tags, ","))
}
// WithHeader is a generic option to add headers to a request
func WithHeader(header, value string) RequestOption {
return func(r *http.Request) error {
if value != "" {
r.Header.Set(header, value)
}
return nil
}
}
// WithQueryParam is a generic option to add query parameters to a request
func WithQueryParam(param, value string) RequestOption {
return func(r *http.Request) error {
if value != "" {
q := r.URL.Query()
q.Add(param, value)
r.URL.RawQuery = q.Encode()
}
return nil
}
}

212
cmd/access.go Normal file
View File

@@ -0,0 +1,212 @@
package cmd
import (
"errors"
"fmt"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/auth"
"heckel.io/ntfy/util"
)
const (
userEveryone = "everyone"
)
var flagsAccess = append(
userCommandFlags(),
&cli.BoolFlag{Name: "reset", Aliases: []string{"r"}, Usage: "reset access for user (and topic)"},
)
var cmdAccess = &cli.Command{
Name: "access",
Usage: "Grant/revoke access to a topic, or show access",
UsageText: "ntfy access [USERNAME [TOPIC [PERMISSION]]]",
Flags: flagsAccess,
Before: initConfigFileInputSource("config", flagsAccess),
Action: execUserAccess,
Category: categoryServer,
Description: `Manage the access control list for the ntfy server.
This is a server-only command. It directly manages the user.db as defined in the server config
file server.yml. The command only works if 'auth-file' is properly defined. Please also refer
to the related command 'ntfy user'.
The command allows you to show the access control list, as well as change it, depending on how
it is called.
Usage:
ntfy access # Shows access control list (alias: 'ntfy user list')
ntfy access USERNAME # Shows access control entries for USERNAME
ntfy access USERNAME TOPIC PERMISSION # Allow/deny access for USERNAME to TOPIC
Arguments:
USERNAME an existing user, as created with 'ntfy user add', or "everyone"/"*"
to define access rules for anonymous/unauthenticated clients
TOPIC name of a topic with optional wildcards, e.g. "mytopic*"
PERMISSION one of the following:
- read-write (alias: rw)
- read-only (aliases: read, ro)
- write-only (aliases: write, wo)
- deny (alias: none)
Examples:
ntfy access # Shows access control list (alias: 'ntfy user list')
ntfy access phil # Shows access for user phil
ntfy access phil mytopic rw # Allow read-write access to mytopic for user phil
ntfy access everyone mytopic rw # Allow anonymous read-write access to mytopic
ntfy access everyone "up*" write # Allow anonymous write-only access to topics "up..."
ntfy access --reset # Reset entire access control list
ntfy access --reset phil # Reset all access for user phil
ntfy access --reset phil mytopic # Reset access for user phil and topic mytopic
`,
}
func execUserAccess(c *cli.Context) error {
if c.NArg() > 3 {
return errors.New("too many arguments, please check 'ntfy access --help' for usage details")
}
manager, err := createAuthManager(c)
if err != nil {
return err
}
username := c.Args().Get(0)
if username == userEveryone {
username = auth.Everyone
}
topic := c.Args().Get(1)
perms := c.Args().Get(2)
reset := c.Bool("reset")
if reset {
if perms != "" {
return errors.New("too many arguments, please check 'ntfy access --help' for usage details")
}
return resetAccess(c, manager, username, topic)
} else if perms == "" {
if topic != "" {
return errors.New("invalid syntax, please check 'ntfy access --help' for usage details")
}
return showAccess(c, manager, username)
}
return changeAccess(c, manager, username, topic, perms)
}
func changeAccess(c *cli.Context, manager auth.Manager, username string, topic string, perms string) error {
if !util.InStringList([]string{"", "read-write", "rw", "read-only", "read", "ro", "write-only", "write", "wo", "none", "deny"}, perms) {
return errors.New("permission must be one of: read-write, read-only, write-only, or deny (or the aliases: read, ro, write, wo, none)")
}
read := util.InStringList([]string{"read-write", "rw", "read-only", "read", "ro"}, perms)
write := util.InStringList([]string{"read-write", "rw", "write-only", "write", "wo"}, perms)
user, err := manager.User(username)
if err == auth.ErrNotFound {
return fmt.Errorf("user %s does not exist", username)
} else if user.Role == auth.RoleAdmin {
return fmt.Errorf("user %s is an admin user, access control entries have no effect", username)
}
if err := manager.AllowAccess(username, topic, read, write); err != nil {
return err
}
if read && write {
fmt.Fprintf(c.App.ErrWriter, "granted read-write access to topic %s\n\n", topic)
} else if read {
fmt.Fprintf(c.App.ErrWriter, "granted read-only access to topic %s\n\n", topic)
} else if write {
fmt.Fprintf(c.App.ErrWriter, "granted write-only access to topic %s\n\n", topic)
} else {
fmt.Fprintf(c.App.ErrWriter, "revoked all access to topic %s\n\n", topic)
}
return showUserAccess(c, manager, username)
}
func resetAccess(c *cli.Context, manager auth.Manager, username, topic string) error {
if username == "" {
return resetAllAccess(c, manager)
} else if topic == "" {
return resetUserAccess(c, manager, username)
}
return resetUserTopicAccess(c, manager, username, topic)
}
func resetAllAccess(c *cli.Context, manager auth.Manager) error {
if err := manager.ResetAccess("", ""); err != nil {
return err
}
fmt.Fprintln(c.App.ErrWriter, "reset access for all users")
return nil
}
func resetUserAccess(c *cli.Context, manager auth.Manager, username string) error {
if err := manager.ResetAccess(username, ""); err != nil {
return err
}
fmt.Fprintf(c.App.ErrWriter, "reset access for user %s\n\n", username)
return showUserAccess(c, manager, username)
}
func resetUserTopicAccess(c *cli.Context, manager auth.Manager, username string, topic string) error {
if err := manager.ResetAccess(username, topic); err != nil {
return err
}
fmt.Fprintf(c.App.ErrWriter, "reset access for user %s and topic %s\n\n", username, topic)
return showUserAccess(c, manager, username)
}
func showAccess(c *cli.Context, manager auth.Manager, username string) error {
if username == "" {
return showAllAccess(c, manager)
}
return showUserAccess(c, manager, username)
}
func showAllAccess(c *cli.Context, manager auth.Manager) error {
users, err := manager.Users()
if err != nil {
return err
}
return showUsers(c, manager, users)
}
func showUserAccess(c *cli.Context, manager auth.Manager, username string) error {
users, err := manager.User(username)
if err == auth.ErrNotFound {
return fmt.Errorf("user %s does not exist", username)
} else if err != nil {
return err
}
return showUsers(c, manager, []*auth.User{users})
}
func showUsers(c *cli.Context, manager auth.Manager, users []*auth.User) error {
for _, user := range users {
fmt.Fprintf(c.App.ErrWriter, "user %s (%s)\n", user.Name, user.Role)
if user.Role == auth.RoleAdmin {
fmt.Fprintf(c.App.ErrWriter, "- read-write access to all topics (admin role)\n")
} else if len(user.Grants) > 0 {
for _, grant := range user.Grants {
if grant.AllowRead && grant.AllowWrite {
fmt.Fprintf(c.App.ErrWriter, "- read-write access to topic %s\n", grant.TopicPattern)
} else if grant.AllowRead {
fmt.Fprintf(c.App.ErrWriter, "- read-only access to topic %s\n", grant.TopicPattern)
} else if grant.AllowWrite {
fmt.Fprintf(c.App.ErrWriter, "- write-only access to topic %s\n", grant.TopicPattern)
} else {
fmt.Fprintf(c.App.ErrWriter, "- no access to topic %s\n", grant.TopicPattern)
}
}
} else {
fmt.Fprintf(c.App.ErrWriter, "- no topic-specific permissions\n")
}
if user.Name == auth.Everyone {
defaultRead, defaultWrite := manager.DefaultAccess()
if defaultRead && defaultWrite {
fmt.Fprintln(c.App.ErrWriter, "- read-write access to all (other) topics (server config)")
} else if defaultRead {
fmt.Fprintln(c.App.ErrWriter, "- read-only access to all (other) topics (server config)")
} else if defaultWrite {
fmt.Fprintln(c.App.ErrWriter, "- write-only access to all (other) topics (server config)")
} else {
fmt.Fprintln(c.App.ErrWriter, "- no access to any (other) topics (server config)")
}
}
}
return nil
}

87
cmd/access_test.go Normal file
View File

@@ -0,0 +1,87 @@
package cmd
import (
"fmt"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/server"
"heckel.io/ntfy/test"
"testing"
)
func TestCLI_Access_Show(t *testing.T) {
s, conf, port := newTestServerWithAuth(t)
defer test.StopServer(t, s, port)
app, _, _, stderr := newTestApp()
require.Nil(t, runAccessCommand(app, conf))
require.Contains(t, stderr.String(), "user * (anonymous)\n- no topic-specific permissions\n- no access to any (other) topics (server config)")
}
func TestCLI_Access_Grant_And_Publish(t *testing.T) {
s, conf, port := newTestServerWithAuth(t)
defer test.StopServer(t, s, port)
app, stdin, _, _ := newTestApp()
stdin.WriteString("philpass\nphilpass\nbenpass\nbenpass")
require.Nil(t, runUserCommand(app, conf, "add", "--role=admin", "phil"))
require.Nil(t, runUserCommand(app, conf, "add", "ben"))
require.Nil(t, runAccessCommand(app, conf, "ben", "announcements", "rw"))
require.Nil(t, runAccessCommand(app, conf, "ben", "sometopic", "read"))
require.Nil(t, runAccessCommand(app, conf, "everyone", "announcements", "read"))
app, _, _, stderr := newTestApp()
require.Nil(t, runAccessCommand(app, conf))
expected := `user phil (admin)
- read-write access to all topics (admin role)
user ben (user)
- read-write access to topic announcements
- read-only access to topic sometopic
user * (anonymous)
- read-only access to topic announcements
- no access to any (other) topics (server config)
`
require.Equal(t, expected, stderr.String())
// See if access permissions match
app, _, _, _ = newTestApp()
require.Error(t, app.Run([]string{
"ntfy",
"publish",
fmt.Sprintf("http://127.0.0.1:%d/announcements", port),
}))
require.Nil(t, app.Run([]string{
"ntfy",
"publish",
"-u", "ben:benpass",
fmt.Sprintf("http://127.0.0.1:%d/announcements", port),
}))
require.Nil(t, app.Run([]string{
"ntfy",
"publish",
"-u", "phil:philpass",
fmt.Sprintf("http://127.0.0.1:%d/announcements", port),
}))
require.Nil(t, app.Run([]string{
"ntfy",
"subscribe",
"--poll",
fmt.Sprintf("http://127.0.0.1:%d/announcements", port),
}))
require.Error(t, app.Run([]string{
"ntfy",
"subscribe",
"--poll",
fmt.Sprintf("http://127.0.0.1:%d/something-else", port),
}))
}
func runAccessCommand(app *cli.App, conf *server.Config, args ...string) error {
userArgs := []string{
"ntfy",
"access",
"--auth-file=" + conf.AuthFile,
"--auth-default-access=" + confToDefaultAccess(conf),
}
return app.Run(append(userArgs, args...))
}

View File

@@ -5,61 +5,52 @@ import (
"fmt"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"heckel.io/ntfy/config"
"heckel.io/ntfy/server"
"log"
"heckel.io/ntfy/util"
"os"
)
var (
defaultClientRootConfigFile = "/etc/ntfy/client.yml"
defaultClientUserConfigFile = "~/.config/ntfy/client.yml"
)
const (
categoryClient = "Client commands"
categoryServer = "Server commands"
)
// New creates a new CLI application
func New() *cli.App {
flags := []cli.Flag{
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: "/etc/ntfy/config.yml", DefaultText: "/etc/ntfy/config.yml", Usage: "config file"},
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-http", Aliases: []string{"l"}, EnvVars: []string{"NTFY_LISTEN_HTTP"}, Value: config.DefaultListenHTTP, Usage: "ip:port used to as listen address"}),
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "keepalive-interval", Aliases: []string{"k"}, EnvVars: []string{"NTFY_KEEPALIVE_INTERVAL"}, Value: config.DefaultKeepaliveInterval, Usage: "default interval of keepalive messages"}),
}
return &cli.App{
Name: "ntfy",
Usage: "Simple pub-sub notification service",
UsageText: "ntfy [OPTION..]",
HideHelp: true,
HideVersion: true,
EnableBashCompletion: true,
UseShortOptionHandling: true,
Reader: os.Stdin,
Writer: os.Stdout,
ErrWriter: os.Stderr,
Action: execRun,
Before: initConfigFileInputSource("config", flags),
Flags: flags,
Commands: []*cli.Command{
// Server commands
cmdServe,
cmdUser,
cmdAccess,
// Client commands
cmdPublish,
cmdSubscribe,
},
}
}
func execRun(c *cli.Context) error {
// Read all the options
listenHTTP := c.String("listen-http")
keepaliveInterval := c.Duration("keepalive-interval")
// Run main bot, can be killed by signal
conf := config.New(listenHTTP)
conf.KeepaliveInterval = keepaliveInterval
s := server.New(conf)
if err := s.Run(); err != nil {
log.Fatalln(err)
}
log.Printf("Exiting.")
return nil
}
// initConfigFileInputSource is like altsrc.InitInputSourceWithContext and altsrc.NewYamlSourceFromFlagFunc, but checks
// if the config flag is exists and only loads it if it does. If the flag is set and the file exists, it fails.
func initConfigFileInputSource(configFlag string, flags []cli.Flag) cli.BeforeFunc {
return func(context *cli.Context) error {
configFile := context.String(configFlag)
if context.IsSet(configFlag) && !fileExists(configFile) {
if context.IsSet(configFlag) && !util.FileExists(configFile) {
return fmt.Errorf("config file %s does not exist", configFile)
} else if !context.IsSet(configFlag) && !fileExists(configFile) {
} else if !context.IsSet(configFlag) && !util.FileExists(configFile) {
return nil
}
inputSource, err := altsrc.NewYamlSourceFromFile(configFile)
@@ -69,8 +60,3 @@ func initConfigFileInputSource(configFlag string, flags []cli.Flag) cli.BeforeFu
return altsrc.ApplyInputSourceValues(context, inputSource, flags)
}
}
func fileExists(filename string) bool {
stat, _ := os.Stat(filename)
return stat != nil
}

35
cmd/app_test.go Normal file
View File

@@ -0,0 +1,35 @@
package cmd
import (
"bytes"
"encoding/json"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/client"
"os"
"strings"
"testing"
)
// This only contains helpers so far
func TestMain(m *testing.M) {
// log.SetOutput(io.Discard)
os.Exit(m.Run())
}
func newTestApp() (*cli.App, *bytes.Buffer, *bytes.Buffer, *bytes.Buffer) {
var stdin, stdout, stderr bytes.Buffer
app := New()
app.Reader = &stdin
app.Writer = &stdout
app.ErrWriter = &stderr
return app, &stdin, &stdout, &stderr
}
func toMessage(t *testing.T, s string) *client.Message {
var m *client.Message
if err := json.NewDecoder(strings.NewReader(s)).Decode(&m); err != nil {
t.Fatal(err)
}
return m
}

183
cmd/publish.go Normal file
View File

@@ -0,0 +1,183 @@
package cmd
import (
"errors"
"fmt"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/client"
"heckel.io/ntfy/util"
"io"
"os"
"path/filepath"
"strings"
)
var cmdPublish = &cli.Command{
Name: "publish",
Aliases: []string{"pub", "send", "trigger"},
Usage: "Send message via a ntfy server",
UsageText: "ntfy send [OPTIONS..] TOPIC [MESSAGE]\n NTFY_TOPIC=.. ntfy send [OPTIONS..] -P [MESSAGE]",
Action: execPublish,
Category: categoryClient,
Flags: []cli.Flag{
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG"}, Usage: "client config file"},
&cli.StringFlag{Name: "title", Aliases: []string{"t"}, EnvVars: []string{"NTFY_TITLE"}, Usage: "message title"},
&cli.StringFlag{Name: "priority", Aliases: []string{"p"}, EnvVars: []string{"NTFY_PRIORITY"}, Usage: "priority of the message (1=min, 2=low, 3=default, 4=high, 5=max)"},
&cli.StringFlag{Name: "tags", Aliases: []string{"tag", "T"}, EnvVars: []string{"NTFY_TAGS"}, Usage: "comma separated list of tags and emojis"},
&cli.StringFlag{Name: "delay", Aliases: []string{"at", "in", "D"}, EnvVars: []string{"NTFY_DELAY"}, Usage: "delay/schedule message"},
&cli.StringFlag{Name: "click", Aliases: []string{"U"}, EnvVars: []string{"NTFY_CLICK"}, Usage: "URL to open when notification is clicked"},
&cli.StringFlag{Name: "actions", Aliases: []string{"A"}, EnvVars: []string{"NTFY_ACTIONS"}, Usage: "actions JSON array or simple definition"},
&cli.StringFlag{Name: "attach", Aliases: []string{"a"}, EnvVars: []string{"NTFY_ATTACH"}, Usage: "URL to send as an external attachment"},
&cli.StringFlag{Name: "filename", Aliases: []string{"name", "n"}, EnvVars: []string{"NTFY_FILENAME"}, Usage: "filename for the attachment"},
&cli.StringFlag{Name: "file", Aliases: []string{"f"}, EnvVars: []string{"NTFY_FILE"}, Usage: "file to upload as an attachment"},
&cli.StringFlag{Name: "email", Aliases: []string{"mail", "e"}, EnvVars: []string{"NTFY_EMAIL"}, Usage: "also send to e-mail address"},
&cli.StringFlag{Name: "user", Aliases: []string{"u"}, EnvVars: []string{"NTFY_USER"}, Usage: "username[:password] used to auth against the server"},
&cli.BoolFlag{Name: "no-cache", Aliases: []string{"C"}, EnvVars: []string{"NTFY_NO_CACHE"}, Usage: "do not cache message server-side"},
&cli.BoolFlag{Name: "no-firebase", Aliases: []string{"F"}, EnvVars: []string{"NTFY_NO_FIREBASE"}, Usage: "do not forward message to Firebase"},
&cli.BoolFlag{Name: "env-topic", Aliases: []string{"P"}, EnvVars: []string{"NTFY_ENV_TOPIC"}, Usage: "use topic from NTFY_TOPIC env variable"},
&cli.BoolFlag{Name: "quiet", Aliases: []string{"q"}, EnvVars: []string{"NTFY_QUIET"}, Usage: "do print message"},
},
Description: `Publish a message to a ntfy server.
Examples:
ntfy publish mytopic This is my message # Send simple message
ntfy send myserver.com/mytopic "This is my message" # Send message to different default host
ntfy pub -p high backups "Backups failed" # Send high priority message
ntfy pub --tags=warning,skull backups "Backups failed" # Add tags/emojis to message
ntfy pub --delay=10s delayed_topic Laterzz # Delay message by 10s
ntfy pub --at=8:30am delayed_topic Laterzz # Send message at 8:30am
ntfy pub -e phil@example.com alerts 'App is down!' # Also send email to phil@example.com
ntfy pub --click="https://reddit.com" redd 'New msg' # Opens Reddit when notification is clicked
ntfy pub --attach="http://some.tld/file.zip" files # Send ZIP archive from URL as attachment
ntfy pub --file=flower.jpg flowers 'Nice!' # Send image.jpg as attachment
ntfy pub -u phil:mypass secret Psst # Publish with username/password
NTFY_USER=phil:mypass ntfy pub secret Psst # Use env variables to set username/password
NTFY_TOPIC=mytopic ntfy pub -P "some message"" # Use NTFY_TOPIC variable as topic
cat flower.jpg | ntfy pub --file=- flowers 'Nice!' # Same as above, send image.jpg as attachment
ntfy trigger mywebhook # Sending without message, useful for webhooks
Please also check out the docs on publishing messages. Especially for the --tags and --delay options,
it has incredibly useful information: https://ntfy.sh/docs/publish/.
The default config file for all client commands is /etc/ntfy/client.yml (if root user),
or ~/.config/ntfy/client.yml for all other users.`,
}
func execPublish(c *cli.Context) error {
conf, err := loadConfig(c)
if err != nil {
return err
}
title := c.String("title")
priority := c.String("priority")
tags := c.String("tags")
delay := c.String("delay")
click := c.String("click")
actions := c.String("actions")
attach := c.String("attach")
filename := c.String("filename")
file := c.String("file")
email := c.String("email")
user := c.String("user")
noCache := c.Bool("no-cache")
noFirebase := c.Bool("no-firebase")
envTopic := c.Bool("env-topic")
quiet := c.Bool("quiet")
var topic, message string
if envTopic {
topic = os.Getenv("NTFY_TOPIC")
if c.NArg() > 0 {
message = strings.Join(c.Args().Slice(), " ")
}
} else {
if c.NArg() < 1 {
return errors.New("must specify topic, type 'ntfy publish --help' for help")
}
topic = c.Args().Get(0)
if c.NArg() > 1 {
message = strings.Join(c.Args().Slice()[1:], " ")
}
}
var options []client.PublishOption
if title != "" {
options = append(options, client.WithTitle(title))
}
if priority != "" {
options = append(options, client.WithPriority(priority))
}
if tags != "" {
options = append(options, client.WithTagsList(tags))
}
if delay != "" {
options = append(options, client.WithDelay(delay))
}
if click != "" {
options = append(options, client.WithClick(click))
}
if actions != "" {
options = append(options, client.WithActions(strings.ReplaceAll(actions, "\n", " ")))
}
if attach != "" {
options = append(options, client.WithAttach(attach))
}
if filename != "" {
options = append(options, client.WithFilename(filename))
}
if email != "" {
options = append(options, client.WithEmail(email))
}
if noCache {
options = append(options, client.WithNoCache())
}
if noFirebase {
options = append(options, client.WithNoFirebase())
}
if user != "" {
var pass string
parts := strings.SplitN(user, ":", 2)
if len(parts) == 2 {
user = parts[0]
pass = parts[1]
} else {
fmt.Fprint(c.App.ErrWriter, "Enter Password: ")
p, err := util.ReadPassword(c.App.Reader)
if err != nil {
return err
}
pass = string(p)
fmt.Fprintf(c.App.ErrWriter, "\r%s\r", strings.Repeat(" ", 20))
}
options = append(options, client.WithBasicAuth(user, pass))
}
var body io.Reader
if file == "" {
body = strings.NewReader(message)
} else {
if message != "" {
options = append(options, client.WithMessage(message))
}
if file == "-" {
if filename == "" {
options = append(options, client.WithFilename("stdin"))
}
body = c.App.Reader
} else {
if filename == "" {
options = append(options, client.WithFilename(filepath.Base(file)))
}
body, err = os.Open(file)
if err != nil {
return err
}
}
}
cl := client.New(conf)
m, err := cl.PublishReader(topic, body, options...)
if err != nil {
return err
}
if !quiet {
fmt.Fprintln(c.App.Writer, strings.TrimSpace(m.Raw))
}
return nil
}

72
cmd/publish_test.go Normal file
View File

@@ -0,0 +1,72 @@
package cmd
import (
"fmt"
"github.com/stretchr/testify/require"
"heckel.io/ntfy/test"
"heckel.io/ntfy/util"
"testing"
)
func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) {
testMessage := util.RandomString(10)
app, _, _, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "publish", "ntfytest", "ntfy unit test " + testMessage}))
app2, _, stdout, _ := newTestApp()
require.Nil(t, app2.Run([]string{"ntfy", "subscribe", "--poll", "ntfytest"}))
require.Contains(t, stdout.String(), testMessage)
}
func TestCLI_Publish_Subscribe_Poll(t *testing.T) {
s, port := test.StartServer(t)
defer test.StopServer(t, s, port)
topic := fmt.Sprintf("http://127.0.0.1:%d/mytopic", port)
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "publish", topic, "some message"}))
m := toMessage(t, stdout.String())
require.Equal(t, "some message", m.Message)
app2, _, stdout, _ := newTestApp()
require.Nil(t, app2.Run([]string{"ntfy", "subscribe", "--poll", topic}))
m = toMessage(t, stdout.String())
require.Equal(t, "some message", m.Message)
}
func TestCLI_Publish_All_The_Things(t *testing.T) {
s, port := test.StartServer(t)
defer test.StopServer(t, s, port)
topic := fmt.Sprintf("http://127.0.0.1:%d/mytopic", port)
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{
"ntfy", "publish",
"--title", "this is a title",
"--priority", "high",
"--tags", "tag1,tag2",
// No --delay, --email
"--click", "https://ntfy.sh",
"--attach", "https://f-droid.org/F-Droid.apk",
"--filename", "fdroid.apk",
"--no-cache",
"--no-firebase",
topic,
"some message",
}))
m := toMessage(t, stdout.String())
require.Equal(t, "message", m.Event)
require.Equal(t, "mytopic", m.Topic)
require.Equal(t, "some message", m.Message)
require.Equal(t, "this is a title", m.Title)
require.Equal(t, 4, m.Priority)
require.Equal(t, []string{"tag1", "tag2"}, m.Tags)
require.Equal(t, "https://ntfy.sh", m.Click)
require.Equal(t, "https://f-droid.org/F-Droid.apk", m.Attachment.URL)
require.Equal(t, "fdroid.apk", m.Attachment.Name)
require.Equal(t, int64(0), m.Attachment.Size)
require.Equal(t, "", m.Attachment.Owner)
require.Equal(t, int64(0), m.Attachment.Expires)
require.Equal(t, "", m.Attachment.Type)
}

246
cmd/serve.go Normal file
View File

@@ -0,0 +1,246 @@
package cmd
import (
"errors"
"fmt"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"heckel.io/ntfy/server"
"heckel.io/ntfy/util"
"log"
"math"
"net"
"strings"
"time"
)
var flagsServe = []cli.Flag{
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: "/etc/ntfy/server.yml", DefaultText: "/etc/ntfy/server.yml", Usage: "config file"},
altsrc.NewStringFlag(&cli.StringFlag{Name: "base-url", Aliases: []string{"B"}, EnvVars: []string{"NTFY_BASE_URL"}, Usage: "externally visible base URL for this host (e.g. https://ntfy.sh)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-http", Aliases: []string{"l"}, EnvVars: []string{"NTFY_LISTEN_HTTP"}, Value: server.DefaultListenHTTP, Usage: "ip:port used to as HTTP listen address"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-https", Aliases: []string{"L"}, EnvVars: []string{"NTFY_LISTEN_HTTPS"}, Usage: "ip:port used to as HTTPS listen address"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-unix", Aliases: []string{"U"}, EnvVars: []string{"NTFY_LISTEN_UNIX"}, Usage: "listen on unix socket path"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "key-file", Aliases: []string{"K"}, EnvVars: []string{"NTFY_KEY_FILE"}, Usage: "private key file, if listen-https is set"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "cert-file", Aliases: []string{"E"}, EnvVars: []string{"NTFY_CERT_FILE"}, Usage: "certificate file, if listen-https is set"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "firebase-key-file", Aliases: []string{"F"}, EnvVars: []string{"NTFY_FIREBASE_KEY_FILE"}, Usage: "Firebase credentials file; if set additionally publish to FCM topic"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-file", Aliases: []string{"C"}, EnvVars: []string{"NTFY_CACHE_FILE"}, Usage: "cache file used for message caching"}),
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "cache-duration", Aliases: []string{"b"}, EnvVars: []string{"NTFY_CACHE_DURATION"}, Value: server.DefaultCacheDuration, Usage: "buffer messages for this time to allow `since` requests"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-cache-dir", EnvVars: []string{"NTFY_ATTACHMENT_CACHE_DIR"}, Usage: "cache directory for attached files"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-total-size-limit", Aliases: []string{"A"}, EnvVars: []string{"NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT"}, DefaultText: "5G", Usage: "limit of the on-disk attachment cache"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-file-size-limit", Aliases: []string{"Y"}, EnvVars: []string{"NTFY_ATTACHMENT_FILE_SIZE_LIMIT"}, DefaultText: "15M", Usage: "per-file attachment size limit (e.g. 300k, 2M, 100M)"}),
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "attachment-expiry-duration", Aliases: []string{"X"}, EnvVars: []string{"NTFY_ATTACHMENT_EXPIRY_DURATION"}, Value: server.DefaultAttachmentExpiryDuration, DefaultText: "3h", Usage: "duration after which uploaded attachments will be deleted (e.g. 3h, 20h)"}),
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "keepalive-interval", Aliases: []string{"k"}, EnvVars: []string{"NTFY_KEEPALIVE_INTERVAL"}, Value: server.DefaultKeepaliveInterval, Usage: "interval of keepalive messages"}),
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "manager-interval", Aliases: []string{"m"}, EnvVars: []string{"NTFY_MANAGER_INTERVAL"}, Value: server.DefaultManagerInterval, Usage: "interval of for message pruning and stats printing"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-root", EnvVars: []string{"NTFY_WEB_ROOT"}, Value: "app", Usage: "sets web root to landing page (home) or web app (app)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-addr", EnvVars: []string{"NTFY_SMTP_SENDER_ADDR"}, Usage: "SMTP server address (host:port) for outgoing emails"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-user", EnvVars: []string{"NTFY_SMTP_SENDER_USER"}, Usage: "SMTP user (if e-mail sending is enabled)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-pass", EnvVars: []string{"NTFY_SMTP_SENDER_PASS"}, Usage: "SMTP password (if e-mail sending is enabled)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-from", EnvVars: []string{"NTFY_SMTP_SENDER_FROM"}, Usage: "SMTP sender address (if e-mail sending is enabled)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-listen", EnvVars: []string{"NTFY_SMTP_SERVER_LISTEN"}, Usage: "SMTP server address (ip:port) for incoming emails, e.g. :25"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-domain", EnvVars: []string{"NTFY_SMTP_SERVER_DOMAIN"}, Usage: "SMTP domain for incoming e-mail, e.g. ntfy.sh"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-addr-prefix", EnvVars: []string{"NTFY_SMTP_SERVER_ADDR_PREFIX"}, Usage: "SMTP email address prefix for topics to prevent spam (e.g. 'ntfy-')"}),
altsrc.NewIntFlag(&cli.IntFlag{Name: "global-topic-limit", Aliases: []string{"T"}, EnvVars: []string{"NTFY_GLOBAL_TOPIC_LIMIT"}, Value: server.DefaultTotalTopicLimit, Usage: "total number of topics allowed"}),
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-subscription-limit", EnvVars: []string{"NTFY_VISITOR_SUBSCRIPTION_LIMIT"}, Value: server.DefaultVisitorSubscriptionLimit, Usage: "number of subscriptions per visitor"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-attachment-total-size-limit", EnvVars: []string{"NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: "100M", Usage: "total storage limit used for attachments per visitor"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-attachment-daily-bandwidth-limit", EnvVars: []string{"NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT"}, Value: "500M", Usage: "total daily attachment download/upload bandwidth limit per visitor"}),
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-request-limit-burst", EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_BURST"}, Value: server.DefaultVisitorRequestLimitBurst, Usage: "initial limit of requests per visitor"}),
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "visitor-request-limit-replenish", EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_REPLENISH"}, Value: server.DefaultVisitorRequestLimitReplenish, Usage: "interval at which burst limit is replenished (one per x)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-request-limit-exempt-hosts", EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS"}, Value: "", Usage: "hostnames and/or IP addresses of hosts that will be exempt from the visitor request limit"}),
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-email-limit-burst", EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_BURST"}, Value: server.DefaultVisitorEmailLimitBurst, Usage: "initial limit of e-mails per visitor"}),
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "visitor-email-limit-replenish", EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_REPLENISH"}, Value: server.DefaultVisitorEmailLimitReplenish, Usage: "interval at which burst limit is replenished (one per x)"}),
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "behind-proxy", Aliases: []string{"P"}, EnvVars: []string{"NTFY_BEHIND_PROXY"}, Value: false, Usage: "if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting)"}),
}
var cmdServe = &cli.Command{
Name: "serve",
Usage: "Run the ntfy server",
UsageText: "ntfy serve [OPTIONS..]",
Action: execServe,
Category: categoryServer,
Flags: flagsServe,
Before: initConfigFileInputSource("config", flagsServe),
Description: `Run the ntfy server and listen for incoming requests
The command will load the configuration from /etc/ntfy/server.yml. Config options can
be overridden using the command line options.
Examples:
ntfy serve # Starts server in the foreground (on port 80)
ntfy serve --listen-http :8080 # Starts server with alternate port`,
}
func execServe(c *cli.Context) error {
if c.NArg() > 0 {
return errors.New("no arguments expected, see 'ntfy serve --help' for help")
}
// Read all the options
baseURL := c.String("base-url")
listenHTTP := c.String("listen-http")
listenHTTPS := c.String("listen-https")
listenUnix := c.String("listen-unix")
keyFile := c.String("key-file")
certFile := c.String("cert-file")
firebaseKeyFile := c.String("firebase-key-file")
cacheFile := c.String("cache-file")
cacheDuration := c.Duration("cache-duration")
authFile := c.String("auth-file")
authDefaultAccess := c.String("auth-default-access")
attachmentCacheDir := c.String("attachment-cache-dir")
attachmentTotalSizeLimitStr := c.String("attachment-total-size-limit")
attachmentFileSizeLimitStr := c.String("attachment-file-size-limit")
attachmentExpiryDuration := c.Duration("attachment-expiry-duration")
keepaliveInterval := c.Duration("keepalive-interval")
managerInterval := c.Duration("manager-interval")
webRoot := c.String("web-root")
smtpSenderAddr := c.String("smtp-sender-addr")
smtpSenderUser := c.String("smtp-sender-user")
smtpSenderPass := c.String("smtp-sender-pass")
smtpSenderFrom := c.String("smtp-sender-from")
smtpServerListen := c.String("smtp-server-listen")
smtpServerDomain := c.String("smtp-server-domain")
smtpServerAddrPrefix := c.String("smtp-server-addr-prefix")
totalTopicLimit := c.Int("global-topic-limit")
visitorSubscriptionLimit := c.Int("visitor-subscription-limit")
visitorAttachmentTotalSizeLimitStr := c.String("visitor-attachment-total-size-limit")
visitorAttachmentDailyBandwidthLimitStr := c.String("visitor-attachment-daily-bandwidth-limit")
visitorRequestLimitBurst := c.Int("visitor-request-limit-burst")
visitorRequestLimitReplenish := c.Duration("visitor-request-limit-replenish")
visitorRequestLimitExemptHosts := util.SplitNoEmpty(c.String("visitor-request-limit-exempt-hosts"), ",")
visitorEmailLimitBurst := c.Int("visitor-email-limit-burst")
visitorEmailLimitReplenish := c.Duration("visitor-email-limit-replenish")
behindProxy := c.Bool("behind-proxy")
// Check values
if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) {
return errors.New("if set, FCM key file must exist")
} else if keepaliveInterval < 5*time.Second {
return errors.New("keepalive interval cannot be lower than five seconds")
} else if managerInterval < 5*time.Second {
return errors.New("manager interval cannot be lower than five seconds")
} else if cacheDuration > 0 && cacheDuration < managerInterval {
return errors.New("cache duration cannot be lower than manager interval")
} else if keyFile != "" && !util.FileExists(keyFile) {
return errors.New("if set, key file must exist")
} else if certFile != "" && !util.FileExists(certFile) {
return errors.New("if set, certificate file must exist")
} else if listenHTTPS != "" && (keyFile == "" || certFile == "") {
return errors.New("if listen-https is set, both key-file and cert-file must be set")
} else if smtpSenderAddr != "" && (baseURL == "" || smtpSenderUser == "" || smtpSenderPass == "" || smtpSenderFrom == "") {
return errors.New("if smtp-sender-addr is set, base-url, smtp-sender-user, smtp-sender-pass and smtp-sender-from must also be set")
} else if smtpServerListen != "" && smtpServerDomain == "" {
return errors.New("if smtp-server-listen is set, smtp-server-domain must also be set")
} else if attachmentCacheDir != "" && baseURL == "" {
return errors.New("if attachment-cache-dir is set, base-url must also be set")
} else if baseURL != "" && !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") {
return errors.New("if set, base-url must start with http:// or https://")
} else if !util.InStringList([]string{"read-write", "read-only", "write-only", "deny-all"}, authDefaultAccess) {
return errors.New("if set, auth-default-access must start set to 'read-write', 'read-only', 'write-only' or 'deny-all'")
} else if !util.InStringList([]string{"app", "home"}, webRoot) {
return errors.New("if set, web-root must be 'home' or 'app'")
}
// Default auth permissions
webRootIsApp := webRoot == "app"
authDefaultRead := authDefaultAccess == "read-write" || authDefaultAccess == "read-only"
authDefaultWrite := authDefaultAccess == "read-write" || authDefaultAccess == "write-only"
// Special case: Unset default
if listenHTTP == "-" {
listenHTTP = ""
}
// Convert sizes to bytes
attachmentTotalSizeLimit, err := parseSize(attachmentTotalSizeLimitStr, server.DefaultAttachmentTotalSizeLimit)
if err != nil {
return err
}
attachmentFileSizeLimit, err := parseSize(attachmentFileSizeLimitStr, server.DefaultAttachmentFileSizeLimit)
if err != nil {
return err
}
visitorAttachmentTotalSizeLimit, err := parseSize(visitorAttachmentTotalSizeLimitStr, server.DefaultVisitorAttachmentTotalSizeLimit)
if err != nil {
return err
}
visitorAttachmentDailyBandwidthLimit, err := parseSize(visitorAttachmentDailyBandwidthLimitStr, server.DefaultVisitorAttachmentDailyBandwidthLimit)
if err != nil {
return err
} else if visitorAttachmentDailyBandwidthLimit > math.MaxInt {
return fmt.Errorf("config option visitor-attachment-daily-bandwidth-limit must be lower than %d", math.MaxInt)
}
// Resolve hosts
visitorRequestLimitExemptIPs := make([]string, 0)
for _, host := range visitorRequestLimitExemptHosts {
ips, err := net.LookupIP(host)
if err != nil {
log.Printf("cannot resolve host %s: %s, ignoring visitor request exemption", host, err.Error())
continue
}
for _, ip := range ips {
visitorRequestLimitExemptIPs = append(visitorRequestLimitExemptIPs, ip.String())
}
}
// Run server
conf := server.NewConfig()
conf.BaseURL = baseURL
conf.ListenHTTP = listenHTTP
conf.ListenHTTPS = listenHTTPS
conf.ListenUnix = listenUnix
conf.KeyFile = keyFile
conf.CertFile = certFile
conf.FirebaseKeyFile = firebaseKeyFile
conf.CacheFile = cacheFile
conf.CacheDuration = cacheDuration
conf.AuthFile = authFile
conf.AuthDefaultRead = authDefaultRead
conf.AuthDefaultWrite = authDefaultWrite
conf.AttachmentCacheDir = attachmentCacheDir
conf.AttachmentTotalSizeLimit = attachmentTotalSizeLimit
conf.AttachmentFileSizeLimit = attachmentFileSizeLimit
conf.AttachmentExpiryDuration = attachmentExpiryDuration
conf.KeepaliveInterval = keepaliveInterval
conf.ManagerInterval = managerInterval
conf.WebRootIsApp = webRootIsApp
conf.SMTPSenderAddr = smtpSenderAddr
conf.SMTPSenderUser = smtpSenderUser
conf.SMTPSenderPass = smtpSenderPass
conf.SMTPSenderFrom = smtpSenderFrom
conf.SMTPServerListen = smtpServerListen
conf.SMTPServerDomain = smtpServerDomain
conf.SMTPServerAddrPrefix = smtpServerAddrPrefix
conf.TotalTopicLimit = totalTopicLimit
conf.VisitorSubscriptionLimit = visitorSubscriptionLimit
conf.VisitorAttachmentTotalSizeLimit = visitorAttachmentTotalSizeLimit
conf.VisitorAttachmentDailyBandwidthLimit = int(visitorAttachmentDailyBandwidthLimit)
conf.VisitorRequestLimitBurst = visitorRequestLimitBurst
conf.VisitorRequestLimitReplenish = visitorRequestLimitReplenish
conf.VisitorRequestExemptIPAddrs = visitorRequestLimitExemptIPs
conf.VisitorEmailLimitBurst = visitorEmailLimitBurst
conf.VisitorEmailLimitReplenish = visitorEmailLimitReplenish
conf.BehindProxy = behindProxy
s, err := server.New(conf)
if err != nil {
log.Fatalln(err)
}
if err := s.Run(); err != nil {
log.Fatalln(err)
}
log.Printf("Exiting.")
return nil
}
func parseSize(s string, defaultValue int64) (v int64, err error) {
if s == "" {
return defaultValue, nil
}
v, err = util.ParseSize(s)
if err != nil {
return 0, err
}
return v, nil
}

77
cmd/serve_test.go Normal file
View File

@@ -0,0 +1,77 @@
package cmd
import (
"fmt"
"github.com/gorilla/websocket"
"github.com/stretchr/testify/require"
"heckel.io/ntfy/client"
"heckel.io/ntfy/test"
"heckel.io/ntfy/util"
"math/rand"
"os"
"os/exec"
"path/filepath"
"testing"
"time"
)
func init() {
rand.Seed(time.Now().UnixMilli())
}
func TestCLI_Serve_Unix_Curl(t *testing.T) {
sockFile := filepath.Join(t.TempDir(), "ntfy.sock")
configFile := newEmptyFile(t) // Avoid issues with existing server.yml file on system
go func() {
app, _, _, _ := newTestApp()
err := app.Run([]string{"ntfy", "serve", "--config=" + configFile, "--listen-http=-", "--listen-unix=" + sockFile})
require.Nil(t, err)
}()
for i := 0; i < 40 && !util.FileExists(sockFile); i++ {
time.Sleep(50 * time.Millisecond)
}
require.True(t, util.FileExists(sockFile))
cmd := exec.Command("curl", "-s", "--unix-socket", sockFile, "-d", "this is a message", "localhost/mytopic")
out, err := cmd.Output()
require.Nil(t, err)
m := toMessage(t, string(out))
require.Equal(t, "this is a message", m.Message)
}
func TestCLI_Serve_WebSocket(t *testing.T) {
port := 10000 + rand.Intn(20000)
go func() {
configFile := newEmptyFile(t) // Avoid issues with existing server.yml file on system
app, _, _, _ := newTestApp()
err := app.Run([]string{"ntfy", "serve", "--config=" + configFile, fmt.Sprintf("--listen-http=:%d", port)})
require.Nil(t, err)
}()
test.WaitForPortUp(t, port)
ws, _, err := websocket.DefaultDialer.Dial(fmt.Sprintf("ws://127.0.0.1:%d/mytopic/ws", port), nil)
require.Nil(t, err)
messageType, data, err := ws.ReadMessage()
require.Nil(t, err)
require.Equal(t, websocket.TextMessage, messageType)
require.Equal(t, "open", toMessage(t, string(data)).Event)
c := client.New(client.NewConfig())
_, err = c.Publish(fmt.Sprintf("http://127.0.0.1:%d/mytopic", port), "my message")
require.Nil(t, err)
messageType, data, err = ws.ReadMessage()
require.Nil(t, err)
require.Equal(t, websocket.TextMessage, messageType)
m := toMessage(t, string(data))
require.Equal(t, "my message", m.Message)
require.Equal(t, "mytopic", m.Topic)
}
func newEmptyFile(t *testing.T) string {
filename := filepath.Join(t.TempDir(), "empty")
require.Nil(t, os.WriteFile(filename, []byte{}, 0600))
return filename
}

261
cmd/subscribe.go Normal file
View File

@@ -0,0 +1,261 @@
package cmd
import (
"errors"
"fmt"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/client"
"heckel.io/ntfy/util"
"log"
"os"
"os/exec"
"os/user"
"strings"
)
var cmdSubscribe = &cli.Command{
Name: "subscribe",
Aliases: []string{"sub"},
Usage: "Subscribe to one or more topics on a ntfy server",
UsageText: "ntfy subscribe [OPTIONS..] [TOPIC]",
Action: execSubscribe,
Category: categoryClient,
Flags: []cli.Flag{
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "client config file"},
&cli.StringFlag{Name: "since", Aliases: []string{"s"}, Usage: "return events since `SINCE` (Unix timestamp, or all)"},
&cli.StringFlag{Name: "user", Aliases: []string{"u"}, Usage: "username[:password] used to auth against the server"},
&cli.BoolFlag{Name: "from-config", Aliases: []string{"C"}, Usage: "read subscriptions from config file (service mode)"},
&cli.BoolFlag{Name: "poll", Aliases: []string{"p"}, Usage: "return events and exit, do not listen for new events"},
&cli.BoolFlag{Name: "scheduled", Aliases: []string{"sched", "S"}, Usage: "also return scheduled/delayed events"},
&cli.BoolFlag{Name: "verbose", Aliases: []string{"v"}, Usage: "print verbose output"},
},
Description: `Subscribe to a topic from a ntfy server, and either print or execute a command for
every arriving message. There are 3 modes in which the command can be run:
ntfy subscribe TOPIC
This prints the JSON representation of every incoming message. It is useful when you
have a command that wants to stream-read incoming JSON messages. Unless --poll is passed,
this command stays open forever.
Examples:
ntfy subscribe mytopic # Prints JSON for incoming messages for ntfy.sh/mytopic
ntfy sub home.lan/backups # Subscribe to topic on different server
ntfy sub --poll home.lan/backups # Just query for latest messages and exit
ntfy sub -u phil:mypass secret # Subscribe with username/password
ntfy subscribe TOPIC COMMAND
This executes COMMAND for every incoming messages. The message fields are passed to the
command as environment variables:
Variable Aliases Description
--------------- --------------------- -----------------------------------
$NTFY_ID $id Unique message ID
$NTFY_TIME $time Unix timestamp of the message delivery
$NTFY_TOPIC $topic Topic name
$NTFY_MESSAGE $message, $m Message body
$NTFY_TITLE $title, $t Message title
$NTFY_PRIORITY $priority, $prio, $p Message priority (1=min, 5=max)
$NTFY_TAGS $tags, $tag, $ta Message tags (comma separated list)
$NTFY_RAW $raw Raw JSON message
Examples:
ntfy sub mytopic 'notify-send "$m"' # Execute command for incoming messages
ntfy sub topic1 /my/script.sh # Execute script for incoming messages
ntfy subscribe --from-config
Service mode (used in ntfy-client.service). This reads the config file (/etc/ntfy/client.yml
or ~/.config/ntfy/client.yml) and sets up subscriptions for every topic in the "subscribe:"
block (see config file).
Examples:
ntfy sub --from-config # Read topics from config file
ntfy sub --config=/my/client.yml --from-config # Read topics from alternate config file
The default config file for all client commands is /etc/ntfy/client.yml (if root user),
or ~/.config/ntfy/client.yml for all other users.`,
}
func execSubscribe(c *cli.Context) error {
// Read config and options
conf, err := loadConfig(c)
if err != nil {
return err
}
cl := client.New(conf)
since := c.String("since")
user := c.String("user")
poll := c.Bool("poll")
scheduled := c.Bool("scheduled")
fromConfig := c.Bool("from-config")
topic := c.Args().Get(0)
command := c.Args().Get(1)
if !fromConfig {
conf.Subscribe = nil // wipe if --from-config not passed
}
var options []client.SubscribeOption
if since != "" {
options = append(options, client.WithSince(since))
}
if user != "" {
var pass string
parts := strings.SplitN(user, ":", 2)
if len(parts) == 2 {
user = parts[0]
pass = parts[1]
} else {
fmt.Fprint(c.App.ErrWriter, "Enter Password: ")
p, err := util.ReadPassword(c.App.Reader)
if err != nil {
return err
}
pass = string(p)
fmt.Fprintf(c.App.ErrWriter, "\r%s\r", strings.Repeat(" ", 20))
}
options = append(options, client.WithBasicAuth(user, pass))
}
if poll {
options = append(options, client.WithPoll())
}
if scheduled {
options = append(options, client.WithScheduled())
}
if topic == "" && len(conf.Subscribe) == 0 {
return errors.New("must specify topic, type 'ntfy subscribe --help' for help")
}
// Execute poll or subscribe
if poll {
return doPoll(c, cl, conf, topic, command, options...)
}
return doSubscribe(c, cl, conf, topic, command, options...)
}
func doPoll(c *cli.Context, cl *client.Client, conf *client.Config, topic, command string, options ...client.SubscribeOption) error {
for _, s := range conf.Subscribe { // may be nil
if err := doPollSingle(c, cl, s.Topic, s.Command, options...); err != nil {
return err
}
}
if topic != "" {
if err := doPollSingle(c, cl, topic, command, options...); err != nil {
return err
}
}
return nil
}
func doPollSingle(c *cli.Context, cl *client.Client, topic, command string, options ...client.SubscribeOption) error {
messages, err := cl.Poll(topic, options...)
if err != nil {
return err
}
for _, m := range messages {
printMessageOrRunCommand(c, m, command)
}
return nil
}
func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic, command string, options ...client.SubscribeOption) error {
commands := make(map[string]string) // Subscription ID -> command
for _, s := range conf.Subscribe { // May be nil
topicOptions := append(make([]client.SubscribeOption, 0), options...)
for filter, value := range s.If {
topicOptions = append(topicOptions, client.WithFilter(filter, value))
}
if s.User != "" && s.Password != "" {
topicOptions = append(topicOptions, client.WithBasicAuth(s.User, s.Password))
}
subscriptionID := cl.Subscribe(s.Topic, topicOptions...)
commands[subscriptionID] = s.Command
}
if topic != "" {
subscriptionID := cl.Subscribe(topic, options...)
commands[subscriptionID] = command
}
for m := range cl.Messages {
command, ok := commands[m.SubscriptionID]
if !ok {
continue
}
printMessageOrRunCommand(c, m, command)
}
return nil
}
func printMessageOrRunCommand(c *cli.Context, m *client.Message, command string) {
if command != "" {
runCommand(c, command, m)
} else {
fmt.Fprintln(c.App.Writer, m.Raw)
}
}
func runCommand(c *cli.Context, command string, m *client.Message) {
if err := runCommandInternal(c, command, m); err != nil {
fmt.Fprintf(c.App.ErrWriter, "Command failed: %s\n", err.Error())
}
}
func runCommandInternal(c *cli.Context, command string, m *client.Message) error {
scriptFile, err := createTmpScript(command)
if err != nil {
return err
}
defer os.Remove(scriptFile)
verbose := c.Bool("verbose")
if verbose {
log.Printf("[%s] Executing: %s (for message: %s)", util.ShortTopicURL(m.TopicURL), command, m.Raw)
}
cmd := exec.Command("sh", "-c", scriptFile)
cmd.Stdin = c.App.Reader
cmd.Stdout = c.App.Writer
cmd.Stderr = c.App.ErrWriter
cmd.Env = envVars(m)
return cmd.Run()
}
func createTmpScript(command string) (string, error) {
scriptFile := fmt.Sprintf("%s/ntfy-subscribe-%s.sh.tmp", os.TempDir(), util.RandomString(10))
script := fmt.Sprintf("#!/bin/sh\n%s", command)
if err := os.WriteFile(scriptFile, []byte(script), 0700); err != nil {
return "", err
}
return scriptFile, nil
}
func envVars(m *client.Message) []string {
env := os.Environ()
env = append(env, envVar(m.ID, "NTFY_ID", "id")...)
env = append(env, envVar(m.Topic, "NTFY_TOPIC", "topic")...)
env = append(env, envVar(fmt.Sprintf("%d", m.Time), "NTFY_TIME", "time")...)
env = append(env, envVar(m.Message, "NTFY_MESSAGE", "message", "m")...)
env = append(env, envVar(m.Title, "NTFY_TITLE", "title", "t")...)
env = append(env, envVar(fmt.Sprintf("%d", m.Priority), "NTFY_PRIORITY", "priority", "prio", "p")...)
env = append(env, envVar(strings.Join(m.Tags, ","), "NTFY_TAGS", "tags", "tag", "ta")...)
env = append(env, envVar(m.Raw, "NTFY_RAW", "raw")...)
return env
}
func envVar(value string, vars ...string) []string {
env := make([]string, 0)
for _, v := range vars {
env = append(env, fmt.Sprintf("%s=%s", v, value))
}
return env
}
func loadConfig(c *cli.Context) (*client.Config, error) {
filename := c.String("config")
if filename != "" {
return client.LoadConfig(filename)
}
u, _ := user.Current()
configFile := defaultClientRootConfigFile
if u.Uid != "0" {
configFile = util.ExpandHome(defaultClientUserConfigFile)
}
if s, _ := os.Stat(configFile); s != nil {
return client.LoadConfig(configFile)
}
return client.NewConfig(), nil
}

272
cmd/user.go Normal file
View File

@@ -0,0 +1,272 @@
package cmd
import (
"crypto/subtle"
"errors"
"fmt"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"heckel.io/ntfy/auth"
"heckel.io/ntfy/util"
"strings"
)
var flagsUser = userCommandFlags()
var cmdUser = &cli.Command{
Name: "user",
Usage: "Manage/show users",
UsageText: "ntfy user [list|add|remove|change-pass|change-role] ...",
Flags: flagsUser,
Before: initConfigFileInputSource("config", flagsUser),
Category: categoryServer,
Subcommands: []*cli.Command{
{
Name: "add",
Aliases: []string{"a"},
Usage: "Adds a new user",
UsageText: "ntfy user add [--role=admin|user] USERNAME",
Action: execUserAdd,
Flags: []cli.Flag{
&cli.StringFlag{Name: "role", Aliases: []string{"r"}, Value: string(auth.RoleUser), Usage: "user role"},
},
Description: `Add a new user to the ntfy user database.
A user can be either a regular user, or an admin. A regular user has no read or write access (unless
granted otherwise by the auth-default-access setting). An admin user has read and write access to all
topics.
Examples:
ntfy user add phil # Add regular user phil
ntfy user add --role=admin phil # Add admin user phil
`,
},
{
Name: "remove",
Aliases: []string{"del", "rm"},
Usage: "Removes a user",
UsageText: "ntfy user remove USERNAME",
Action: execUserDel,
Description: `Remove a user from the ntfy user database.
Example:
ntfy user del phil
`,
},
{
Name: "change-pass",
Aliases: []string{"chp"},
Usage: "Changes a user's password",
UsageText: "ntfy user change-pass USERNAME",
Action: execUserChangePass,
Description: `Change the password for the given user.
The new password will be read from STDIN, and it'll be confirmed by typing
it twice.
Example:
ntfy user change-pass phil
`,
},
{
Name: "change-role",
Aliases: []string{"chr"},
Usage: "Changes the role of a user",
UsageText: "ntfy user change-role USERNAME ROLE",
Action: execUserChangeRole,
Description: `Change the role for the given user to admin or user.
This command can be used to change the role of a user either from a regular user
to an admin user, or the other way around:
- admin: an admin has read/write access to all topics
- user: a regular user only has access to what was explicitly granted via 'ntfy access'
When changing the role of a user to "admin", all access control entries for that
user are removed, since they are no longer necessary.
Example:
ntfy user change-role phil admin # Make user phil an admin
ntfy user change-role phil user # Remove admin role from user phil
`,
},
{
Name: "list",
Aliases: []string{"l"},
Usage: "Shows a list of users",
Action: execUserList,
Description: `Shows a list of all configured users, including the everyone ('*') user.
This is a server-only command. It directly reads from the user.db as defined in the server config
file server.yml. The command only works if 'auth-file' is properly defined.
This command is an alias to calling 'ntfy access' (display access control list).
`,
},
},
Description: `Manage users of the ntfy server.
This is a server-only command. It directly manages the user.db as defined in the server config
file server.yml. The command only works if 'auth-file' is properly defined. Please also refer
to the related command 'ntfy access'.
The command allows you to add/remove/change users in the ntfy user database, as well as change
passwords or roles.
Examples:
ntfy user list # Shows list of users (alias: 'ntfy access')
ntfy user add phil # Add regular user phil
ntfy user add --role=admin phil # Add admin user phil
ntfy user del phil # Delete user phil
ntfy user change-pass phil # Change password for user phil
ntfy user change-role phil admin # Make user phil an admin
`,
}
func execUserAdd(c *cli.Context) error {
username := c.Args().Get(0)
role := auth.Role(c.String("role"))
if username == "" {
return errors.New("username expected, type 'ntfy user add --help' for help")
} else if username == userEveryone {
return errors.New("username not allowed")
} else if !auth.AllowedRole(role) {
return errors.New("role must be either 'user' or 'admin'")
}
manager, err := createAuthManager(c)
if err != nil {
return err
}
if user, _ := manager.User(username); user != nil {
return fmt.Errorf("user %s already exists", username)
}
password, err := readPasswordAndConfirm(c)
if err != nil {
return err
}
if err := manager.AddUser(username, password, role); err != nil {
return err
}
fmt.Fprintf(c.App.ErrWriter, "user %s added with role %s\n", username, role)
return nil
}
func execUserDel(c *cli.Context) error {
username := c.Args().Get(0)
if username == "" {
return errors.New("username expected, type 'ntfy user del --help' for help")
} else if username == userEveryone {
return errors.New("username not allowed")
}
manager, err := createAuthManager(c)
if err != nil {
return err
}
if _, err := manager.User(username); err == auth.ErrNotFound {
return fmt.Errorf("user %s does not exist", username)
}
if err := manager.RemoveUser(username); err != nil {
return err
}
fmt.Fprintf(c.App.ErrWriter, "user %s removed\n", username)
return nil
}
func execUserChangePass(c *cli.Context) error {
username := c.Args().Get(0)
if username == "" {
return errors.New("username expected, type 'ntfy user change-pass --help' for help")
} else if username == userEveryone {
return errors.New("username not allowed")
}
manager, err := createAuthManager(c)
if err != nil {
return err
}
if _, err := manager.User(username); err == auth.ErrNotFound {
return fmt.Errorf("user %s does not exist", username)
}
password, err := readPasswordAndConfirm(c)
if err != nil {
return err
}
if err := manager.ChangePassword(username, password); err != nil {
return err
}
fmt.Fprintf(c.App.ErrWriter, "changed password for user %s\n", username)
return nil
}
func execUserChangeRole(c *cli.Context) error {
username := c.Args().Get(0)
role := auth.Role(c.Args().Get(1))
if username == "" || !auth.AllowedRole(role) {
return errors.New("username and new role expected, type 'ntfy user change-role --help' for help")
} else if username == userEveryone {
return errors.New("username not allowed")
}
manager, err := createAuthManager(c)
if err != nil {
return err
}
if _, err := manager.User(username); err == auth.ErrNotFound {
return fmt.Errorf("user %s does not exist", username)
}
if err := manager.ChangeRole(username, role); err != nil {
return err
}
fmt.Fprintf(c.App.ErrWriter, "changed role for user %s to %s\n", username, role)
return nil
}
func execUserList(c *cli.Context) error {
manager, err := createAuthManager(c)
if err != nil {
return err
}
users, err := manager.Users()
if err != nil {
return err
}
return showUsers(c, manager, users)
}
func createAuthManager(c *cli.Context) (auth.Manager, error) {
authFile := c.String("auth-file")
authDefaultAccess := c.String("auth-default-access")
if authFile == "" {
return nil, errors.New("option auth-file not set; auth is unconfigured for this server")
} else if !util.FileExists(authFile) {
return nil, errors.New("auth-file does not exist; please start the server at least once to create it")
} else if !util.InStringList([]string{"read-write", "read-only", "write-only", "deny-all"}, authDefaultAccess) {
return nil, errors.New("if set, auth-default-access must start set to 'read-write', 'read-only' or 'deny-all'")
}
authDefaultRead := authDefaultAccess == "read-write" || authDefaultAccess == "read-only"
authDefaultWrite := authDefaultAccess == "read-write" || authDefaultAccess == "write-only"
return auth.NewSQLiteAuth(authFile, authDefaultRead, authDefaultWrite)
}
func readPasswordAndConfirm(c *cli.Context) (string, error) {
fmt.Fprint(c.App.ErrWriter, "password: ")
password, err := util.ReadPassword(c.App.Reader)
if err != nil {
return "", err
}
fmt.Fprintf(c.App.ErrWriter, "\r%s\rconfirm: ", strings.Repeat(" ", 25))
confirm, err := util.ReadPassword(c.App.Reader)
if err != nil {
return "", err
}
fmt.Fprintf(c.App.ErrWriter, "\r%s\r", strings.Repeat(" ", 25))
if subtle.ConstantTimeCompare(confirm, password) != 1 {
return "", errors.New("passwords do not match: try it again, but this time type slooowwwlly")
}
return string(password), nil
}
func userCommandFlags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: "/etc/ntfy/server.yml", DefaultText: "/etc/ntfy/server.yml", Usage: "config file"},
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}),
}
}

145
cmd/user_test.go Normal file
View File

@@ -0,0 +1,145 @@
package cmd
import (
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/server"
"heckel.io/ntfy/test"
"path/filepath"
"testing"
)
func TestCLI_User_Add(t *testing.T) {
s, conf, port := newTestServerWithAuth(t)
defer test.StopServer(t, s, port)
app, stdin, _, stderr := newTestApp()
stdin.WriteString("mypass\nmypass")
require.Nil(t, runUserCommand(app, conf, "add", "phil"))
require.Contains(t, stderr.String(), "user phil added with role user")
}
func TestCLI_User_Add_Exists(t *testing.T) {
s, conf, port := newTestServerWithAuth(t)
defer test.StopServer(t, s, port)
app, stdin, _, stderr := newTestApp()
stdin.WriteString("mypass\nmypass")
require.Nil(t, runUserCommand(app, conf, "add", "phil"))
require.Contains(t, stderr.String(), "user phil added with role user")
app, stdin, _, _ = newTestApp()
stdin.WriteString("mypass\nmypass")
err := runUserCommand(app, conf, "add", "phil")
require.Error(t, err)
require.Contains(t, err.Error(), "user phil already exists")
}
func TestCLI_User_Add_Admin(t *testing.T) {
s, conf, port := newTestServerWithAuth(t)
defer test.StopServer(t, s, port)
app, stdin, _, stderr := newTestApp()
stdin.WriteString("mypass\nmypass")
require.Nil(t, runUserCommand(app, conf, "add", "--role=admin", "phil"))
require.Contains(t, stderr.String(), "user phil added with role admin")
}
func TestCLI_User_Add_Password_Mismatch(t *testing.T) {
s, conf, port := newTestServerWithAuth(t)
defer test.StopServer(t, s, port)
app, stdin, _, _ := newTestApp()
stdin.WriteString("mypass\nNOTMATCH")
err := runUserCommand(app, conf, "add", "phil")
require.Error(t, err)
require.Contains(t, err.Error(), "passwords do not match: try it again, but this time type slooowwwlly")
}
func TestCLI_User_ChangePass(t *testing.T) {
s, conf, port := newTestServerWithAuth(t)
defer test.StopServer(t, s, port)
// Add user
app, stdin, _, stderr := newTestApp()
stdin.WriteString("mypass\nmypass")
require.Nil(t, runUserCommand(app, conf, "add", "phil"))
require.Contains(t, stderr.String(), "user phil added with role user")
// Change pass
app, stdin, _, stderr = newTestApp()
stdin.WriteString("newpass\nnewpass")
require.Nil(t, runUserCommand(app, conf, "change-pass", "phil"))
require.Contains(t, stderr.String(), "changed password for user phil")
}
func TestCLI_User_ChangeRole(t *testing.T) {
s, conf, port := newTestServerWithAuth(t)
defer test.StopServer(t, s, port)
// Add user
app, stdin, _, stderr := newTestApp()
stdin.WriteString("mypass\nmypass")
require.Nil(t, runUserCommand(app, conf, "add", "phil"))
require.Contains(t, stderr.String(), "user phil added with role user")
// Change role
app, _, _, stderr = newTestApp()
require.Nil(t, runUserCommand(app, conf, "change-role", "phil", "admin"))
require.Contains(t, stderr.String(), "changed role for user phil to admin")
}
func TestCLI_User_Delete(t *testing.T) {
s, conf, port := newTestServerWithAuth(t)
defer test.StopServer(t, s, port)
// Add user
app, stdin, _, stderr := newTestApp()
stdin.WriteString("mypass\nmypass")
require.Nil(t, runUserCommand(app, conf, "add", "phil"))
require.Contains(t, stderr.String(), "user phil added with role user")
// Delete user
app, _, _, stderr = newTestApp()
require.Nil(t, runUserCommand(app, conf, "del", "phil"))
require.Contains(t, stderr.String(), "user phil removed")
// Delete user again (does not exist)
app, _, _, _ = newTestApp()
err := runUserCommand(app, conf, "del", "phil")
require.Error(t, err)
require.Contains(t, err.Error(), "user phil does not exist")
}
func newTestServerWithAuth(t *testing.T) (s *server.Server, conf *server.Config, port int) {
conf = server.NewConfig()
conf.AuthFile = filepath.Join(t.TempDir(), "user.db")
conf.AuthDefaultRead = false
conf.AuthDefaultWrite = false
s, port = test.StartServerWithConfig(t, conf)
return
}
func runUserCommand(app *cli.App, conf *server.Config, args ...string) error {
userArgs := []string{
"ntfy",
"user",
"--auth-file=" + conf.AuthFile,
"--auth-default-access=" + confToDefaultAccess(conf),
}
return app.Run(append(userArgs, args...))
}
func confToDefaultAccess(conf *server.Config) string {
var defaultAccess string
if conf.AuthDefaultRead && conf.AuthDefaultWrite {
defaultAccess = "read-write"
} else if conf.AuthDefaultRead && !conf.AuthDefaultWrite {
defaultAccess = "read-only"
} else if !conf.AuthDefaultRead && conf.AuthDefaultWrite {
defaultAccess = "write-only"
} else if !conf.AuthDefaultRead && !conf.AuthDefaultWrite {
defaultAccess = "deny-all"
}
return defaultAccess
}

View File

@@ -1,41 +0,0 @@
// Package config provides the main configuration
package config
import (
"golang.org/x/time/rate"
"time"
)
// Defines default config settings
const (
DefaultListenHTTP = ":80"
DefaultKeepaliveInterval = 30 * time.Second
defaultManagerInterval = time.Minute
)
// Defines the max number of requests, here:
// 50 requests bucket, replenished at a rate of 1 per second
var (
defaultLimit = rate.Every(time.Second)
defaultLimitBurst = 50
)
// Config is the main config struct for the application. Use New to instantiate a default config struct.
type Config struct {
ListenHTTP string
Limit rate.Limit
LimitBurst int
KeepaliveInterval time.Duration
ManagerInterval time.Duration
}
// New instantiates a default new config
func New(listenHTTP string) *Config {
return &Config{
ListenHTTP: listenHTTP,
Limit: defaultLimit,
LimitBurst: defaultLimitBurst,
KeepaliveInterval: DefaultKeepaliveInterval,
ManagerInterval: defaultManagerInterval,
}
}

View File

@@ -1,9 +0,0 @@
# ntfy config file
# Listen address for the HTTP web server
#
# Format: <hostname>:<port>
# Default: :80
# Required: No
#
# listen-http: ":80"

870
docs/config.md Normal file
View File

@@ -0,0 +1,870 @@
# Configuring the ntfy server
The ntfy server can be configured in three ways: using a config file (typically at `/etc/ntfy/server.yml`,
see [server.yml](https://github.com/binwiederhier/ntfy/blob/main/server/server.yml)), via command line arguments
or using environment variables.
## Quick start
By default, simply running `ntfy serve` will start the server at port 80. No configuration needed. Batteries included 😀.
If everything works as it should, you'll see something like this:
```
$ ntfy serve
2021/11/30 19:59:08 Listening on :80
```
You can immediately start [publishing messages](publish.md), or subscribe via the [Android app](subscribe/phone.md),
[the web UI](subscribe/web.md), or simply via [curl or your favorite HTTP client](subscribe/api.md). To configure
the server further, check out the [config options table](#config-options) or simply type `ntfy serve --help` to
get a list of [command line options](#command-line-options).
## Example config
!!! info
Definitely check out the **[server.yml](https://github.com/binwiederhier/ntfy/blob/main/server/server.yml)** file.
It contains examples and detailed descriptions of all the settings.
The most basic settings are `base-url` (the external URL of the ntfy server), the HTTP/HTTPS listen address (`listen-http`
and `listen-https`), and socket path (`listen-unix`). All the other things are additional features.
Here are a few working sample configs:
=== "server.yml (HTTP-only, with cache + attachments)"
``` yaml
base-url: "http://ntfy.example.com"
cache-file: "/var/cache/ntfy/cache.db"
attachment-cache-dir: "/var/cache/ntfy/attachments"
```
=== "server.yml (HTTP+HTTPS, with cache + attachments)"
``` yaml
base-url: "http://ntfy.example.com"
listen-http: ":80"
listen-https: ":443"
key-file: "/etc/letsencrypt/live/ntfy.example.com.key"
cert-file: "/etc/letsencrypt/live/ntfy.example.com.crt"
cache-file: "/var/cache/ntfy/cache.db"
attachment-cache-dir: "/var/cache/ntfy/attachments"
```
=== "server.yml (ntfy.sh config)"
``` yaml
# All the things: Behind a proxy, Firebase, cache, attachments,
# SMTP publishing & receiving
base-url: "https://ntfy.sh"
listen-http: "127.0.0.1:2586"
firebase-key-file: "/etc/ntfy/firebase.json"
cache-file: "/var/cache/ntfy/cache.db"
behind-proxy: true
attachment-cache-dir: "/var/cache/ntfy/attachments"
smtp-sender-addr: "email-smtp.us-east-2.amazonaws.com:587"
smtp-sender-user: "AKIDEADBEEFAFFE12345"
smtp-sender-pass: "Abd13Kf+sfAk2DzifjafldkThisIsNotARealKeyOMG."
smtp-sender-from: "ntfy@ntfy.sh"
smtp-server-listen: ":25"
smtp-server-domain: "ntfy.sh"
smtp-server-addr-prefix: "ntfy-"
keepalive-interval: "45s"
```
## Message cache
If desired, ntfy can temporarily keep notifications in an in-memory or an on-disk cache. Caching messages for a short period
of time is important to allow [phones](subscribe/phone.md) and other devices with brittle Internet connections to be able to retrieve
notifications that they may have missed.
By default, ntfy keeps messages **in-memory for 12 hours**, which means that **cached messages do not survive an application
restart**. You can override this behavior using the following config settings:
* `cache-file`: if set, ntfy will store messages in a SQLite based cache (default is empty, which means in-memory cache).
**This is required if you'd like messages to be retained across restarts**.
* `cache-duration`: defines the duration for which messages are stored in the cache (default is `12h`).
You can also entirely disable the cache by setting `cache-duration` to `0`. When the cache is disabled, messages are only
passed on to the connected subscribers, but never stored on disk or even kept in memory longer than is needed to forward
the message to the subscribers.
Subscribers can retrieve cached messaging using the [`poll=1` parameter](subscribe/api.md#poll-for-messages), as well as the
[`since=` parameter](subscribe/api.md#fetch-cached-messages).
## Attachments
If desired, you may allow users to upload and [attach files to notifications](publish.md#attachments). To enable
this feature, you have to simply configure an attachment cache directory and a base URL (`attachment-cache-dir`, `base-url`).
Once these options are set and the directory is writable by the server user, you can upload attachments via PUT.
By default, attachments are stored in the disk-cache **for only 3 hours**. The main reason for this is to avoid legal issues
and such when hosting user controlled content. Typically, this is more than enough time for the user (or the auto download
feature) to download the file. The following config options are relevant to attachments:
* `base-url` is the root URL for the ntfy server; this is needed for the generated attachment URLs
* `attachment-cache-dir` is the cache directory for attached files
* `attachment-total-size-limit` is the size limit of the on-disk attachment cache (default: 5G)
* `attachment-file-size-limit` is the per-file attachment size limit (e.g. 300k, 2M, 100M, default: 15M)
* `attachment-expiry-duration` is the duration after which uploaded attachments will be deleted (e.g. 3h, 20h, default: 3h)
Here's an example config using mostly the defaults (except for the cache directory, which is empty by default):
=== "/etc/ntfy/server.yml (minimal)"
``` yaml
base-url: "https://ntfy.sh"
attachment-cache-dir: "/var/cache/ntfy/attachments"
```
=== "/etc/ntfy/server.yml (all options)"
``` yaml
base-url: "https://ntfy.sh"
attachment-cache-dir: "/var/cache/ntfy/attachments"
attachment-total-size-limit: "5G"
attachment-file-size-limit: "15M"
attachment-expiry-duration: "3h"
visitor-attachment-total-size-limit: "100M"
visitor-attachment-daily-bandwidth-limit: "500M"
```
Please also refer to the [rate limiting](#rate-limiting) settings below, specifically `visitor-attachment-total-size-limit`
and `visitor-attachment-daily-bandwidth-limit`. Setting these conservatively is necessary to avoid abuse.
## Access control
By default, the ntfy server is open for everyone, meaning **everyone can read and write to any topic** (this is how
ntfy.sh is configured). To restrict access to your own server, you can optionally configure authentication and authorization.
ntfy's auth is implemented with a simple [SQLite](https://www.sqlite.org/)-based backend. It implements two roles
(`user` and `admin`) and per-topic `read` and `write` permissions using an [access control list (ACL)](https://en.wikipedia.org/wiki/Access-control_list).
Access control entries can be applied to users as well as the special everyone user (`*`), which represents anonymous API access.
To set up auth, simply **configure the following two options**:
* `auth-file` is the user/access database; it is created automatically if it doesn't already exist; suggested
location `/var/lib/ntfy/user.db` (easiest if deb/rpm package is used)
* `auth-default-access` defines the default/fallback access if no access control entry is found; it can be
set to `read-write` (default), `read-only`, `write-only` or `deny-all`.
Once configured, you can use the `ntfy user` command to [add or modify users](#users-and-roles), and the `ntfy access` command
lets you [modify the access control list](#access-control-list-acl) for specific users and topic patterns. Both of these
commands **directly edit the auth database** (as defined in `auth-file`), so they only work on the server, and only if the user
accessing them has the right permissions.
### Users and roles
The `ntfy user` command allows you to add/remove/change users in the ntfy user database, as well as change
passwords or roles (`user` or `admin`). In practice, you'll often just create one admin
user with `ntfy user add --role=admin ...` and be done with all this (see [example below](#example-private-instance)).
**Roles:**
* Role `user` (default): Users with this role have no special permissions. Manage access using `ntfy access`
(see [below](#access-control-list-acl)).
* Role `admin`: Users with this role can read/write to all topics. Granular access control is not necessary.
**Example commands** (type `ntfy user --help` or `ntfy user COMMAND --help` for more details):
```
ntfy user list # Shows list of users (alias: 'ntfy access')
ntfy user add phil # Add regular user phil
ntfy user add --role=admin phil # Add admin user phil
ntfy user del phil # Delete user phil
ntfy user change-pass phil # Change password for user phil
ntfy user change-role phil admin # Make user phil an admin
```
### Access control list (ACL)
The access control list (ACL) **manages access to topics for non-admin users, and for anonymous access (`everyone`/`*`)**.
Each entry represents the access permissions for a user to a specific topic or topic pattern.
The ACL can be displayed or modified with the `ntfy access` command:
```
ntfy access # Shows access control list (alias: 'ntfy user list')
ntfy access USERNAME # Shows access control entries for USERNAME
ntfy access USERNAME TOPIC PERMISSION # Allow/deny access for USERNAME to TOPIC
```
A `USERNAME` is an existing user, as created with `ntfy user add` (see [users and roles](#users-and-roles)), or the
anonymous user `everyone` or `*`, which represents clients that access the API without username/password.
A `TOPIC` is either a specific topic name (e.g. `mytopic`, or `phil_alerts`), or a wildcard pattern that matches any
number of topics (e.g. `alerts_*` or `ben-*`). Only the wildcard character `*` is supported. It stands for zero to any
number of characters.
A `PERMISSION` is any of the following supported permissions:
* `read-write` (alias: `rw`): Allows [publishing messages](publish.md) to the given topic, as well as
[subscribing](subscribe/api.md) and reading messages
* `read-only` (aliases: `read`, `ro`): Allows only subscribing and reading messages, but not publishing to the topic
* `write-only` (aliases: `write`, `wo`): Allows only publishing to the topic, but not subscribing to it
* `deny` (alias: `none`): Allows neither publishing nor subscribing to a topic
**Example commands** (type `ntfy access --help` for more details):
```
ntfy access # Shows entire access control list
ntfy access phil # Shows access for user phil
ntfy access phil mytopic rw # Allow read-write access to mytopic for user phil
ntfy access everyone mytopic rw # Allow anonymous read-write access to mytopic
ntfy access everyone "up*" write # Allow anonymous write-only access to topics "up..."
ntfy access --reset # Reset entire access control list
ntfy access --reset phil # Reset all access for user phil
ntfy access --reset phil mytopic # Reset access for user phil and topic mytopic
```
**Example ACL:**
```
$ ntfy access
user phil (admin)
- read-write access to all topics (admin role)
user ben (user)
- read-write access to topic garagedoor
- read-write access to topic alerts*
- read-only access to topic furnace
user * (anonymous)
- read-only access to topic announcements
- read-only access to topic server-stats
- no access to any (other) topics (server config)
```
In this example, `phil` has the role `admin`, so he has read-write access to all topics (no ACL entries are necessary).
User `ben` has three topic-specific entries. He can read, but not write to topic `furnace`, and has read-write access
to topic `garagedoor` and all topics starting with the word `alerts` (wildcards). Clients that are not authenticated
(called `*`/`everyone`) only have read access to the `announcements` and `server-stats` topics.
### Example: Private instance
The easiest way to configure a private instance is to set `auth-default-access` to `deny-all` in the `server.yml`:
=== "/etc/ntfy/server.yml"
``` yaml
auth-file "/var/lib/ntfy/user.db"
auth-default-access: "deny-all"
```
After that, simply create an `admin` user:
```
$ ntfy user add --role=admin phil
password: mypass
confirm: mypass
user phil added with role admin
```
Once you've done that, you can publish and subscribe using [Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication)
with the given username/password. Be sure to use HTTPS to avoid eavesdropping and exposing your password. Here's a simple example:
=== "Command line (curl)"
```
curl \
-u phil:mypass \
-d "Look ma, with auth" \
https://ntfy.example.com/mysecrets
```
=== "ntfy CLI"
```
ntfy publish \
-u phil:mypass \
ntfy.example.com/mysecrets \
"Look ma, with auth"
```
=== "HTTP"
``` http
POST /mysecrets HTTP/1.1
Host: ntfy.example.com
Authorization: Basic cGhpbDpteXBhc3M=
Look ma, with auth
```
=== "JavaScript"
``` javascript
fetch('https://ntfy.example.com/mysecrets', {
method: 'POST', // PUT works too
body: 'Look ma, with auth',
headers: {
'Authorization': 'Basic cGhpbDpteXBhc3M='
}
})
```
=== "Go"
``` go
req, _ := http.NewRequest("POST", "https://ntfy.example.com/mysecrets",
strings.NewReader("Look ma, with auth"))
req.Header.Set("Authorization", "Basic cGhpbDpteXBhc3M=")
http.DefaultClient.Do(req)
```
=== "Python"
``` python
requests.post("https://ntfy.example.com/mysecrets",
data="Look ma, with auth",
headers={
"Authorization": "Basic cGhpbDpteXBhc3M="
})
```
=== "PHP"
``` php-inline
file_get_contents('https://ntfy.example.com/mysecrets', false, stream_context_create([
'http' => [
'method' => 'POST', // PUT also works
'header' =>
'Content-Type: text/plain\r\n' .
'Authorization: Basic cGhpbDpteXBhc3M=',
'content' => 'Look ma, with auth'
]
]));
```
## E-mail notifications
To allow forwarding messages via e-mail, you can configure an **SMTP server for outgoing messages**. Once configured,
you can set the `X-Email` header to [send messages via e-mail](publish.md#e-mail-notifications) (e.g.
`curl -d "hi there" -H "X-Email: phil@example.com" ntfy.sh/mytopic`).
As of today, only SMTP servers with PLAIN auth and STARTLS are supported. To enable e-mail sending, you must set the
following settings:
* `base-url` is the root URL for the ntfy server; this is needed for e-mail footer
* `smtp-sender-addr` is the hostname:port of the SMTP server
* `smtp-sender-user` and `smtp-sender-pass` are the username and password of the SMTP user
* `smtp-sender-from` is the e-mail address of the sender
Here's an example config using [Amazon SES](https://aws.amazon.com/ses/) for outgoing mail (this is how it is
configured for `ntfy.sh`):
=== "/etc/ntfy/server.yml"
``` yaml
base-url: "https://ntfy.sh"
smtp-sender-addr: "email-smtp.us-east-2.amazonaws.com:587"
smtp-sender-user: "AKIDEADBEEFAFFE12345"
smtp-sender-pass: "Abd13Kf+sfAk2DzifjafldkThisIsNotARealKeyOMG."
smtp-sender-from: "ntfy@ntfy.sh"
```
Please also refer to the [rate limiting](#rate-limiting) settings below, specifically `visitor-email-limit-burst`
and `visitor-email-limit-burst`. Setting these conservatively is necessary to avoid abuse.
## E-mail publishing
To allow publishing messages via e-mail, ntfy can run a lightweight **SMTP server for incoming messages**. Once configured,
users can [send emails to a topic e-mail address](publish.md#e-mail-publishing) (e.g. `mytopic@ntfy.sh` or
`myprefix-mytopic@ntfy.sh`) to publish messages to a topic. This is useful for e-mail based integrations such as for
statuspage.io (though these days most services also support webhooks and HTTP calls).
To configure the SMTP server, you must at least set `smtp-server-listen` and `smtp-server-domain`:
* `smtp-server-listen` defines the IP address and port the SMTP server will listen on, e.g. `:25` or `1.2.3.4:25`
* `smtp-server-domain` is the e-mail domain, e.g. `ntfy.sh` (must be identical to MX record, see below)
* `smtp-server-addr-prefix` is an optional prefix for the e-mail addresses to prevent spam. If set to `ntfy-`, for instance,
only e-mails to `ntfy-$topic@ntfy.sh` will be accepted. If this is not set, all emails to `$topic@ntfy.sh` will be
accepted (which may obviously be a spam problem).
Here's an example config (this is how it is configured for `ntfy.sh`):
=== "/etc/ntfy/server.yml"
``` yaml
smtp-server-listen: ":25"
smtp-server-domain: "ntfy.sh"
smtp-server-addr-prefix: "ntfy-"
```
In addition to configuring the ntfy server, you have to create two DNS records (an [MX record](https://en.wikipedia.org/wiki/MX_record)
and a corresponding A record), so incoming mail will find its way to your server. Here's an example of how `ntfy.sh` is
configured (in [Amazon Route 53](https://aws.amazon.com/route53/)):
<figure markdown>
![DNS records for incoming mail](static/img/screenshot-email-publishing-dns.png){ width=600 }
<figcaption>DNS records for incoming mail</figcaption>
</figure>
You can check if everything is working correctly by sending an email as raw SMTP via `nc`. Create a text file, e.g.
`email.txt`
```
EHLO example.com
MAIL FROM: phil@example.com
RCPT TO: ntfy-mytopic@ntfy.sh
DATA
Subject: Email for you
Content-Type: text/plain; charset="UTF-8"
Hello from 🇩🇪
.
```
And then send the mail via `nc` like this. If you see any lines starting with `451`, those are errors from the
ntfy server. Read them carefully.
```
$ cat email.txt | nc -N ntfy.sh 25
220 ntfy.sh ESMTP Service Ready
250-Hello example.com
...
250 2.0.0 Roger, accepting mail from <phil@example.com>
250 2.0.0 I'll make sure <ntfy-mytopic@ntfy.sh> gets this
```
As for the DNS setup, be sure to verify that `dig MX` and `dig A` are returning results similar to this:
```
$ dig MX ntfy.sh +short
10 mx1.ntfy.sh.
$ dig A mx1.ntfy.sh +short
3.139.215.220
```
## Behind a proxy (TLS, etc.)
!!! warning
If you are running ntfy behind a proxy, you must set the `behind-proxy` flag. Otherwise, all visitors are
[rate limited](#rate-limiting) as if they are one.
It may be desirable to run ntfy behind a proxy (e.g. nginx, HAproxy or Apache), so you can provide TLS certificates
using Let's Encrypt using certbot, or simply because you'd like to share the ports (80/443) with other services.
Whatever your reasons may be, there are a few things to consider.
If you are running ntfy behind a proxy, you should set the `behind-proxy` flag. This will instruct the
[rate limiting](#rate-limiting) logic to use the `X-Forwarded-For` header as the primary identifier for a visitor,
as opposed to the remote IP address. If the `behind-proxy` flag is not set, all visitors will
be counted as one, because from the perspective of the ntfy server, they all share the proxy's IP address.
=== "/etc/ntfy/server.yml"
``` yaml
# Tell ntfy to use "X-Forwarded-For" to identify visitors
behind-proxy: true
```
### TLS/SSL
ntfy supports HTTPS/TLS by setting the `listen-https` [config option](#config-options). However, if you
are behind a proxy, it is recommended that TLS/SSL termination is done by the proxy itself (see below).
I highly recommend using [certbot](https://certbot.eff.org/). I use it with the [dns-route53 plugin](https://certbot-dns-route53.readthedocs.io/en/stable/),
which lets you use [AWS Route 53](https://aws.amazon.com/route53/) as the challenge. That's much easier than using the
HTTP challenge. I've found [this guide](https://nandovieira.com/using-lets-encrypt-in-development-with-nginx-and-aws-route53) to
be incredibly helpful.
### nginx/Apache2/caddy
For your convenience, here's a working config that'll help configure things behind a proxy. Be sure to **enable WebSockets**
by forwarding the `Connection` and `Upgrade` headers accordingly.
In this example, ntfy runs on `:2586` and we proxy traffic to it. We also redirect HTTP to HTTPS for GET requests against a topic
or the root domain:
=== "nginx (/etc/nginx/sites-*/ntfy)"
```
server {
listen 80;
server_name ntfy.sh;
location / {
# Redirect HTTP to HTTPS, but only for GET topic addresses, since we want
# it to work with curl without the annoying https:// prefix
set $redirect_https "";
if ($request_method = GET) {
set $redirect_https "yes";
}
if ($request_uri ~* "^/([-_a-z0-9]{0,64}$|docs/|static/)") {
set $redirect_https "${redirect_https}yes";
}
if ($redirect_https = "yesyes") {
return 302 https://$http_host$request_uri$is_args$query_string;
}
proxy_pass http://127.0.0.1:2586;
proxy_http_version 1.1;
proxy_buffering off;
proxy_request_buffering off;
proxy_redirect off;
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_connect_timeout 3m;
proxy_send_timeout 3m;
proxy_read_timeout 3m;
client_max_body_size 20m; # Must be >= attachment-file-size-limit in /etc/ntfy/server.yml
}
}
server {
listen 443 ssl;
server_name ntfy.sh;
ssl_session_cache builtin:1000 shared:SSL:10m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4;
ssl_prefer_server_ciphers on;
ssl_certificate /etc/letsencrypt/live/ntfy.sh/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ntfy.sh/privkey.pem;
location / {
proxy_pass http://127.0.0.1:2586;
proxy_http_version 1.1;
proxy_buffering off;
proxy_request_buffering off;
proxy_redirect off;
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_connect_timeout 3m;
proxy_send_timeout 3m;
proxy_read_timeout 3m;
client_max_body_size 20m; # Must be >= attachment-file-size-limit in /etc/ntfy/server.yml
}
}
```
=== "Apache2 (/etc/apache2/sites-*/ntfy.conf)"
```
<VirtualHost *:80>
ServerName ntfy.sh
SetEnv proxy-nokeepalive 1
SetEnv proxy-sendchunked 1
ProxyPass / http://127.0.0.1:2586/
ProxyPassReverse / http://127.0.0.1:2586/
# Higher than the max message size of 4096 bytes
LimitRequestBody 102400
# WebSockets support
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/?(.*) "ws://127.0.0.1:2586/$1" [P,L]
# Redirect HTTP to HTTPS, but only for GET topic addresses, since we want
# it to work with curl without the annoying https:// prefix
RewriteEngine on
RewriteCond %{REQUEST_METHOD} GET
RewriteRule ^/([-_A-Za-z0-9]{0,64})$ https://%{SERVER_NAME}/$1 [R,L]
</VirtualHost>
<VirtualHost *:443>
ServerName ntfy.sh
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/ntfy.sh/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/ntfy.sh/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
SetEnv proxy-nokeepalive 1
SetEnv proxy-sendchunked 1
ProxyPass / http://127.0.0.1:2586/
ProxyPassReverse / http://127.0.0.1:2586/
# Higher than the max message size of 4096 bytes
LimitRequestBody 102400
# WebSockets support
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/?(.*) "ws://127.0.0.1:2586/$1" [P,L]
# Redirect HTTP to HTTPS, but only for GET topic addresses, since we want
# it to work with curl without the annoying https:// prefix
RewriteEngine on
RewriteCond %{REQUEST_METHOD} GET
RewriteRule ^/([-_A-Za-z0-9]{0,64})$ https://%{SERVER_NAME}/$1 [R,L]
</VirtualHost>
```
=== "caddy"
```
# Note that this config is most certainly incomplete. Please help out and let me know what's missing
# via Discord/Matrix or in a GitHub issue.
ntfy.sh, http://nfty.sh {
reverse_proxy 127.0.0.1:2586
}
```
## Firebase (FCM)
!!! info
Using Firebase is **optional** and only works if you modify and [build your own Android .apk](develop.md#android-app).
For a self-hosted instance, it's easier to just not bother with FCM.
[Firebase Cloud Messaging (FCM)](https://firebase.google.com/docs/cloud-messaging) is the Google approved way to send
push messages to Android devices. FCM is the only method that an Android app can receive messages without having to run a
[foreground service](https://developer.android.com/guide/components/foreground-services).
For the main host [ntfy.sh](https://ntfy.sh), the [ntfy Android app](subscribe/phone.md) uses Firebase to send messages
to the device. For other hosts, instant delivery is used and FCM is not involved.
To configure FCM for your self-hosted instance of the ntfy server, follow these steps:
1. Sign up for a [Firebase account](https://console.firebase.google.com/)
2. Create a Firebase app and download the key file (e.g. `myapp-firebase-adminsdk-...json`)
3. Place the key file in `/etc/ntfy`, set the `firebase-key-file` in `server.yml` accordingly and restart the ntfy server
4. Build your own Android .apk following [these instructions](develop.md#android-app)
Example:
```
# If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app.
# This is optional and only required to support Android apps (which don't allow background services anymore).
#
firebase-key-file: "/etc/ntfy/ntfy-sh-firebase-adminsdk-ahnce-9f4d6f14b5.json"
```
## Rate limiting
!!! info
Be aware that if you are running ntfy behind a proxy, you must set the `behind-proxy` flag.
Otherwise, all visitors are rate limited as if they are one.
By default, ntfy runs without authentication, so it is vitally important that we protect the server from abuse or overload.
There are various limits and rate limits in place that you can use to configure the server:
* **Global limit**: A global limit applies across all visitors (IPs, clients, users)
* **Visitor limit**: A visitor limit only applies to a certain visitor. A **visitor** is identified by its IP address
(or the `X-Forwarded-For` header if `behind-proxy` is set). All config options that start with the word `visitor` apply
only on a per-visitor basis.
During normal usage, you shouldn't encounter these limits at all, and even if you burst a few requests or emails
(e.g. when you reconnect after a connection drop), it shouldn't have any effect.
### General limits
Let's do the easy limits first:
* `global-topic-limit` defines the total number of topics before the server rejects new topics. It defaults to 15,000.
* `visitor-subscription-limit` is the number of subscriptions (open connections) per visitor. This value defaults to 30.
### Request limits
In addition to the limits above, there is a requests/second limit per visitor for all sensitive GET/PUT/POST requests.
This limit uses a [token bucket](https://en.wikipedia.org/wiki/Token_bucket) (using Go's [rate package](https://pkg.go.dev/golang.org/x/time/rate)):
Each visitor has a bucket of 60 requests they can fire against the server (defined by `visitor-request-limit-burst`).
After the 60, new requests will encounter a `429 Too Many Requests` response. The visitor request bucket is refilled at a rate of one
request every 5s (defined by `visitor-request-limit-replenish`)
* `visitor-request-limit-burst` is the initial bucket of requests each visitor has. This defaults to 60.
* `visitor-request-limit-replenish` is the rate at which the bucket is refilled (one request per x). Defaults to 5s.
* `visitor-request-limit-exempt-hosts` is a comma-separated list of hostnames and IPs to be exempt from request rate
limiting; hostnames are resolved at the time the server is started. Defaults to an empty list.
### Attachment limits
Aside from the global file size and total attachment cache limits (see [above](#attachments)), there are two relevant
per-visitor limits:
* `visitor-attachment-total-size-limit` is the total storage limit used for attachments per visitor. It defaults to 100M.
The per-visitor storage is automatically decreased as attachments expire. External attachments (attached via `X-Attach`,
see [publishing docs](publish.md#attachments)) do not count here.
* `visitor-attachment-daily-bandwidth-limit` is the total daily attachment download/upload bandwidth limit per visitor,
including PUT and GET requests. This is to protect your precious bandwidth from abuse, since egress costs money in
most cloud providers. This defaults to 500M.
### E-mail limits
Similarly to the request limit, there is also an e-mail limit (only relevant if [e-mail notifications](#e-mail-notifications)
are enabled):
* `visitor-email-limit-burst` is the initial bucket of emails each visitor has. This defaults to 16.
* `visitor-email-limit-replenish` is the rate at which the bucket is refilled (one email per x). Defaults to 1h.
## Tuning for scale
If you're running ntfy for your home server, you probably don't need to worry about scale at all. In its default config,
if it's not behind a proxy, the ntfy server can keep about **as many connections as the open file limit allows**.
This limit is typically called `nofile`. Other than that, RAM and CPU are obviously relevant. You may also want to check
out [this discussion on Reddit](https://www.reddit.com/r/golang/comments/r9u4ee/how_many_actively_connected_http_clients_can_a_go/).
Depending on *how you run it*, here are a few limits that are relevant:
### For systemd services
If you're running ntfy in a systemd service (e.g. for .deb/.rpm packages), the main limiting factor is the
`LimitNOFILE` setting in the systemd unit. The default open files limit for `ntfy.service` is 10,000. You can override it
by creating a `/etc/systemd/system/ntfy.service.d/override.conf` file. As far as I can tell, `/etc/security/limits.conf`
is not relevant.
=== "/etc/systemd/system/ntfy.service.d/override.conf"
```
# Allow 20,000 ntfy connections (and give room for other file handles)
[Service]
LimitNOFILE=20500
```
### Outside of systemd
If you're running outside systemd, you may want to adjust your `/etc/security/limits.conf` file to
increase the `nofile` setting. Here's an example that increases the limit to 5,000. You can find out the current setting
by running `ulimit -n`, or manually override it temporarily by running `ulimit -n 50000`.
=== "/etc/security/limits.conf"
```
# Increase open files limit globally
* hard nofile 20500
```
### Proxy limits (nginx, Apache2)
If you are running [behind a proxy](#behind-a-proxy-tls-etc) (e.g. nginx, Apache), the open files limit of the proxy is also
relevant. So if your proxy runs inside of systemd, increase the limits in systemd for the proxy. Typically, the proxy
open files limit has to be **double the number of how many connections you'd like to support**, because the proxy has
to maintain the client connection and the connection to ntfy.
=== "/etc/nginx/nginx.conf"
```
events {
# Allow 40,000 proxy connections (2x of the desired ntfy connection count;
# and give room for other file handles)
worker_connections 40500;
}
```
=== "/etc/systemd/system/nginx.service.d/override.conf"
```
# Allow 40,000 proxy connections (2x of the desired ntfy connection count;
# and give room for other file handles)
[Service]
LimitNOFILE=40500
```
### Banning bad actors (fail2ban)
If you put stuff on the Internet, bad actors will try to break them or break in. [fail2ban](https://www.fail2ban.org/)
and nginx's [ngx_http_limit_req_module module](http://nginx.org/en/docs/http/ngx_http_limit_req_module.html) can be used
to ban client IPs if they misbehave. This is on top of the [rate limiting](#rate-limiting) inside the ntfy server.
Here's an example for how ntfy.sh is configured, following the instructions from two tutorials ([here](https://easyengine.io/tutorials/nginx/fail2ban/)
and [here](https://easyengine.io/tutorials/nginx/block-wp-login-php-bruteforce-attack/)):
=== "/etc/nginx/nginx.conf"
```
http {
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
}
```
=== "/etc/nginx/sites-enabled/ntfy.sh"
```
# For each server/location block
server {
location / {
limit_req zone=one burst=1000 nodelay;
}
}
```
=== "/etc/fail2ban/filter.d/nginx-req-limit.conf"
```
[Definition]
failregex = limiting requests, excess:.* by zone.*client: <HOST>
ignoreregex =
```
=== "/etc/fail2ban/jail.local"
```
[nginx-req-limit]
enabled = true
filter = nginx-req-limit
action = iptables-multiport[name=ReqLimit, port="http,https", protocol=tcp]
logpath = /var/log/nginx/error.log
findtime = 600
bantime = 7200
maxretry = 10
```
## Config options
Each config option can be set in the config file `/etc/ntfy/server.yml` (e.g. `listen-http: :80`) or as a
CLI option (e.g. `--listen-http :80`. Here's a list of all available options. Alternatively, you can set an environment
variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
| Config option | Env variable | Format | Default | Description |
|--------------------------------------------|-------------------------------------------------|-----------------------------------------------------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `base-url` | `NTFY_BASE_URL` | *URL* | - | Public facing base URL of the service (e.g. `https://ntfy.sh`) |
| `listen-http` | `NTFY_LISTEN_HTTP` | `[host]:port` | `:80` | Listen address for the HTTP web server |
| `listen-https` | `NTFY_LISTEN_HTTPS` | `[host]:port` | - | Listen address for the HTTPS web server. If set, you also need to set `key-file` and `cert-file`. |
| `listen-unix` | `NTFY_LISTEN_UNIX` | *filename* | - | Path to a Unix socket to listen on |
| `key-file` | `NTFY_KEY_FILE` | *filename* | - | HTTPS/TLS private key file, only used if `listen-https` is set. |
| `cert-file` | `NTFY_CERT_FILE` | *filename* | - | HTTPS/TLS certificate file, only used if `listen-https` is set. |
| `firebase-key-file` | `NTFY_FIREBASE_KEY_FILE` | *filename* | - | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. See [Firebase (FCM](#firebase-fcm). |
| `cache-file` | `NTFY_CACHE_FILE` | *filename* | - | If set, messages are cached in a local SQLite database instead of only in-memory. This allows for service restarts without losing messages in support of the since= parameter. See [message cache](#message-cache). |
| `cache-duration` | `NTFY_CACHE_DURATION` | *duration* | 12h | Duration for which messages will be buffered before they are deleted. This is required to support the `since=...` and `poll=1` parameter. Set this to `0` to disable the cache entirely. |
| `auth-file` | `NTFY_AUTH_FILE` | *filename* | - | Auth database file used for access control. If set, enables authentication and access control. See [access control](#access-control). |
| `auth-default-access` | `NTFY_AUTH_DEFAULT_ACCESS` | `read-write`, `read-only`, `write-only`, `deny-all` | `read-write` | Default permissions if no matching entries in the auth database are found. Default is `read-write`. |
| `behind-proxy` | `NTFY_BEHIND_PROXY` | *bool* | false | If set, the X-Forwarded-For header is used to determine the visitor IP address instead of the remote address of the connection. |
| `attachment-cache-dir` | `NTFY_ATTACHMENT_CACHE_DIR` | *directory* | - | Cache directory for attached files. To enable attachments, this has to be set. |
| `attachment-total-size-limit` | `NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT` | *size* | 5G | Limit of the on-disk attachment cache directory. If the limits is exceeded, new attachments will be rejected. |
| `attachment-file-size-limit` | `NTFY_ATTACHMENT_FILE_SIZE_LIMIT` | *size* | 15M | Per-file attachment size limit (e.g. 300k, 2M, 100M). Larger attachment will be rejected. |
| `attachment-expiry-duration` | `NTFY_ATTACHMENT_EXPIRY_DURATION` | *duration* | 3h | Duration after which uploaded attachments will be deleted (e.g. 3h, 20h). Strongly affects `visitor-attachment-total-size-limit`. |
| `smtp-sender-addr` | `NTFY_SMTP_SENDER_ADDR` | `host:port` | - | SMTP server address to allow email sending |
| `smtp-sender-user` | `NTFY_SMTP_SENDER_USER` | *string* | - | SMTP user; only used if e-mail sending is enabled |
| `smtp-sender-pass` | `NTFY_SMTP_SENDER_PASS` | *string* | - | SMTP password; only used if e-mail sending is enabled |
| `smtp-sender-from` | `NTFY_SMTP_SENDER_FROM` | *e-mail address* | - | SMTP sender e-mail address; only used if e-mail sending is enabled |
| `smtp-server-listen` | `NTFY_SMTP_SERVER_LISTEN` | `[ip]:port` | - | Defines the IP address and port the SMTP server will listen on, e.g. `:25` or `1.2.3.4:25` |
| `smtp-server-domain` | `NTFY_SMTP_SERVER_DOMAIN` | *domain name* | - | SMTP server e-mail domain, e.g. `ntfy.sh` |
| `smtp-server-addr-prefix` | `NTFY_SMTP_SERVER_ADDR_PREFIX` | `[ip]:port` | - | Optional prefix for the e-mail addresses to prevent spam, e.g. `ntfy-` |
| `keepalive-interval` | `NTFY_KEEPALIVE_INTERVAL` | *duration* | 45s | Interval in which keepalive messages are sent to the client. This is to prevent intermediaries closing the connection for inactivity. Note that the Android app has a hardcoded timeout at 77s, so it should be less than that. |
| `manager-interval` | `$NTFY_MANAGER_INTERVAL` | *duration* | 1m | Interval in which the manager prunes old messages, deletes topics and prints the stats. |
| `web-root` | `NTFY_WEB_ROOT` | `app` or `home` | `app` | Sets web root to landing page (home) or web app (app) |
| `global-topic-limit` | `NTFY_GLOBAL_TOPIC_LIMIT` | *number* | 15,000 | Rate limiting: Total number of topics before the server rejects new topics. |
| `visitor-subscription-limit` | `NTFY_VISITOR_SUBSCRIPTION_LIMIT` | *number* | 30 | Rate limiting: Number of subscriptions per visitor (IP address) |
| `visitor-attachment-total-size-limit` | `NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT` | *size* | 100M | Rate limiting: Total storage limit used for attachments per visitor, for all attachments combined. Storage is freed after attachments expire. See `attachment-expiry-duration`. |
| `visitor-attachment-daily-bandwidth-limit` | `NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT` | *size* | 500M | Rate limiting: Total daily attachment download/upload traffic limit per visitor. This is to protect your bandwidth costs from exploding. |
| `visitor-request-limit-burst` | `NTFY_VISITOR_REQUEST_LIMIT_BURST` | *number* | 60 | Rate limiting: Allowed GET/PUT/POST requests per second, per visitor. This setting is the initial bucket of requests each visitor has |
| `visitor-request-limit-replenish` | `NTFY_VISITOR_REQUEST_LIMIT_REPLENISH` | *duration* | 5s | Rate limiting: Strongly related to `visitor-request-limit-burst`: The rate at which the bucket is refilled |
| `visitor-request-limit-exempt-hosts` | `NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS` | *comma-separated host/IP list* | - | Rate limiting: List of hostnames and IPs to be exempt from request rate limiting |
| `visitor-email-limit-burst` | `NTFY_VISITOR_EMAIL_LIMIT_BURST` | *number* | 16 | Rate limiting:Initial limit of e-mails per visitor |
| `visitor-email-limit-replenish` | `NTFY_VISITOR_EMAIL_LIMIT_REPLENISH` | *duration* | 1h | Rate limiting: Strongly related to `visitor-email-limit-burst`: The rate at which the bucket is refilled |
The format for a *duration* is: `<number>(smh)`, e.g. 30s, 20m or 1h.
The format for a *size* is: `<number>(GMK)`, e.g. 1G, 200M or 4000k.
## Command line options
```
$ ntfy serve --help
NAME:
ntfy serve - Run the ntfy server
USAGE:
ntfy serve [OPTIONS..]
CATEGORY:
Server commands
DESCRIPTION:
Run the ntfy server and listen for incoming requests
The command will load the configuration from /etc/ntfy/server.yml. Config options can
be overridden using the command line options.
Examples:
ntfy serve # Starts server in the foreground (on port 80)
ntfy serve --listen-http :8080 # Starts server with alternate port
OPTIONS:
--config value, -c value config file (default: /etc/ntfy/server.yml) [$NTFY_CONFIG_FILE]
--base-url value, -B value externally visible base URL for this host (e.g. https://ntfy.sh) [$NTFY_BASE_URL]
--listen-http value, -l value ip:port used to as HTTP listen address (default: ":80") [$NTFY_LISTEN_HTTP]
--listen-https value, -L value ip:port used to as HTTPS listen address [$NTFY_LISTEN_HTTPS]
--listen-unix value, -U value listen on unix socket path [$NTFY_LISTEN_UNIX]
--key-file value, -K value private key file, if listen-https is set [$NTFY_KEY_FILE]
--cert-file value, -E value certificate file, if listen-https is set [$NTFY_CERT_FILE]
--firebase-key-file value, -F value Firebase credentials file; if set additionally publish to FCM topic [$NTFY_FIREBASE_KEY_FILE]
--cache-file value, -C value cache file used for message caching [$NTFY_CACHE_FILE]
--cache-duration since, -b since buffer messages for this time to allow since requests (default: 12h0m0s) [$NTFY_CACHE_DURATION]
--auth-file value, -H value auth database file used for access control [$NTFY_AUTH_FILE]
--auth-default-access value, -p value default permissions if no matching entries in the auth database are found (default: "read-write") [$NTFY_AUTH_DEFAULT_ACCESS]
--attachment-cache-dir value cache directory for attached files [$NTFY_ATTACHMENT_CACHE_DIR]
--attachment-total-size-limit value, -A value limit of the on-disk attachment cache (default: 5G) [$NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT]
--attachment-file-size-limit value, -Y value per-file attachment size limit (e.g. 300k, 2M, 100M) (default: 15M) [$NTFY_ATTACHMENT_FILE_SIZE_LIMIT]
--attachment-expiry-duration value, -X value duration after which uploaded attachments will be deleted (e.g. 3h, 20h) (default: 3h) [$NTFY_ATTACHMENT_EXPIRY_DURATION]
--keepalive-interval value, -k value interval of keepalive messages (default: 45s) [$NTFY_KEEPALIVE_INTERVAL]
--manager-interval value, -m value interval of for message pruning and stats printing (default: 1m0s) [$NTFY_MANAGER_INTERVAL]
--web-root value sets web root to landing page (home) or web app (app) (default: "app") [$NTFY_WEB_ROOT]
--smtp-sender-addr value SMTP server address (host:port) for outgoing emails [$NTFY_SMTP_SENDER_ADDR]
--smtp-sender-user value SMTP user (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_USER]
--smtp-sender-pass value SMTP password (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_PASS]
--smtp-sender-from value SMTP sender address (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_FROM]
--smtp-server-listen value SMTP server address (ip:port) for incoming emails, e.g. :25 [$NTFY_SMTP_SERVER_LISTEN]
--smtp-server-domain value SMTP domain for incoming e-mail, e.g. ntfy.sh [$NTFY_SMTP_SERVER_DOMAIN]
--smtp-server-addr-prefix value SMTP email address prefix for topics to prevent spam (e.g. 'ntfy-') [$NTFY_SMTP_SERVER_ADDR_PREFIX]
--global-topic-limit value, -T value total number of topics allowed (default: 15000) [$NTFY_GLOBAL_TOPIC_LIMIT]
--visitor-subscription-limit value number of subscriptions per visitor (default: 30) [$NTFY_VISITOR_SUBSCRIPTION_LIMIT]
--visitor-attachment-total-size-limit value total storage limit used for attachments per visitor (default: "100M") [$NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT]
--visitor-attachment-daily-bandwidth-limit value total daily attachment download/upload bandwidth limit per visitor (default: "500M") [$NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT]
--visitor-request-limit-burst value initial limit of requests per visitor (default: 60) [$NTFY_VISITOR_REQUEST_LIMIT_BURST]
--visitor-request-limit-replenish value interval at which burst limit is replenished (one per x) (default: 5s) [$NTFY_VISITOR_REQUEST_LIMIT_REPLENISH]
--visitor-request-limit-exempt-hosts value hostnames and/or IP addresses of hosts that will be exempt from the visitor request limit [$NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS]
--visitor-email-limit-burst value initial limit of e-mails per visitor (default: 16) [$NTFY_VISITOR_EMAIL_LIMIT_BURST]
--visitor-email-limit-replenish value interval at which burst limit is replenished (one per x) (default: 1h0m0s) [$NTFY_VISITOR_EMAIL_LIMIT_REPLENISH]
--behind-proxy, -P if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting) (default: false) [$NTFY_BEHIND_PROXY]
--help, -h show help (default: false)
```

45
docs/deprecations.md Normal file
View File

@@ -0,0 +1,45 @@
# Deprecation notices
This page is used to list deprecation notices for ntfy. Deprecated commands and options will be
**removed after ~3 months** from the time they were deprecated.
## Active deprecations
### Android app: WebSockets will become the default connection protocol
> Active since 2022-03-13, behavior will change in **June 2022**
In future versions of the Android app, instant delivery connections and connections to self-hosted servers will
be using the WebSockets protocol. This potentially requires [configuration changes in your proxy](https://ntfy.sh/docs/config/#nginxapache2caddy).
Due to [reports of varying battery consumption](https://github.com/binwiederhier/ntfy/issues/190) (which entirely
seems to depend on the phone), JSON HTTP stream support will not be removed. Instead, I'll just flip the default to
WebSocket in June.
### Android app: Using `since=<timestamp>` instead of `since=<id>`
> Active since 2022-02-27, behavior will change in **May 2022**
In about 3 months, the Android app will start using `since=<id>` instead of `since=<timestamp>`, which means that it will
not work with servers older than v1.16.0 anymore. This is to simplify handling of deduplication in the Android app.
The `since=<timestamp>` endpoint will continue to work. This is merely a notice that the Android app behavior will change.
## Previous deprecations
### Running server via `ntfy` (instead of `ntfy serve`)
> Deprecated 2021-12-17, behavior changed with v1.10.0
As more commands are added to the `ntfy` CLI tool, using just `ntfy` to run the server is not practical
anymore. Please use `ntfy serve` instead. This also applies to Docker images, as they can also execute more than
just the server.
=== "Before"
```
$ ntfy
2021/12/17 08:16:01 Listening on :80/http
```
=== "After"
```
$ ntfy serve
2021/12/17 08:16:01 Listening on :80/http
```

312
docs/develop.md Normal file
View File

@@ -0,0 +1,312 @@
# Development
Hurray 🥳 🎉, you are interested in writing code for ntfy! **That's awesome.** 😎
I tried my very best to write up detailed instructions, but if at any point in time you run into issues, don't
hesitate to **contact me on [Discord](https://discord.gg/cT7ECsZj9w) or [Matrix](https://matrix.to/#/#ntfy:matrix.org)**.
## ntfy server
The ntfy server source code is available [on GitHub](https://github.com/binwiederhier/ntfy). The codebase for the
server consists of three components:
* **The main server/client** is written in [Go](https://go.dev/) (so you'll need Go). Its main entrypoint is at
[main.go](https://github.com/binwiederhier/ntfy/blob/main/main.go), and the meat you're likely interested in is
in [server.go](https://github.com/binwiederhier/ntfy/blob/main/server/server.go). Notably, the server uses a
[SQLite](https://sqlite.org) library called [go-sqlite3](https://github.com/mattn/go-sqlite3), which requires
[Cgo](https://go.dev/blog/cgo) and `CGO_ENABLED=1` to be set. Otherwise things will not work (see below).
* **The documentation** is generated by [MkDocs](https://www.mkdocs.org/) and [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/),
which is written in [Python](https://www.python.org/). You'll need Python and MkDocs (via `pip`) only if you want to
build the docs.
* **The web app** is written in [React](https://reactjs.org/), using [MUI](https://mui.com/). It uses [Create React App](https://create-react-app.dev/)
to build the production build. If you want to modify the web app, you need [nodejs](https://nodejs.org/en/) (for `npm`)
and install all the 100,000 dependencies (*sigh*).
All of these components are built and then **baked into one binary**.
### Navigating the code
Code:
* [main.go](https://github.com/binwiederhier/ntfy/blob/main/main.go) - Main entrypoint into the CLI, for both server and client
* [cmd/](https://github.com/binwiederhier/ntfy/tree/main/cmd) - CLI commands, such as `serve` or `publish`
* [server/](https://github.com/binwiederhier/ntfy/tree/main/server) - The meat of the server logic
* [docs/](https://github.com/binwiederhier/ntfy/tree/main/docs) - The [MkDocs](https://www.mkdocs.org/) documentation, also see `mkdocs.yml`
* [web/](https://github.com/binwiederhier/ntfy/tree/main/web) - The [React](https://reactjs.org/) application, also see `web/package.json`
Build related:
* [Makefile](https://github.com/binwiederhier/ntfy/blob/main/Makefile) - Main entrypoint for all things related to building
* [.goreleaser.yml](https://github.com/binwiederhier/ntfy/blob/main/.goreleaser.yml) - Describes all build outputs (for [GoReleaser](https://goreleaser.com/))
* [go.mod](https://github.com/binwiederhier/ntfy/blob/main/go.mod) - Go modules dependency file
* [mkdocs.yml](https://github.com/binwiederhier/ntfy/blob/main/mkdocs.yml) - Config file for the docs (for [MkDocs](https://www.mkdocs.org/))
* [web/package.json](https://github.com/binwiederhier/ntfy/blob/main/web/package.json) - Build and dependency file for web app (for npm)
The `web/` and `docs/` folder are the sources for web app and documentation. During the build process,
the generated output is copied to `server/site` (web app and landing page) and `server/docs` (documentation).
### Build requirements
* [Go](https://go.dev/) (required for main server)
* [gcc](https://gcc.gnu.org/) (required main server, for SQLite cgo-based bindings)
* [Make](https://www.gnu.org/software/make/) (required for convenience)
* [libsqlite3/libsqlite3-dev](https://www.sqlite.org/) (required for main server, for SQLite cgo-based bindings)
* [GoReleaser](https://goreleaser.com/) (required for a proper main server build)
* [Python](https://www.python.org/) (for `pip`, only to build the docs)
* [nodejs](https://nodejs.org/en/) (for `npm`, only to build the web app)
### Install dependencies
These steps **assume Ubuntu**. Steps may vary on different Linux distributions.
First, install [Go](https://go.dev/) (see [official instructions](https://go.dev/doc/install)):
``` shell
wget https://go.dev/dl/go1.18.linux-amd64.tar.gz
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.18.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin
go version # verifies that it worked
```
Install [GoReleaser](https://goreleaser.com/) (see [official instructions](https://goreleaser.com/install/)):
``` shell
go install github.com/goreleaser/goreleaser@latest
goreleaser -v # verifies that it worked
```
Install [nodejs](https://nodejs.org/en/) (see [official instructions](https://nodejs.org/en/download/package-manager/)):
``` shell
curl -fsSL https://deb.nodesource.com/setup_17.x | sudo -E bash -
sudo apt-get install -y nodejs
npm -v # verifies that it worked
```
Then install a few other things required:
``` shell
sudo apt install \
build-essential \
libsqlite3-dev \
gcc-arm-linux-gnueabi \
gcc-aarch64-linux-gnu \
python3-pip \
upx \
git
```
### Check out code
Now check out via git from the [GitHub repository](https://github.com/binwiederhier/ntfy):
=== "via HTTPS"
``` shell
git clone https://github.com/binwiederhier/ntfy.git
cd ntfy
```
=== "via SSH"
``` shell
git clone git@github.com:binwiederhier/ntfy.git
cd ntfy
```
### Build all the things
Now you can finally build everything. There are tons of `make` targets, so maybe just review what's there first
by typing `make`:
``` shell
$ make
Typical commands (more see below):
make build - Build web app, documentation and server/client (sloowwww)
make server-amd64 - Build server/client binary (amd64, no web app or docs)
make install-amd64 - Install ntfy binary to /usr/bin/ntfy (amd64)
make web - Build the web app
make docs - Build the documentation
make check - Run all tests, vetting/formatting checks and linters
...
```
If you want to build the **ntfy binary including web app and docs for all supported architectures** (amd64, armv7, and amd64),
you can simply run `make build`:
``` shell
$ make build
...
# This builds web app, docs, and the ntfy binary (for amd64, armv7 and arm64).
# This will be SLOW (5+ minutes on my laptop on the first run). Maybe look at the other make targets?
```
You'll see all the outputs in the `dist/` folder afterwards:
``` bash
$ find dist
dist
dist/metadata.json
dist/ntfy_arm64_linux_arm64
dist/ntfy_arm64_linux_arm64/ntfy
dist/ntfy_armv7_linux_arm_7
dist/ntfy_armv7_linux_arm_7/ntfy
dist/ntfy_amd64_linux_amd64
dist/ntfy_amd64_linux_amd64/ntfy
dist/config.yaml
dist/artifacts.json
```
If you also want to build the **Debian/RPM packages and the Docker images for all supported architectures**, you can
use the `make release-snapshot` target:
``` shell
$ make release-snapshot
...
# This will be REALLY SLOW (sometimes 5+ minutes on my laptop)
```
During development, you may want to be more picky and build only certain things. Here are a few examples.
### Build the ntfy binary
To build only the `ntfy` binary **without the web app or documentation**, use the `make server-...` targets:
``` shell
$ make
Build server & client (not release version):
make server - Build server & client (all architectures)
make server-amd64 - Build server & client (amd64 only)
make server-armv7 - Build server & client (armv7 only)
make server-arm64 - Build server & client (arm64 only)
```
So if you're on an amd64/x86_64-based machine, you may just want to run `make server-amd64` during testing. On a modern
system, this shouldn't take longer than 5-10 seconds. I often combine it with `install-amd64` so I can run the binary
right away:
``` shell
$ make server-amd64 install-amd64
$ ntfy serve
```
**During development of the main app, you can also just use `go run main.go`**, as long as you run
`make server-deps-static-sites`at least once and `CGO_ENABLED=1`:
``` shell
$ export CGO_ENABLED=1
$ make server-deps-static-sites
$ go run main.go serve
2022/03/18 08:43:55 Listening on :2586[http]
...
```
If you don't run `server-deps-static-sites`, you may see an error *`pattern ...: no matching files found`*:
```
$ go run main.go serve
server/server.go:85:13: pattern docs: no matching files found
```
This is because we use `go:embed` to embed the documentation and web app, so the Go code expects files to be
present at `server/docs` and `server/site`. If they are not, you'll see the above error. The `server-deps-static-sites`
target creates dummy files that ensures that you'll be able to build.
### Build the web app
The sources for the web app live in `web/`. As long as you have `npm` installed (see above), building the web app
is really simple. Just type `make web` and you're in business:
``` shell
$ make web
...
```
This will build the web app using Create React App and then **copy the production build to the `server/site` folder**, so
that when you `make server` (or `make server-amd64`, ...), you will have the web app included in the `ntfy` binary.
If you're developing on the web app, it's best to just `cd web` and run `npm start` manually. This will open your browser
at `http://127.0.0.1:3000` with the web app, and as you edit the source files, they will be recompiled and the browser
will automatically refresh:
``` shell
$ cd web
$ npm start
```
### Build the docs
The sources for the docs live in `docs/`. Similarly to the web app, you can simply run `make docs` to build the
documentation. As long as you have `mkdocs` installed (see above), this should work fine:
``` shell
$ make docs
...
```
If you are changing the documentation, you should be running `mkdocs serve` directly. This will build the documentation,
serve the files at `http://127.0.0.1:8000/`, and rebuild every time you save the source files:
```
$ mkdocs serve
INFO - Building documentation...
INFO - Cleaning site directory
INFO - Documentation built in 5.53 seconds
INFO - [16:28:14] Serving on http://127.0.0.1:8000/
```
Then you can navigate to http://127.0.0.1:8000/ and whenever you change a markdown file in your text editor it'll automatically update.
## Android app
The ntfy Android app source code is available [on GitHub](https://github.com/binwiederhier/ntfy-android).
The Android app has two flavors:
* **Google Play:** The `play` flavor includes [Firebase (FCM)](https://firebase.google.com/) and requires a Firebase account
* **F-Droid:** The `fdroid` flavor does not include Firebase or Google dependencies
### Navigating the code
* [main/](https://github.com/binwiederhier/ntfy-android/tree/main/app/src/main) - Main Android app source code
* [play/](https://github.com/binwiederhier/ntfy-android/tree/main/app/src/play) - Google Play / Firebase specific code
* [fdroid/](https://github.com/binwiederhier/ntfy-android/tree/main/app/src/fdroid) - F-Droid Firebase stubs
* [build.gradle](https://github.com/binwiederhier/ntfy-android/blob/main/app/build.gradle) - Main build file
### IDE/Environment
You should download [Android Studio](https://developer.android.com/studio) (or [IntelliJ IDEA](https://www.jetbrains.com/idea/)
with the relevant Android plugins). Everything else will just be a pain for you. Do yourself a favor. 😀
### Check out the code
First check out the repository:
=== "via HTTPS"
``` shell
git clone https://github.com/binwiederhier/ntfy-android.git
cd ntfy-android
```
=== "via SSH"
``` shell
git clone git@github.com:binwiederhier/ntfy-android.git
cd ntfy-android
```
Then either follow the steps for building with or without Firebase.
### Build F-Droid flavor (no FCM)
!!! info
I do build the ntfy Android app using IntelliJ IDEA (Android Studio), so I don't know if these Gradle commands will
work without issues. Please give me feedback if it does/doesn't work for you.
Without Firebase, you may want to still change the default `app_base_url` in [strings.xml](https://github.com/binwiederhier/ntfy-android/blob/main/app/src/main/res/values/strings.xml)
if you're self-hosting the server. Then run:
```
# To build an unsigned .apk (app/build/outputs/apk/fdroid/*.apk)
./gradlew assembleFdroidRelease
# To build a bundle .aab (app/fdroid/release/*.aab)
./gradlew bundleFdroidRelease
```
### Build Play flavor (FCM)
!!! info
I do build the ntfy Android app using IntelliJ IDEA (Android Studio), so I don't know if these Gradle commands will
work without issues. Please give me feedback if it does/doesn't work for you.
To build your own version with Firebase, you must:
* Create a Firebase/FCM account
* Place your account file at `app/google-services.json`
* And change `app_base_url` in [strings.xml](https://github.com/binwiederhier/ntfy-android/blob/main/app/src/main/res/values/strings.xml)
* Then run:
```
# To build an unsigned .apk (app/build/outputs/apk/play/*.apk)
./gradlew assemblePlayRelease
# To build a bundle .aab (app/play/release/*.aab)
./gradlew bundlePlayRelease
```

1831
docs/emojis.md Normal file

File diff suppressed because it is too large Load Diff

368
docs/examples.md Normal file
View File

@@ -0,0 +1,368 @@
# Examples
There are a million ways to use ntfy, but here are some inspirations. I try to collect
<a href="https://github.com/binwiederhier/ntfy/tree/main/examples">examples on GitHub</a>, so be sure to check
those out, too.
## A long process is done: backups, copying data, pipelines, ...
I started adding notifications pretty much all of my scripts. Typically, I just chain the <tt>curl</tt> call
directly to the command I'm running. The following example will either send <i>Laptop backup succeeded</i>
or ⚠️ <i>Laptop backup failed</i> directly to my phone:
```
rsync -a root@laptop /backups/laptop \
&& zfs snapshot ... \
&& curl -H prio:low -d "Laptop backup succeeded" ntfy.sh/backups \
|| curl -H tags:warning -H prio:high -d "Laptop backup failed" ntfy.sh/backups
```
## Low disk space alerts
Here's a simple cronjob that I use to alert me when the disk space on the root disk is running low. It's simple, but
effective.
``` bash
#!/bin/bash
mingigs=10
avail=$(df | awk '$6 == "/" && $4 < '$mingigs' * 1024*1024 { print $4/1024/1024 }')
topicurl=https://ntfy.sh/mytopic
if [ -n "$avail" ]; then
curl \
-d "Only $avail GB available on the root disk. Better clean that up." \
-H "Title: Low disk space alert on $(hostname)" \
-H "Priority: high" \
-H "Tags: warning,cd" \
$topicurl
fi
```
## Server-sent messages in your web app
Just as you can [subscribe to topics in the Web UI](subscribe/web.md), you can use ntfy in your own
web application. Check out the <a href="/example.html">live example</a> or just look the source of this page.
## Notify on SSH login
Years ago my home server was broken into. That shook me hard, so every time someone logs into any machine that I
own, I now message myself. Here's an example of how to use <a href="https://en.wikipedia.org/wiki/Linux_PAM">PAM</a>
to notify yourself on SSH login.
=== "/etc/pam.d/sshd"
```
# at the end of the file
session optional pam_exec.so /usr/bin/ntfy-ssh-login.sh
```
=== "/usr/bin/ntfy-ssh-login.sh"
```bash
#!/bin/bash
if [ "${PAM_TYPE}" = "open_session" ]; then
curl \
-H prio:high \
-H tags:warning \
-d "SSH login: ${PAM_USER} from ${PAM_RHOST}" \
ntfy.sh/alerts
fi
```
## Collect data from multiple machines
The other day I was running tasks on 20 servers, and I wanted to collect the interim results
as a CSV in one place. Each of the servers was publishing to a topic as the results completed (`publish-result.sh`),
and I had one central collector to grab the results as they came in (`collect-results.sh`).
It looked something like this:
=== "collect-results.sh"
```bash
while read result; do
[ -n "$result" ] && echo "$result" >> results.csv
done < <(stdbuf -i0 -o0 curl -s ntfy.sh/results/raw)
```
=== "publish-result.sh"
```bash
// This script was run on each of the 20 servers. It was doing heavy processing ...
// Publish script results
curl -d "$(hostname),$count,$time" ntfy.sh/results
```
## Ansible, Salt and Puppet
You can easily integrate ntfy into Ansible, Salt, or Puppet to notify you when runs are done or are highstated.
One of my co-workers uses the following Ansible task to let him know when things are done:
```yml
- name: Send ntfy.sh update
uri:
url: "https://ntfy.sh/{{ ntfy_channel }}"
method: POST
body: "{{ inventory_hostname }} reseeding complete"
```
## Watchtower notifications (shoutrrr)
You can use `shoutrrr` generic webhook support to send watchtower notifications to your ntfy topic.
Example docker-compose.yml:
```yml
services:
watchtower:
image: containrrr/watchtower
environment:
- WATCHTOWER_NOTIFICATIONS=shoutrrr
- WATCHTOWER_NOTIFICATION_URL=generic+https://ntfy.sh/my_watchtower_topic?title=WatchtowerUpdates
```
Or, if you only want to send notifications using shoutrrr:
```
shoutrrr send -u "generic+https://ntfy.sh/my_watchtower_topic?title=WatchtowerUpdates" -m "testMessage"
```
## Random cronjobs
Alright, here's one for the history books. I desperately want the `github.com/ntfy` organization, but all my tickets with
GitHub have been hopeless. In case it ever becomes available, I want to know immediately.
``` cron
# Check github/ntfy user
*/6 * * * * if curl -s https://api.github.com/users/ntfy | grep "Not Found"; then curl -d "github.com/ntfy is available" -H "Tags: tada" -H "Prio: high" ntfy.sh/my-alerts; fi
~
```
## Download notifications (Sonarr, Radarr, Lidarr, Readarr, Prowlarr, SABnzbd)
It's possible to use custom scripts for all the *arr services, plus SABnzbd. Notifications for downloads, warnings, grabs etc.
Some simple bash scripts to achieve this are kindly provided in [nickexyz's repository](https://github.com/nickexyz/ntfy-shellscripts).
## Node-RED
You can use the HTTP request node to send messages with [Node-RED](https://nodered.org), some examples:
<details>
<summary>Example: Send a message (click to expand)</summary>
```
[
{
"id": "c956e688cc74ad8e",
"type": "http request",
"z": "fabdd7a3.4045a",
"name": "ntfy.sh",
"method": "POST",
"ret": "txt",
"paytoqs": "ignore",
"url": "https://ntfy.sh/mytopic",
"tls": "",
"persist": false,
"proxy": "",
"authType": "",
"senderr": false,
"credentials":
{
"user": "",
"password": ""
},
"x": 590,
"y": 3160,
"wires":
[
[]
]
},
{
"id": "32ee1eade51fae50",
"type": "function",
"z": "fabdd7a3.4045a",
"name": "data",
"func": "msg.payload = \"Something happened\";\nmsg.headers = {};\nmsg.headers['tags'] = 'house';\nmsg.headers['X-Title'] = 'Home Assistant';\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 470,
"y": 3160,
"wires":
[
[
"c956e688cc74ad8e"
]
]
},
{
"id": "b287e59cd2311815",
"type": "inject",
"z": "fabdd7a3.4045a",
"name": "Manual start",
"props":
[
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": "20",
"topic": "",
"payload": "",
"payloadType": "date",
"x": 330,
"y": 3160,
"wires":
[
[
"32ee1eade51fae50"
]
]
}
]
```
</details>
![Node red message flow](static/img/nodered-message.png)
<details>
<summary>Example: Send a picture (click to expand)</summary>
```
[
{
"id": "d135a13eadeb9d6d",
"type": "http request",
"z": "fabdd7a3.4045a",
"name": "Download image",
"method": "GET",
"ret": "bin",
"paytoqs": "ignore",
"url": "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png",
"tls": "",
"persist": false,
"proxy": "",
"authType": "",
"senderr": false,
"credentials":
{
"user": "",
"password": ""
},
"x": 490,
"y": 3320,
"wires":
[
[
"6e75bc41d2ec4a03"
]
]
},
{
"id": "6e75bc41d2ec4a03",
"type": "function",
"z": "fabdd7a3.4045a",
"name": "data",
"func": "msg.payload = msg.payload;\nmsg.headers = {};\nmsg.headers['tags'] = 'house';\nmsg.headers['X-Title'] = 'Home Assistant - Picture';\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 650,
"y": 3320,
"wires":
[
[
"eb160615b6ceda98"
]
]
},
{
"id": "eb160615b6ceda98",
"type": "http request",
"z": "fabdd7a3.4045a",
"name": "ntfy.sh",
"method": "PUT",
"ret": "bin",
"paytoqs": "ignore",
"url": "https://ntfy.sh/mytopic",
"tls": "",
"persist": false,
"proxy": "",
"authType": "",
"senderr": false,
"credentials":
{
"user": "",
"password": ""
},
"x": 770,
"y": 3320,
"wires":
[
[]
]
},
{
"id": "5b8dbf15c8a7a3a5",
"type": "inject",
"z": "fabdd7a3.4045a",
"name": "Manual start",
"props":
[
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": "20",
"topic": "",
"payload": "",
"payloadType": "date",
"x": 310,
"y": 3320,
"wires":
[
[
"d135a13eadeb9d6d"
]
]
}
]
```
</details>
![Node red picture flow](static/img/nodered-picture.png)
## Gatus service health check
An example for a custom alert with <a href="https://github.com/TwiN/gatus">Gatus</a>
```
alerting:
custom:
url: "https://ntfy.sh"
method: "POST"
body: |
{
"topic": "mytopic",
"message": "[ENDPOINT_NAME] - [ALERT_DESCRIPTION]",
"title": "Gatus",
"tags": ["[ALERT_TRIGGERED_OR_RESOLVED]"],
"priority": 3
}
default-alert:
enabled: true
description: "health check failed"
send-on-resolved: true
failure-threshold: 3
success-threshold: 3
placeholders:
ALERT_TRIGGERED_OR_RESOLVED:
TRIGGERED: "warning"
RESOLVED: "white_check_mark"
```

49
docs/faq.md Normal file
View File

@@ -0,0 +1,49 @@
# Frequently asked questions (FAQ)
## Isn't this like ...?
Who knows. I didn't do a lot of research before making this. It was fun making it.
## Can I use this in my app? Will it stay free?
Yes. As long as you don't abuse it, it'll be available and free of charge. I do not plan on monetizing
the service.
## What are the uptime guarantees?
Best effort.
## What happens if there are multiple subscribers to the same topic?
As per usual with pub-sub, all subscribers receive notifications if they are
subscribed to a topic.
## Will you know what topics exist, can you spy on me?
If you don't trust me or your messages are sensitive, run your own server. It's <a href="https://github.com/binwiederhier/ntfy">open source</a>.
That said, the logs do not contain any topic names or other details about you.
Messages are cached for the duration configured in `server.yml` (12h by default) to facilitate service restarts, message polling and to overcome
client network disruptions.
## Can I self-host it?
Yes. The server (including this Web UI) can be self-hosted, and the Android app supports adding topics from
your own server as well. Check out the [install instructions](install.md).
## Why is Firebase used?
In addition to caching messages locally and delivering them to long-polling subscribers, all messages are also
published to Firebase Cloud Messaging (FCM) (if `FirebaseKeyFile` is set, which it is on ntfy.sh). This
is to facilitate notifications on Android.
If you do not care for Firebase, I suggest you install the [F-Droid version](https://f-droid.org/en/packages/io.heckel.ntfy/)
of the app and [self-host your own ntfy server](install.md).
## How much battery does the Android app use?
If you use the ntfy.sh server, and you don't use the [instant delivery](subscribe/phone.md#instant-delivery) feature,
the Android app uses no additional battery, since Firebase Cloud Messaging (FCM) is used. If you use your own server,
or you use *instant delivery*, the app has to maintain a constant connection to the server, which consumes about 0-1% of
battery in 17h of use (on my phone). There has been a ton of testing and improvement around this. I think it's pretty
decent now.
## What is instant delivery?
[Instant delivery](subscribe/phone.md#instant-delivery) is a feature in the Android app. If turned on, the app maintains a constant connection to the
server and listens for incoming notifications. This consumes <a href="#battery-usage">additional battery</a>,
but delivers notifications instantly.
## Why is there no iOS app (yet)?
I don't have an iPhone or a Mac, so I didn't make an iOS app yet. It'd be awesome if
<a href="https://github.com/binwiederhier/ntfy/issues/4">someone else could help out</a>.

95
docs/index.md Normal file
View File

@@ -0,0 +1,95 @@
# Getting started
ntfy lets you **send push notifications to your phone or desktop via scripts from any computer**, using simple HTTP PUT
or POST requests. I use it to notify myself when scripts fail, or long-running commands complete.
## Step 1: Get the app
<a href="https://play.google.com/store/apps/details?id=io.heckel.ntfy"><img src="../../static/img/badge-googleplay.png"></a>
<a href="https://f-droid.org/en/packages/io.heckel.ntfy/"><img src="../../static/img/badge-fdroid.png"></a>
<a href="https://github.com/binwiederhier/ntfy/issues/4"><img src="../../static/img/badge-appstore.png"></a>
To [receive notifications on your phone](subscribe/phone.md), install the app, either via Google Play or F-Droid.
Once installed, open it and subscribe to a topic of your choosing. Topics don't have to explicitly be created, so just
pick a name and use it later when you [publish a message](publish.md). Note that **topic names are public, so it's wise
to choose something that cannot be guessed easily.**
For this guide, we'll just use `mytopic` as our topic name:
<figure markdown>
![adding a topic](static/img/getting-started-add.png){ width=500 }
<figcaption>Creating/adding your first topic</figcaption>
</figure>
That's it. After you tap "Subscribe", the app is listening for new messages on that topic.
## Step 2: Send a message
Now let's [send a message](publish.md) to our topic. It's easy in every language, since we're just using HTTP PUT/POST,
or with the [ntfy CLI](install.md). The message is in the request body. Here's an example showing how to publish a
simple message using a POST request:
=== "Command line (curl)"
```
curl -d "Backup successful 😀" ntfy.sh/mytopic
```
=== "ntfy CLI"
```
ntfy publish mytopic "Backup successful 😀"
```
=== "HTTP"
``` http
POST /mytopic HTTP/1.1
Host: ntfy.sh
Backup successful 😀
```
=== "JavaScript"
``` javascript
fetch('https://ntfy.sh/mytopic', {
method: 'POST', // PUT works too
body: 'Backup successful 😀'
})
```
=== "Go"
``` go
http.Post("https://ntfy.sh/mytopic", "text/plain",
strings.NewReader("Backup successful 😀"))
```
=== "Python"
``` python
requests.post("https://ntfy.sh/mytopic",
data="Backup successful 😀".encode(encoding='utf-8'))
```
=== "PHP"
``` php-inline
file_get_contents('https://ntfy.sh/mytopic', false, stream_context_create([
'http' => [
'method' => 'POST', // PUT also works
'header' => 'Content-Type: text/plain',
'content' => 'Backup successful 😀'
]
]));
```
This will create a notification that looks like this:
<figure markdown>
![basic notification](static/img/android-screenshot-basic-notification.png){ width=500 }
<figcaption>Android notification</figcaption>
</figure>
That's it. You're all set. Go play and read the rest of the docs. I highly recommend reading at least the page on
[publishing messages](publish.md), as well as the detailed page on the [Android app](subscribe/phone.md).
Here's another video showing the entire process:
<figure>
<video controls muted autoplay loop width="650" src="static/img/android-video-overview.mp4"></video>
<figcaption>Sending push notifications to your Android phone</figcaption>
</figure>

237
docs/install.md Normal file
View File

@@ -0,0 +1,237 @@
# Installing ntfy
The `ntfy` CLI allows you to [publish messages](publish.md), [subscribe to topics](subscribe/cli.md) as well as to
self-host your own ntfy server. It's all pretty straight forward. Just install the binary, package or Docker image,
configure it and run it. Just like any other software. No fuzz.
!!! info
The following steps are only required if you want to **self-host your own ntfy server or you want to use the ntfy CLI**.
If you just want to [send messages using ntfy.sh](publish.md), you don't need to install anything. You can just use
`curl`.
## General steps
The ntfy server comes as a statically linked binary and is shipped as tarball, deb/rpm packages and as a Docker image.
We support amd64, armv7 and arm64.
1. Install ntfy using one of the methods described below
2. Then (optionally) edit `/etc/ntfy/server.yml` for the server (see [configuration](config.md) or [sample server.yml](https://github.com/binwiederhier/ntfy/blob/main/server/server.yml))
3. Or (optionally) create/edit `~/.config/ntfy/client.yml` (or `/etc/ntfy/client.yml`, see [sample client.yml](https://github.com/binwiederhier/ntfy/blob/main/client/client.yml))
To run the ntfy server, then just run `ntfy serve` (or `systemctl start ntfy` when using the deb/rpm).
To send messages, use `ntfy publish`. To subscribe to topics, use `ntfy subscribe` (see [subscribing via CLI][subscribe/cli.md]
for details).
## Binaries and packages
Please check out the [releases page](https://github.com/binwiederhier/ntfy/releases) for binaries and
deb/rpm packages.
=== "x86_64/amd64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.1/ntfy_1.21.1_linux_x86_64.tar.gz
tar zxvf ntfy_1.21.1_linux_x86_64.tar.gz
sudo cp -a ntfy_1.21.1_linux_x86_64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.21.1_linux_x86_64/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "armv6"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.1/ntfy_1.21.1_linux_armv6.tar.gz
tar zxvf ntfy_1.21.1_linux_armv6.tar.gz
sudo cp -a ntfy_1.21.1_linux_armv6/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.21.1_linux_armv6/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "armv7/armhf"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.1/ntfy_1.21.1_linux_armv7.tar.gz
tar zxvf ntfy_1.21.1_linux_armv7.tar.gz
sudo cp -a ntfy_1.21.1_linux_armv7/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.21.1_linux_armv7/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "arm64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.1/ntfy_1.21.1_linux_arm64.tar.gz
tar zxvf ntfy_1.21.1_linux_arm64.tar.gz
sudo cp -a ntfy_1.21.1_linux_arm64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.21.1_linux_arm64/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
## Debian/Ubuntu repository
Installation via Debian repository:
=== "x86_64/amd64"
```bash
curl -sSL https://archive.heckel.io/apt/pubkey.txt | sudo apt-key add -
sudo apt install apt-transport-https
sudo sh -c "echo 'deb [arch=amd64] https://archive.heckel.io/apt debian main' \
> /etc/apt/sources.list.d/archive.heckel.io.list"
sudo apt update
sudo apt install ntfy
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "armv7/armhf"
```bash
curl -sSL https://archive.heckel.io/apt/pubkey.txt | sudo apt-key add -
sudo apt install apt-transport-https
sudo sh -c "echo 'deb [arch=armhf] https://archive.heckel.io/apt debian main' \
> /etc/apt/sources.list.d/archive.heckel.io.list"
sudo apt update
sudo apt install ntfy
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "arm64"
```bash
curl -sSL https://archive.heckel.io/apt/pubkey.txt | sudo apt-key add -
sudo apt install apt-transport-https
sudo sh -c "echo 'deb [arch=arm64] https://archive.heckel.io/apt debian main' \
> /etc/apt/sources.list.d/archive.heckel.io.list"
sudo apt update
sudo apt install ntfy
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
Manually installing the .deb file:
=== "x86_64/amd64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.1/ntfy_1.21.1_linux_amd64.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "armv6"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.1/ntfy_1.21.1_linux_armv6.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "armv7/armhf"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.1/ntfy_1.21.1_linux_armv7.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "arm64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.1/ntfy_1.21.1_linux_arm64.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
## Fedora/RHEL/CentOS
=== "x86_64/amd64"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.21.1/ntfy_1.21.1_linux_amd64.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "armv6"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.21.1/ntfy_1.21.1_linux_armv6.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "armv7/armhf"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.21.1/ntfy_1.21.1_linux_armv7.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "arm64"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.21.1/ntfy_1.21.1_linux_arm64.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
## Arch Linux
ntfy can be installed using an [AUR package](https://aur.archlinux.org/packages/ntfysh-bin/). You can use an [AUR helper](https://wiki.archlinux.org/title/AUR_helpers) like `paru`, `yay` or others to download, build and install ntfy and keep it up to date.
```
paru -S ntfysh-bin
```
Alternatively, run the following commands to install ntfy manually:
```
curl https://aur.archlinux.org/cgit/aur.git/snapshot/ntfysh-bin.tar.gz | tar xzv
cd ntfysh-bin
makepkg -si
```
## Docker
The [ntfy image](https://hub.docker.com/r/binwiederhier/ntfy) is available for amd64, armv7 and arm64. It should be pretty
straight forward to use.
The server exposes its web UI and the API on port 80, so you need to expose that in Docker. To use the persistent
[message cache](config.md#message-cache), you also need to map a volume to `/var/cache/ntfy`. To change other settings,
you should map `/etc/ntfy`, so you can edit `/etc/ntfy/server.yml`.
Basic usage (no cache or additional config):
```
docker run -p 80:80 -it binwiederhier/ntfy serve
```
With persistent cache (configured as command line arguments):
```bash
docker run \
-v /var/cache/ntfy:/var/cache/ntfy \
-p 80:80 \
-it \
binwiederhier/ntfy \
--cache-file /var/cache/ntfy/cache.db \
serve
```
With other config options (configured via `/etc/ntfy/server.yml`, see [configuration](config.md) for details):
```bash
docker run \
-v /etc/ntfy:/etc/ntfy \
-p 80:80 \
-it \
binwiederhier/ntfy \
serve
```
Using docker-compose:
```yaml
version: "2.1"
services:
ntfy:
image: binwiederhier/ntfy
container_name: ntfy
command:
- serve
volumes:
- /var/cache/ntfy:/var/cache/ntfy
- /etc/ntfy:/etc/ntfy
ports:
- 80:80
restart: unless-stopped
```
Alternatively, you may wish to build a customized Docker image that can be run with fewer command-line arguments and without delivering the configuration file separately.
```
FROM binwiederhier/ntfy
COPY server.yml /etc/ntfy/server.yml
ENTRYPOINT ["ntfy", "serve"]
```
This image can be pushed to a container registry and shipped independently. All that's needed when running it is mapping ntfy's port to a host port.

12
docs/privacy.md Normal file
View File

@@ -0,0 +1,12 @@
# Privacy policy
I love free software, and I'm doing this because it's fun. I have no bad intentions, and **I will
never monetize or sell your information, and this service and software will always stay free and open.**
Neither the server nor the app record any personal information, or share any of the messages and topics with
any outside service. All data is exclusively used to make the service function properly. The only external service
I use is Firebase Cloud Messaging (FCM) service, which is required to provide instant Android notifications (see
[FAQ](faq.md) for details). To avoid FCM altogether, download the F-Droid version.
The web server does not log or otherwise store request paths, remote IP addresses or even topics or messages,
aside from a short on-disk cache to support service restarts.

2618
docs/publish.md Normal file

File diff suppressed because it is too large Load Diff

421
docs/releases.md Normal file
View File

@@ -0,0 +1,421 @@
# Release notes
Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases)
and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).
<!--
## ntfy Android app v1.12.0 (UNRELEASED)
**Features:**
* Custom notification [action buttons](https://ntfy.sh/docs/publish/#action-buttons) ([#134](https://github.com/binwiederhier/ntfy/issues/134),
thanks to [@mrherman](https://github.com/mrherman) for reporting)
* Support for [ntfy:// deep links](https://ntfy.sh/docs/subscribe/phone/#ntfy-links) ([#20](https://github.com/binwiederhier/ntfy/issues/20), thanks
to [@Copephobia](https://github.com/Copephobia) for reporting)
* [Fastlane metadata](https://hosted.weblate.org/projects/ntfy/android-fastlane/) can now be translated too ([#198](https://github.com/binwiederhier/ntfy/issues/198),
thanks to [@StoyanDimitrov](https://github.com/StoyanDimitrov) for reporting)
* Channel settings option to configure DND override, sounds, etc. ([#91](https://github.com/binwiederhier/ntfy/issues/91))
**Bugs:**
* Validate URLs when changing default server and server in user management ([#193](https://github.com/binwiederhier/ntfy/issues/193),
thanks to [@StoyanDimitrov](https://github.com/StoyanDimitrov) for reporting)
* Error in sending test notification in different languages ([#209](https://github.com/binwiederhier/ntfy/issues/209),
thanks to [@StoyanDimitrov](https://github.com/StoyanDimitrov) for reporting)
* "[x] Instant delivery in doze mode" checkbox does not work properly ([#211](https://github.com/binwiederhier/ntfy/issues/211))
* Disallow "http" GET/HEAD actions with body ([#221](https://github.com/binwiederhier/ntfy/issues/221), thanks to
[@cmeis](https://github.com/cmeis) for reporting)
* Action "view" with "clear=true" does not work on some phones ([#220](https://github.com/binwiederhier/ntfy/issues/220), thanks to
[@cmeis](https://github.com/cmeis) for reporting)
* Do not group foreground service notification with others ([#219](https://github.com/binwiederhier/ntfy/issues/219), thanks to
[@s-h-a-r-d](https://github.com/s-h-a-r-d) for reporting)
**Additional translations:**
* Japanese (thanks to [@shak](https://hosted.weblate.org/user/shak/))
* Russian (thanks to [@flamey](https://hosted.weblate.org/user/flamey/) and [@ilya.mikheev.coder](https://hosted.weblate.org/user/ilya.mikheev.coder/))
**Thanks for testing:**
Thanks to [@s-h-a-r-d](https://github.com/s-h-a-r-d) (aka @Shard), [@cmeis](https://github.com/cmeis),
@poblabs, and everyone I forgot for testing.
-->
## ntfy server v1.21.1
Released Apr 24, 2022
**Features:**
* Custom notification [action buttons](https://ntfy.sh/docs/publish/#action-buttons) ([#134](https://github.com/binwiederhier/ntfy/issues/134),
thanks to [@mrherman](https://github.com/mrherman) for reporting)
* Added ARMv6 build ([#200](https://github.com/binwiederhier/ntfy/issues/200), thanks to [@jcrubioa](https://github.com/jcrubioa) for reporting)
* Web app internationalization support 🇧🇬 🇩🇪 🇺🇸 🌎 ([#189](https://github.com/binwiederhier/ntfy/issues/189))
**Bugs:**
* Web app: English language strings fixes, additional descriptions for settings ([#203](https://github.com/binwiederhier/ntfy/issues/203), thanks to [@StoyanDimitrov](https://github.com/StoyanDimitrov))
* Web app: Show error message snackbar when sending test notification fails ([#205](https://github.com/binwiederhier/ntfy/issues/205), thanks to [@cmeis](https://github.com/cmeis))
* Web app: basic URL validation in user management ([#204](https://github.com/binwiederhier/ntfy/issues/204), thanks to [@cmeis](https://github.com/cmeis))
* Disallow "http" GET/HEAD actions with body ([#221](https://github.com/binwiederhier/ntfy/issues/221), thanks to
[@cmeis](https://github.com/cmeis) for reporting)
**Translations (web app):**
* Bulgarian (thanks to [@StoyanDimitrov](https://github.com/StoyanDimitrov))
* German (thanks to [@cmeis](https://github.com/cmeis))
* Indonesian (thanks to [@linerly](https://hosted.weblate.org/user/linerly/))
* Japanese (thanks to [@shak](https://hosted.weblate.org/user/shak/))
* Norwegian Bokmål (thanks to [@comradekingu](https://github.com/comradekingu))
* Russian (thanks to [@flamey](https://hosted.weblate.org/user/flamey/) and [@ilya.mikheev.coder](https://hosted.weblate.org/user/ilya.mikheev.coder/))
* Spanish (thanks to [@rogeliodh](https://github.com/rogeliodh))
* Turkish (thanks to [@ersen](https://ersen.moe/))
**Integrations:**
[Apprise](https://github.com/caronc/apprise) support was fully released in [v0.9.8.2](https://github.com/caronc/apprise/releases/tag/v0.9.8.2)
of Apprise. Thanks to [@particledecay](https://github.com/particledecay) and [@caronc](https://github.com/caronc) for their fantastic work.
You can try it yourself like this (detailed usage in the [Apprise wiki](https://github.com/caronc/apprise/wiki/Notify_ntfy)):
```
pip3 install apprise
apprise -b "Hi there" ntfys://mytopic
```
## ntfy Android app v1.11.0
Released Apr 7, 2022
**Features:**
* Download attachments to cache folder ([#181](https://github.com/binwiederhier/ntfy/issues/181))
* Regularly delete attachments for deleted notifications ([#142](https://github.com/binwiederhier/ntfy/issues/142))
* Translations to different languages ([#188](https://github.com/binwiederhier/ntfy/issues/188), thanks to
[@StoyanDimitrov](https://github.com/StoyanDimitrov) for initiating things)
**Bugs:**
* IllegalStateException: Failed to build unique file ([#177](https://github.com/binwiederhier/ntfy/issues/177), thanks to [@Fallenbagel](https://github.com/Fallenbagel) for reporting)
* SQLiteConstraintException: Crash during UP registration ([#185](https://github.com/binwiederhier/ntfy/issues/185))
* Refresh preferences screen after settings import (#183, thanks to [@cmeis](https://github.com/cmeis) for reporting)
* Add priority strings to strings.xml to make it translatable (#192, thanks to [@StoyanDimitrov](https://github.com/StoyanDimitrov))
**Translations:**
* English language improvements (thanks to [@comradekingu](https://github.com/comradekingu))
* Bulgarian (thanks to [@StoyanDimitrov](https://github.com/StoyanDimitrov))
* Chinese/Simplified (thanks to [@poi](https://hosted.weblate.org/user/poi) and [@PeterCxy](https://hosted.weblate.org/user/PeterCxy))
* Dutch (*incomplete*, thanks to [@diony](https://hosted.weblate.org/user/diony))
* French (thanks to [@Kusoneko](https://kusoneko.moe/) and [@mlcsthor](https://hosted.weblate.org/user/mlcsthor/))
* German (thanks to [@cmeis](https://github.com/cmeis))
* Italian (thanks to [@theTranslator](https://hosted.weblate.org/user/theTranslator/))
* Indonesian (thanks to [@linerly](https://hosted.weblate.org/user/linerly/))
* Norwegian Bokmål (*incomplete*, thanks to [@comradekingu](https://github.com/comradekingu))
* Portuguese/Brazil (thanks to [@LW](https://hosted.weblate.org/user/LW/))
* Spanish (thanks to [@rogeliodh](https://github.com/rogeliodh))
* Turkish (thanks to [@ersen](https://ersen.moe/))
**Thanks:**
* Many thanks to [@cmeis](https://github.com/cmeis), [@Fallenbagel](https://github.com/Fallenbagel), [@Joeharrison94](https://github.com/Joeharrison94),
and [@rogeliodh](https://github.com/rogeliodh) for input on the new attachment logic, and for testing the release
## ntfy server v1.20.0
Released Apr 6, 2022
**Features:**:
* Added message bar and publish dialog ([#196](https://github.com/binwiederhier/ntfy/issues/196))
**Bugs:**
* Added `EXPOSE 80/tcp` to Dockerfile to support auto-discovery in [Traefik](https://traefik.io/) ([#195](https://github.com/binwiederhier/ntfy/issues/195), thanks to [@s-h-a-r-d](https://github.com/s-h-a-r-d))
**Documentation:**
* Added docker-compose example to [install instructions](install.md#docker) ([#194](https://github.com/binwiederhier/ntfy/pull/194), thanks to [@s-h-a-r-d](https://github.com/s-h-a-r-d))
**Integrations:**
* [Apprise](https://github.com/caronc/apprise) has added integration into ntfy ([#99](https://github.com/binwiederhier/ntfy/issues/99), [apprise#524](https://github.com/caronc/apprise/pull/524),
thanks to [@particledecay](https://github.com/particledecay) and [@caronc](https://github.com/caronc) for their fantastic work)
## ntfy server v1.19.0
Released Mar 30, 2022
**Bugs:**
* Do not pack binary with `upx` for armv7/arm64 due to `illegal instruction` errors ([#191](https://github.com/binwiederhier/ntfy/issues/191), thanks to [@iexos](https://github.com/iexos))
* Do not allow comma in topic name in publish via GET endpoint (no ticket)
* Add "Access-Control-Allow-Origin: *" for attachments (no ticket, thanks to @FrameXX)
* Make pruning run again in web app ([#186](https://github.com/binwiederhier/ntfy/issues/186))
* Added missing params `delay` and `email` to publish as JSON body (no ticket)
**Documentation:**
* Improved [e-mail publishing](config.md#e-mail-publishing) documentation
## ntfy server v1.18.1
Released Mar 21, 2022
_This release ships no features or bug fixes. It's merely a documentation update._
**Documentation:**
* Overhaul of [developer documentation](https://ntfy.sh/docs/develop/)
* PowerShell examples for [publish documentation](https://ntfy.sh/docs/publish/) ([#138](https://github.com/binwiederhier/ntfy/issues/138), thanks to [@Joeharrison94](https://github.com/Joeharrison94))
* Additional examples for [NodeRED, Gatus, Sonarr, Radarr, ...](https://ntfy.sh/docs/examples/) (thanks to [@nickexyz](https://github.com/nickexyz))
* Fixes in developer instructions (thanks to [@Fallenbagel](https://github.com/Fallenbagel) for reporting)
## ntfy Android app v1.10.0
Released Mar 21, 2022
**Features:**
* Support for UnifiedPush 2.0 specification (bytes messages, [#130](https://github.com/binwiederhier/ntfy/issues/130))
* Export/import settings and subscriptions ([#115](https://github.com/binwiederhier/ntfy/issues/115), thanks [@cmeis](https://github.com/cmeis) for reporting)
* Open "Click" link when tapping notification ([#110](https://github.com/binwiederhier/ntfy/issues/110), thanks [@cmeis](https://github.com/cmeis) for reporting)
* JSON stream deprecation banner ([#164](https://github.com/binwiederhier/ntfy/issues/164))
**Bug fixes:**
* Display locale-specific times, with AM/PM or 24h format ([#140](https://github.com/binwiederhier/ntfy/issues/140), thanks [@hl2guide](https://github.com/hl2guide) for reporting)
## ntfy server v1.18.0
Released Mar 16, 2022
**Features:**
* [Publish messages as JSON](https://ntfy.sh/docs/publish/#publish-as-json) ([#133](https://github.com/binwiederhier/ntfy/issues/133),
thanks [@cmeis](https://github.com/cmeis) for reporting, thanks to [@Joeharrison94](https://github.com/Joeharrison94) and
[@Fallenbagel](https://github.com/Fallenbagel) for testing)
**Bug fixes:**
* rpm: do not overwrite server.yaml on package upgrade ([#166](https://github.com/binwiederhier/ntfy/issues/166), thanks [@waclaw66](https://github.com/waclaw66) for reporting)
* Typo in [ntfy.sh/announcements](https://ntfy.sh/announcements) topic ([#170](https://github.com/binwiederhier/ntfy/pull/170), thanks to [@sandebert](https://github.com/sandebert))
* Readme image URL fixes ([#156](https://github.com/binwiederhier/ntfy/pull/156), thanks to [@ChaseCares](https://github.com/ChaseCares))
**Deprecations:**
* Removed the ability to run server as `ntfy` (as opposed to `ntfy serve`) as per [deprecation](deprecations.md)
## ntfy server v1.17.1
Released Mar 12, 2022
**Bug fixes:**
* Replace `crypto.subtle` with `hashCode` to errors with Brave/FF-Windows (#157, thanks for reporting @arminus)
## ntfy server v1.17.0
Released Mar 11, 2022
**Features & bug fixes:**
* Replace [web app](https://ntfy.sh/app) with a React/MUI-based web app from the 21st century (#111)
* Web UI broken with auth (#132, thanks for reporting @arminus)
* Send static web resources as `Content-Encoding: gzip`, i.e. docs and web app (no ticket)
* Add support for auth via `?auth=...` query param, used by WebSocket in web app (no ticket)
## ntfy server v1.16.0
Released Feb 27, 2022
**Features & Bug fixes:**
* Add [auth support](https://ntfy.sh/docs/subscribe/cli/#authentication) for subscribing with CLI (#147/#148, thanks @lrabane)
* Add support for [?since=<id>](https://ntfy.sh/docs/subscribe/api/#fetch-cached-messages) (#151, thanks for reporting @nachotp)
**Documentation:**
* Add [watchtower/shoutrr examples](https://ntfy.sh/docs/examples/#watchtower-notifications-shoutrrr) (#150, thanks @rogeliodh)
* Add [release notes](https://ntfy.sh/docs/releases/)
**Technical notes:**
* As of this release, message IDs will be 12 characters long (as opposed to 10 characters). This is to be able to
distinguish them from Unix timestamps for #151.
## ntfy Android app v1.9.1
Released Feb 16, 2022
**Features:**
* Share to topic feature (#131, thanks u/emptymatrix for reporting)
* Ability to pick a default server (#127, thanks to @poblabs for reporting and testing)
* Automatically delete notifications (#71, thanks @arjan-s for reporting)
* Dark theme: Improvements around style and contrast (#119, thanks @kzshantonu for reporting)
**Bug fixes:**
* Do not attempt to download attachments if they are already expired (#135)
* Fixed crash in AddFragment as seen per stack trace in Play Console (no ticket)
**Other thanks:**
* Thanks to @rogeliodh, @cmeis and @poblabs for testing
## ntfy server v1.15.0
Released Feb 14, 2022
**Features & bug fixes:**
* Compress binaries with `upx` (#137)
* Add `visitor-request-limit-exempt-hosts` to exempt friendly hosts from rate limits (#144)
* Double default requests per second limit from 1 per 10s to 1 per 5s (no ticket)
* Convert `\n` to new line for `X-Message` header as prep for sharing feature (see #136)
* Reduce bcrypt cost to 10 to make auth timing more reasonable on slow servers (no ticket)
* Docs update to include [public test topics](https://ntfy.sh/docs/publish/#public-topics) (no ticket)
## ntfy server v1.14.1
Released Feb 9, 2022
**Bug fixes:**
* Fix ARMv8 Docker build (#113, thanks to @djmaze)
* No other significant changes
## ntfy Android app v1.8.1
Released Feb 6, 2022
**Features:**
* Support [auth / access control](https://ntfy.sh/docs/config/#access-control) (#19, thanks to @cmeis, @drsprite/@poblabs,
@gedw99, @karmanyaahm, @Mek101, @gc-ss, @julianfoad, @nmoseman, Jakob, PeterCxy, Techlosopher)
* Export/upload log now allows censored/uncensored logs (no ticket)
* Removed wake lock (except for notification dispatching, no ticket)
* Swipe to remove notifications (#117)
**Bug fixes:**
* Fix download issues on SDK 29 "Movement not allowed" (#116, thanks Jakob)
* Fix for Android 12 crashes (#124, thanks @eskilop)
* Fix WebSocket retry logic bug with multiple servers (no ticket)
* Fix race in refresh logic leading to duplicate connections (no ticket)
* Fix scrolling issue in subscribe to topic dialog (#131, thanks @arminus)
* Fix base URL text field color in dark mode, and size with large fonts (no ticket)
* Fix action bar color in dark mode (make black, no ticket)
**Notes:**
* Foundational work for per-subscription settings
## ntfy server v1.14.0
Released Feb 3, 2022
**Features**:
* Server-side for [authentication & authorization](https://ntfy.sh/docs/config/#access-control) (#19, thanks for testing @cmeis, and for input from @gedw99, @karmanyaahm, @Mek101, @gc-ss, @julianfoad, @nmoseman, Jakob, PeterCxy, Techlosopher)
* Support `NTFY_TOPIC` env variable in `ntfy publish` (#103)
**Bug fixes**:
* Binary UnifiedPush messages should not be converted to attachments (part 1, #101)
**Docs**:
* Clarification regarding attachments (#118, thanks @xnumad)
## ntfy Android app v1.7.1
Released Jan 21, 2022
**New features:**
* Battery improvements: wakelock disabled by default (#76)
* Dark mode: Allow changing app appearance (#102)
* Report logs: Copy/export logs to help troubleshooting (#94)
* WebSockets (experimental): Use WebSockets to subscribe to topics (#96, #100, #97)
* Show battery optimization banner (#105)
**Bug fixes:**
* (Partial) support for binary UnifiedPush messages (#101)
**Notes:**
* The foreground wakelock is now disabled by default
* The service restarter is now scheduled every 3h instead of every 6h
## ntfy server v1.13.0
Released Jan 16, 2022
**Features:**
* [Websockets](https://ntfy.sh/docs/subscribe/api/#websockets) endpoint
* Listen on Unix socket, see [config option](https://ntfy.sh/docs/config/#config-options) `listen-unix`
## ntfy Android app v1.6.0
Released Jan 14, 2022
**New features:**
* Attachments: Send files to the phone (#25, #15)
* Click action: Add a click action URL to notifications (#85)
* Battery optimization: Allow disabling persistent wake-lock (#76, thanks @MatMaul)
* Recognize imported user CA certificate for self-hosted servers (#87, thanks @keith24)
* Remove mentions of "instant delivery" from F-Droid to make it less confusing (no ticket)
**Bug fixes:**
* Subscription "muted until" was not always respected (#90)
* Fix two stack traces reported by Play console vitals (no ticket)
* Truncate FCM messages >4,000 bytes, prefer instant messages (#84)
## ntfy server v1.12.1
Released Jan 14, 2022
**Bug fixes:**
* Fix security issue with attachment peaking (#93)
## ntfy server v1.12.0
Released Jan 13, 2022
**Features:**
* [Attachments](https://ntfy.sh/docs/publish/#attachments) (#25, #15)
* [Click action](https://ntfy.sh/docs/publish/#click-action) (#85)
* Increase FCM priority for high/max priority messages (#70)
**Bug fixes:**
* Make postinst script work properly for rpm-based systems (#83, thanks @cmeis)
* Truncate FCM messages longer than 4000 bytes (#84)
* Fix `listen-https` port (no ticket)
## ntfy Android app v1.5.2
Released Jan 3, 2022
**New features:**
* Allow using ntfy as UnifiedPush distributor (#9)
* Support for longer message up to 4096 bytes (#77)
* Minimum priority: show notifications only if priority X or higher (#79)
* Allowing disabling broadcasts in global settings (#80)
**Bug fixes:**
* Allow int/long extras for SEND_MESSAGE intent (#57)
* Various battery improvement fixes (#76)
## ntfy server v1.11.2
Released Jan 1, 2022
**Features & bug fixes:**
* Increase message limit to 4096 bytes (4k) #77
* Docs for [UnifiedPush](https://unifiedpush.org) #9
* Increase keepalive interval to 55s #76
* Increase Firebase keepalive to 3 hours #76
## ntfy server v1.10.0
Released Dec 28, 2021
**Features & bug fixes:**
* [Publish messages via e-mail](ntfy.sh/docs/publish/#e-mail-publishing) #66
* Server-side work to support [unifiedpush.org](https://unifiedpush.org) #64
* Fixing the Santa bug #65
## Older releases
For older releases, check out the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases)
and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).

129
docs/static/css/extra.css vendored Normal file
View File

@@ -0,0 +1,129 @@
:root {
--md-primary-fg-color: #338574;
--md-primary-fg-color--light: #338574;
--md-primary-fg-color--dark: #338574;
}
.md-header__button.md-logo :is(img, svg) {
width: unset !important;
}
.md-sidebar {
width: 12.5rem !important;
}
.md-typeset h4 {
font-weight: 500 !important;
margin: 0 !important;
font-size: 1.1em !important;
}
.admonition {
font-size: .74rem !important;
}
article {
padding-bottom: 50px;
}
figure img, figure video {
border-radius: 7px;
}
body[data-md-color-scheme="default"] figure img, body[data-md-color-scheme="default"] figure video {
filter: drop-shadow(3px 3px 3px #ccc);
}
body[data-md-color-scheme="slate"] figure img, body[data-md-color-scheme="slate"] figure video {
filter: drop-shadow(3px 3px 3px #1a1313);
}
figure video {
width: 100%;
max-height: 450px;
}
.remove-md-box {
background: none;
border: none;
margin: 0 auto;
}
.remove-md-box td {
padding: 0 10px
}
/* Lightbox; thanks to https://yossiabramov.com/blog/vanilla-js-lightbox */
.screenshots {
text-align: center;
}
.screenshots img {
height: 230px;
margin: 3px;
border-radius: 5px;
filter: drop-shadow(2px 2px 2px #ddd);
}
.screenshots .nowrap {
white-space: nowrap;
}
.lightbox {
opacity: 0;
visibility: hidden;
position: fixed;
left:0;
right: 0;
top: 0;
bottom: 0;
z-index: -1;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s ease-in;
}
.lightbox.show {
background-color: rgba(0,0,0, 0.75);
opacity: 1;
visibility: visible;
z-index: 1000;
}
.lightbox img {
max-width: 90%;
max-height: 90%;
filter: drop-shadow(5px 5px 10px #222);
border-radius: 5px;
}
.lightbox .close-lightbox {
cursor: pointer;
position: absolute;
top: 30px;
right: 30px;
width: 20px;
height: 20px;
}
.lightbox .close-lightbox::after,
.lightbox .close-lightbox::before {
content: '';
width: 3px;
height: 20px;
background-color: #ddd;
position: absolute;
border-radius: 5px;
transform: rotate(45deg);
}
.lightbox .close-lightbox::before {
transform: rotate(-45deg);
}
.lightbox .close-lightbox:hover::after,
.lightbox .close-lightbox:hover::before {
background-color: #fff;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

BIN
docs/static/img/badge-appstore.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
docs/static/img/badge-fdroid.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
docs/static/img/badge-googleplay.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
docs/static/img/favicon.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
docs/static/img/foreground-service.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
docs/static/img/getting-started-add.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

BIN
docs/static/img/nodered-message.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
docs/static/img/nodered-picture.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

BIN
docs/static/img/ntfy.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

47
docs/static/img/priority-1.svg vendored Normal file
View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24px"
viewBox="0 0 24 24"
width="24px"
fill="#000000"
version="1.1"
id="svg1428"
sodipodi:docname="priority_1_24dp.svg"
inkscape:version="1.1.1 (3bf5ae0, 2021-09-20)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1432" />
<sodipodi:namedview
id="namedview1430"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="20.517358"
inkscape:cx="22.834324"
inkscape:cy="15.742768"
inkscape:window-width="1863"
inkscape:window-height="1025"
inkscape:window-x="57"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg1428" />
<path
style="color:#000000;fill:#999999;fill-opacity:1;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
d="m 12.195014,20.828316 a 1.2747098,1.2747098 0 0 0 0.661605,-0.185206 l 6.646593,-4.037178 a 1.2745823,1.2745823 0 0 0 0.427537,-1.751107 1.2745823,1.2745823 0 0 0 -1.750928,-0.427718 l -5.984807,3.635327 -5.9848086,-3.635327 a 1.2745823,1.2745823 0 0 0 -1.750927,0.427718 1.2745823,1.2745823 0 0 0 0.427536,1.751107 l 6.6464146,4.037178 a 1.2747098,1.2747098 0 0 0 0.661785,0.185206 z"
id="rect3554" />
<path
style="color:#000000;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
d="m 12.195014,15.694014 a 1.2747098,1.2747098 0 0 0 0.661605,-0.185206 l 6.646593,-4.037176 A 1.2745823,1.2745823 0 0 0 19.930749,9.7205243 1.2745823,1.2745823 0 0 0 18.179821,9.2928073 L 12.195014,12.928134 6.2102054,9.2928073 a 1.2745823,1.2745823 0 0 0 -1.750927,0.427717 1.2745823,1.2745823 0 0 0 0.427536,1.7511077 l 6.6464146,4.037176 a 1.2747098,1.2747098 0 0 0 0.661785,0.185206 z"
id="path9314" />
<path
style="color:#000000;fill:#cccccc;fill-opacity:1;stroke:none;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
d="m 12.116784,10.426777 a 1.2747098,1.2747098 0 0 0 0.661606,-0.185205 l 6.646593,-4.0371767 a 1.2745823,1.2745823 0 0 0 0.427537,-1.751108 1.2745823,1.2745823 0 0 0 -1.750928,-0.427718 l -5.984808,3.635327 -5.9848066,-3.635327 a 1.2745823,1.2745823 0 0 0 -1.750928,0.427718 1.2745823,1.2745823 0 0 0 0.427537,1.751108 L 11.455,10.241572 a 1.2747098,1.2747098 0 0 0 0.661784,0.185205 z"
id="path9316" />
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

43
docs/static/img/priority-2.svg vendored Normal file
View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24px"
viewBox="0 0 24 24"
width="24px"
fill="#000000"
version="1.1"
id="svg1428"
sodipodi:docname="priority_2_24dp.svg"
inkscape:version="1.1.1 (3bf5ae0, 2021-09-20)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1432" />
<sodipodi:namedview
id="namedview1430"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="20.517358"
inkscape:cx="22.834324"
inkscape:cy="15.742768"
inkscape:window-width="1863"
inkscape:window-height="1025"
inkscape:window-x="57"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg1428" />
<path
style="color:#000000;fill:#999999;fill-opacity:1;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
d="m 12.172712,17.774352 a 1.2747098,1.2747098 0 0 0 0.661605,-0.185206 l 6.646593,-4.037178 a 1.2745823,1.2745823 0 0 0 0.427537,-1.751107 1.2745823,1.2745823 0 0 0 -1.750928,-0.427718 L 12.172712,15.00847 6.1879033,11.373143 a 1.2745823,1.2745823 0 0 0 -1.750927,0.427718 1.2745823,1.2745823 0 0 0 0.427536,1.751107 l 6.6464147,4.037178 a 1.2747098,1.2747098 0 0 0 0.661785,0.185206 z"
id="rect3554" />
<path
style="color:#000000;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
d="m 12.172712,12.64005 a 1.2747098,1.2747098 0 0 0 0.661605,-0.185206 L 19.48091,8.4176679 A 1.2745823,1.2745823 0 0 0 19.908447,6.6665602 1.2745823,1.2745823 0 0 0 18.157519,6.2388432 L 12.172712,9.8741699 6.1879033,6.2388432 a 1.2745823,1.2745823 0 0 0 -1.750927,0.427717 1.2745823,1.2745823 0 0 0 0.427536,1.7511077 l 6.6464147,4.0371761 a 1.2747098,1.2747098 0 0 0 0.661785,0.185206 z"
id="path9314" />
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

43
docs/static/img/priority-4.svg vendored Normal file
View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24px"
viewBox="0 0 24 24"
width="24px"
fill="#000000"
version="1.1"
id="svg1428"
sodipodi:docname="priority_4_24dp.svg"
inkscape:version="1.1.1 (3bf5ae0, 2021-09-20)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1432" />
<sodipodi:namedview
id="namedview1430"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="20.517358"
inkscape:cx="22.834324"
inkscape:cy="15.742768"
inkscape:window-width="1863"
inkscape:window-height="1025"
inkscape:window-x="57"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg1428" />
<path
style="color:#000000;fill:#c60000;fill-opacity:1;stroke:none;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
d="M 12.116784,6.5394415 A 1.2747098,1.2747098 0 0 0 11.455179,6.724648 l -6.6465926,4.037176 a 1.2745823,1.2745823 0 0 0 -0.427537,1.751108 1.2745823,1.2745823 0 0 0 1.7509281,0.427717 l 5.9848065,-3.635327 5.984809,3.635327 A 1.2745823,1.2745823 0 0 0 19.85252,12.512932 1.2745823,1.2745823 0 0 0 19.424984,10.761824 L 12.778569,6.724648 A 1.2747098,1.2747098 0 0 0 12.116784,6.5394415 Z"
id="path9314" />
<path
style="color:#000000;fill:#de0000;fill-opacity:1;stroke:none;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
d="m 12.195014,11.806679 a 1.2747098,1.2747098 0 0 0 -0.661606,0.185205 l -6.6465924,4.037177 a 1.2745823,1.2745823 0 0 0 -0.427537,1.751108 1.2745823,1.2745823 0 0 0 1.750928,0.427718 l 5.9848074,-3.635327 5.984807,3.635327 a 1.2745823,1.2745823 0 0 0 1.750928,-0.427718 1.2745823,1.2745823 0 0 0 -0.427537,-1.751108 l -6.646414,-4.037177 a 1.2747098,1.2747098 0 0 0 -0.661784,-0.185205 z"
id="path9316" />
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

47
docs/static/img/priority-5.svg vendored Normal file
View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24px"
viewBox="0 0 24 24"
width="24px"
fill="#000000"
version="1.1"
id="svg1428"
sodipodi:docname="priority_5_24dp.svg"
inkscape:version="1.1.1 (3bf5ae0, 2021-09-20)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1432" />
<sodipodi:namedview
id="namedview1430"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="20.517358"
inkscape:cx="22.834323"
inkscape:cy="15.742767"
inkscape:window-width="1863"
inkscape:window-height="1025"
inkscape:window-x="57"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg1428" />
<path
style="color:#000000;fill:#aa0000;fill-opacity:1;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
d="M 12.116784,3.40514 A 1.2747098,1.2747098 0 0 0 11.455179,3.5903463 L 4.8085864,7.6275238 A 1.2745823,1.2745823 0 0 0 4.3810494,9.3786313 1.2745823,1.2745823 0 0 0 6.1319775,9.8063489 L 12.116784,6.1710217 18.101593,9.8063489 A 1.2745823,1.2745823 0 0 0 19.85252,9.3786313 1.2745823,1.2745823 0 0 0 19.424984,7.6275238 L 12.778569,3.5903463 A 1.2747098,1.2747098 0 0 0 12.116784,3.40514 Z"
id="rect3554" />
<path
style="color:#000000;fill:#c60000;fill-opacity:1;stroke:none;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
d="M 12.116784,8.5394415 A 1.2747098,1.2747098 0 0 0 11.455179,8.724648 l -6.6465926,4.037176 a 1.2745823,1.2745823 0 0 0 -0.427537,1.751108 1.2745823,1.2745823 0 0 0 1.7509281,0.427717 l 5.9848065,-3.635327 5.984809,3.635327 A 1.2745823,1.2745823 0 0 0 19.85252,14.512932 1.2745823,1.2745823 0 0 0 19.424984,12.761824 L 12.778569,8.724648 A 1.2747098,1.2747098 0 0 0 12.116784,8.5394415 Z"
id="path9314" />
<path
style="color:#000000;fill:#de0000;fill-opacity:1;stroke:none;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
d="m 12.195014,13.806679 a 1.2747098,1.2747098 0 0 0 -0.661606,0.185205 l -6.6465924,4.037177 a 1.2745823,1.2745823 0 0 0 -0.427537,1.751108 1.2745823,1.2745823 0 0 0 1.750928,0.427718 l 5.9848074,-3.635327 5.984807,3.635327 a 1.2745823,1.2745823 0 0 0 1.750928,-0.427718 1.2745823,1.2745823 0 0 0 -0.427537,-1.751108 l -6.646414,-4.037177 a 1.2747098,1.2747098 0 0 0 -0.661784,-0.185205 z"
id="path9316" />
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

BIN
docs/static/img/screenshot-curl.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
docs/static/img/screenshot-email.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
docs/static/img/screenshot-phone-add.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Some files were not shown because too many files have changed in this diff Show More