Compare commits

...

346 Commits

Author SHA1 Message Date
binwiederhier
f5247c50f4 Bump 2025-07-07 21:24:43 +02:00
binwiederhier
1edbda4f31 Release notes 2025-07-07 18:34:05 +02:00
binwiederhier
de7b7218e4 Add languages 2025-07-07 18:28:16 +02:00
binwiederhier
19a4e95a3a Docs 2025-07-07 16:49:15 +02:00
binwiederhier
4578835a8f stdin 2025-07-07 11:04:33 +02:00
binwiederhier
aead619dea Merge branch 'main' of github.com:binwiederhier/ntfy into ipv6 2025-07-06 21:52:49 +02:00
Philipp C. Heckel
deeefee8c0 Merge pull request #1382 from srevn/main
Add piping support
2025-07-06 21:52:23 +02:00
Philipp C. Heckel
5e380e147f Merge pull request #1371 from cyb3rko/docs-update
Smaller docs updates
2025-07-06 21:48:52 +02:00
Philipp C. Heckel
ba5c3a164d Merge pull request #1381 from alecthomas/patch-1
docs: add ntfyexec to integrations
2025-07-06 21:48:18 +02:00
srevn
47da3aeea6 fix unbounded read 2025-07-06 17:53:04 +03:00
srevn
9ed96e5d8b Small cosmetic fixes 2025-07-06 16:31:03 +03:00
srevn
04aff72631 Add example and logging 2025-07-06 10:51:28 +03:00
srevn
6fbcd85d17 Add piping support 2025-07-06 10:23:32 +03:00
binwiederhier
8f60294c5b Docs 2025-07-05 22:48:45 +02:00
binwiederhier
677b44ce61 Docs, rename proxy-trusted-(addresses->hosts) 2025-07-05 22:35:26 +02:00
binwiederhier
000248e6aa Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web into ipv6 2025-07-05 21:46:44 +02:00
binwiederhier
359c789c34 Test for visitorID 2025-07-05 13:11:17 +02:00
Alec Thomas
34e9a771ce docs: add ntfyexec to integrations 2025-07-05 17:05:31 +10:00
binwiederhier
60b8588129 Tests 2025-07-04 16:56:35 +02:00
binwiederhier
7eeaeb8398 server.yml update 2025-07-04 16:51:55 +02:00
binwiederhier
c99d8b66c2 Re-order 2025-07-04 10:19:27 +02:00
binwiederhier
960f690dd6 Merge branch 'main' of github.com:binwiederhier/ntfy into ipv6 2025-07-04 10:17:05 +02:00
binwiederhier
54514454bf Works 2025-07-04 10:16:49 +02:00
binwiederhier
d8c8f31846 IPv6 WIP 2025-07-04 07:38:58 +02:00
Kachelkaiser
ae27c3a5ab Translated using Weblate (German)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2025-06-30 16:08:38 +02:00
Kachelkaiser
48cb816111 Translated using Weblate (German)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2025-06-30 16:08:38 +02:00
Carl Fritze
ff904a5ca6 Translated using Weblate (German)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2025-06-30 16:08:38 +02:00
Priit Jõerüüt
8e7de80353 Translated using Weblate (Estonian)
Currently translated at 67.1% (272 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/et/
2025-06-26 23:01:49 +02:00
huy.phan
9c8a8f8795 Translated using Weblate (Vietnamese)
Currently translated at 20.0% (81 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/vi/
2025-06-26 23:01:47 +02:00
Priit Jõerüüt
df73c6f655 Translated using Weblate (Estonian)
Currently translated at 52.5% (213 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/et/
2025-06-24 18:01:52 +02:00
Kachelkaiser
c1e657db8b Translated using Weblate (German)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2025-06-24 18:01:50 +02:00
Joan
62c8a13ed4 Translated using Weblate (Catalan)
Currently translated at 1.2% (5 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ca/
2025-06-21 13:01:46 +02:00
Joan
994266ab04 Added translation using Weblate (Catalan) 2025-06-20 12:07:37 +02:00
Niko Diamadis
a41e3a1e76 Update App Store badges and remove Docker compose versions 2025-06-20 00:45:42 +02:00
lazar
86bec660bf Translated using Weblate (Romanian)
Currently translated at 60.2% (244 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ro/
2025-06-08 15:50:23 +02:00
Philipp C. Heckel
30301c8a7f Update README.md 2025-06-07 06:49:22 -04:00
binwiederhier
7b470a7f6f Sponsors 2025-06-07 06:45:43 -04:00
Priit Jõerüüt
9d5891963a Translated using Weblate (Estonian)
Currently translated at 44.1% (179 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/et/
2025-06-03 22:04:33 +02:00
Philipp C. Heckel
de8e3bc2aa Merge pull request #1360 from binwiederhier/client-ip-header
Add custom client IP header
2025-06-01 10:12:34 -04:00
binwiederhier
d3f7aa7008 Self-review 2025-06-01 10:12:06 -04:00
binwiederhier
bbfaf2fc4d Add Forwarded header parsing 2025-06-01 09:57:39 -04:00
binwiederhier
db4ac158e3 Section 2025-05-31 23:09:51 -04:00
binwiederhier
7a33e16945 Cleanup, examples 2025-05-31 23:07:40 -04:00
binwiederhier
eac49feb04 Merge branch 'main' of github.com:binwiederhier/ntfy into client-ip-header 2025-05-31 22:42:40 -04:00
binwiederhier
849884c947 Change to "proxy-forwarded-header" and add "proxy-trusted-addrs" 2025-05-31 22:39:18 -04:00
binwiederhier
2cb4d089ab Merge client ip header 2025-05-31 15:33:21 -04:00
binwiederhier
dc797f8594 Fix release noteFix release notess 2025-05-29 20:35:22 -04:00
binwiederhier
061677a78b Bump version 2025-05-29 20:14:54 -04:00
binwiederhier
b4f15ec9d4 Integrations 2025-05-26 13:41:26 -04:00
binwiederhier
af17661053 Typos, server.yml additions 2025-05-25 20:13:13 -04:00
binwiederhier
635ec88c4f Update release log 2025-05-25 15:28:59 -04:00
Philipp C. Heckel
905f048ab4 Merge pull request #1123 from stendler/json-firebase-cache
feat(server): add Cache and Firebase as keys to JSON publishing
2025-05-25 15:24:51 -04:00
binwiederhier
7f86108379 Update docs 2025-05-25 12:57:02 -04:00
Philipp C. Heckel
425e6d064e Merge pull request #1002 from dandersch/ntfy-client_user_service
Add systemd user service for `ntfy-client.service`
2025-05-25 12:42:51 -04:00
Philipp C. Heckel
ebb61fcccf Merge pull request #1353 from binwiederhier/apns-fix
APNs fix
2025-05-25 12:33:53 -04:00
binwiederhier
9f72eb804d Computers are fast 2025-05-25 12:32:16 -04:00
binwiederhier
42af71e546 Fix test 2025-05-25 12:27:21 -04:00
binwiederhier
df818cfebc Merge branch 'main' of github.com:binwiederhier/ntfy 2025-05-25 12:23:20 -04:00
binwiederhier
0de1990c01 Increase number of access tokens per user to 60 2025-05-25 12:23:02 -04:00
binwiederhier
f40023aa23 APNs fix 2025-05-25 12:09:57 -04:00
Philipp C. Heckel
5765a707fc Merge pull request #1352 from leukosaima/patch-1
Add ntfyrr project to integrations
2025-05-25 06:43:38 -04:00
leukosaima
5eb84f759b Add ntfyrr project 2025-05-25 00:20:55 -04:00
binwiederhier
df7dd9c498 Fix weebpush test 2025-05-24 15:55:02 -04:00
binwiederhier
6fe3913aee Increase Web Push expiration to 55/60 days, update configs 2025-05-24 15:26:25 -04:00
Philipp C. Heckel
0ad9716241 Merge pull request #1212 from KuroSetsuna29/config-webpush-expiry
feat: allow configurable web push expiry duration
2025-05-24 14:57:50 -04:00
binwiederhier
f4c37ccfb9 Bump VIte 2025-05-24 14:22:02 -04:00
Philipp C. Heckel
7182d3a4e5 Merge pull request #1342 from binwiederhier/dependabot/npm_and_yarn/web/multi-4e779676ec
Bump esbuild, vite and vite-plugin-pwa in /web
2025-05-24 14:07:01 -04:00
binwiederhier
eecd3245f0 Release notes 2025-05-24 09:36:16 -04:00
binwiederhier
4dc3b38c95 Allow adding/changing user with password hash via v1/users API 2025-05-24 09:31:57 -04:00
binwiederhier
9edab24d4c Merge branch 'main' of github.com:binwiederhier/ntfy 2025-05-24 09:10:07 -04:00
Philipp C. Heckel
3b627b27b3 Merge pull request #1340 from Tom-Hubrecht/hashed-pwd
user: Allow changing the hashed password directly
2025-05-24 09:10:02 -04:00
binwiederhier
80462f7ee5 Refine user API change 2025-05-24 08:58:44 -04:00
Philipp C. Heckel
65e377ec63 Merge pull request #1267 from wunter8/api-change-user-password
Api change user password
2025-05-24 08:53:28 -04:00
dandersch
45e1707d3b remove systemd user daemon-reload from postinst.sh 2025-05-23 23:00:45 +02:00
dandersch
0581a9e680 remove systemd user service cmds from postinst.sh 2025-05-23 22:52:25 +02:00
Hunter Kehoe
0fb60ae72d test change user password and tier in single request 2025-05-22 20:01:50 -06:00
Hunter Kehoe
e36e4856c9 allow changing password or tier with user PUT 2025-05-22 19:57:57 -06:00
Hunter Kehoe
fa48639517 make POST create user and PUT update user 2025-05-22 19:57:02 -06:00
Hunter Kehoe
2b40ad9a12 make staticcheck happy 2025-05-22 19:57:02 -06:00
Hunter Kehoe
ad7ab18fb7 prevent changing admin passwords 2025-05-22 19:57:02 -06:00
Hunter Kehoe
8f9dafce20 change user password via accounts API 2025-05-22 19:57:00 -06:00
binwiederhier
69cf773834 Fix webpush command 2025-05-22 21:56:28 -04:00
binwiederhier
b2b9891a58 Add Tamil language 2025-05-22 21:43:45 -04:00
Andrea Toska
3bf02d3cd9 Translated using Weblate (Albanian)
Currently translated at 15.0% (61 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/sq/
2025-05-23 03:23:15 +02:00
Priit Jõerüüt
8777990d2d Translated using Weblate (Estonian)
Currently translated at 24.9% (101 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/et/
2025-05-23 03:23:15 +02:00
Tyxiel
70f0e7ccc7 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/pt_BR/
2025-05-23 03:23:15 +02:00
Max Badran
adfacf820e Translated using Weblate (Ukrainian)
Currently translated at 99.7% (404 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/uk/
2025-05-23 03:23:15 +02:00
András
35e15cfd9d Translated using Weblate (Hungarian)
Currently translated at 53.5% (217 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/hu/
2025-05-23 03:23:15 +02:00
Eero Häkkinen
4e2a884da5 Translated using Weblate (Finnish)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/fi/
2025-05-23 03:23:15 +02:00
Eero Häkkinen
29cf4f16d1 Translated using Weblate (Finnish)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/fi/
2025-05-23 03:23:15 +02:00
Łukasz Podgórski
609c9fa37d Translated using Weblate (Polish)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/pl/
2025-05-23 03:23:15 +02:00
Korab Arifi
2eb5eb3e29 Translated using Weblate (Albanian)
Currently translated at 10.1% (41 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/sq/
2025-05-23 03:23:15 +02:00
Korab Arifi
a92306b181 Added translation using Weblate (Albanian) 2025-05-23 03:23:15 +02:00
OZZY
047cc22dba Translated using Weblate (Arabic)
Currently translated at 88.3% (358 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ar/
2025-05-23 03:23:15 +02:00
Priit Jõerüüt
f31d777b69 Translated using Weblate (Estonian)
Currently translated at 10.6% (43 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/et/
2025-05-23 03:23:15 +02:00
தமிழ்நேரம்
ac983cd9bc Translated using Weblate (Tamil)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ta/
2025-05-23 03:23:15 +02:00
AtomicDude
dd45fd90b7 Translated using Weblate (Romanian)
Currently translated at 56.7% (230 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ro/
2025-05-23 03:23:15 +02:00
தமிழ்நேரம்
e76e6274a3 Added translation using Weblate (Tamil) 2025-05-23 03:23:15 +02:00
Marius Pop
161ce468fe Translated using Weblate (Romanian)
Currently translated at 46.6% (189 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ro/
2025-05-23 03:23:15 +02:00
Danial Behzadi
04df6f1390 Translated using Weblate (Persian)
Currently translated at 13.8% (56 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/fa/
2025-05-23 03:23:15 +02:00
Faraz Sadri Alamdari
79852fec59 Translated using Weblate (Persian)
Currently translated at 13.8% (56 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/fa/
2025-05-23 03:23:15 +02:00
Danial Behzadi
92de1b5a88 Translated using Weblate (Persian)
Currently translated at 13.8% (56 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/fa/
2025-05-23 03:23:15 +02:00
Ihor Kalnytskyi
fc93de9a28 Translated using Weblate (Ukrainian)
Currently translated at 99.7% (404 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/uk/
2025-05-23 03:23:15 +02:00
qtm
ae9fa85676 Translated using Weblate (Russian)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ru/
2025-05-23 03:23:15 +02:00
Christer Solstrand Johannessen
b26666f635 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/nb_NO/
2025-05-23 03:23:15 +02:00
Christer Solstrand Johannessen
70a9301e25 Translated using Weblate (Norwegian Bokmål)
Currently translated at 51.1% (207 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/nb_NO/
2025-05-23 03:23:15 +02:00
Cairo Braga
86c548ae37 Translated using Weblate (Portuguese)
Currently translated at 76.5% (310 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/pt/
2025-05-23 03:23:15 +02:00
Ed
1e1b2be464 Translated using Weblate (Portuguese)
Currently translated at 76.5% (310 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/pt/
2025-05-23 03:23:15 +02:00
Cairo Braga
1b8906f1fd Translated using Weblate (Portuguese (Brazil))
Currently translated at 84.1% (341 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/pt_BR/
2025-05-23 03:23:15 +02:00
Luis Eduardo Brito
b81f7b21a9 Translated using Weblate (Portuguese (Brazil))
Currently translated at 83.9% (340 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/pt_BR/
2025-05-23 03:23:15 +02:00
K0ntact
db2dc09189 Translated using Weblate (Vietnamese)
Currently translated at 7.1% (29 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/vi/
2025-05-23 03:23:14 +02:00
Shoshin Akamine
5f6b7e6f82 Translated using Weblate (Japanese)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ja/
2025-05-23 03:23:14 +02:00
109247019824
6daf4141c6 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/bg/
2025-05-23 03:23:14 +02:00
Vito0912
41083cfd07 Translated using Weblate (German)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2025-05-23 03:23:14 +02:00
Soaibuzzaman
c03f795508 Added translation using Weblate (Bengali) 2025-05-23 03:23:14 +02:00
Ricardo Vieira
58d7cb8ef8 Translated using Weblate (Portuguese)
Currently translated at 76.2% (309 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/pt/
2025-05-23 03:23:14 +02:00
Linerly
8acf0f4350 Translated using Weblate (Indonesian)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/id/
2025-05-23 03:23:14 +02:00
Carl Fritze
236b7b7a16 Translated using Weblate (German)
Currently translated at 99.5% (403 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2025-05-23 03:23:14 +02:00
Petri Hämäläinen
871883f6e9 Translated using Weblate (Finnish)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/fi/
2025-05-23 03:23:14 +02:00
Malte Saling
a92c8a9ec9 Translated using Weblate (German)
Currently translated at 95.5% (387 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2025-05-23 03:23:14 +02:00
Vito0912
1c6aa49fca Translated using Weblate (German)
Currently translated at 95.3% (386 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2025-05-23 03:23:14 +02:00
githubozaurus
49d258706d Translated using Weblate (Romanian)
Currently translated at 31.1% (126 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ro/
2025-05-23 03:23:14 +02:00
josé m
bbce1200b4 Translated using Weblate (Galician)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/gl/
2025-05-23 03:23:14 +02:00
Stefano Maggi
94d0c5a335 Translated using Weblate (Italian)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/it/
2025-05-23 03:23:14 +02:00
Jakob Malchow
7835fc65c4 Translated using Weblate (Italian)
Currently translated at 87.6% (355 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/it/
2025-05-23 03:23:14 +02:00
Stefano Maggi
dc6b8ece1e Translated using Weblate (Italian)
Currently translated at 87.6% (355 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/it/
2025-05-23 03:23:14 +02:00
josé m
f595dff66f Translated using Weblate (Galician)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/gl/
2025-05-23 03:23:14 +02:00
binwiederhier
0514ea4ac0 Merge branch 'main' of github.com:binwiederhier/ntfy 2025-05-22 20:59:29 -04:00
binwiederhier
1598087e1f Fix tests 2025-05-22 20:58:28 -04:00
Philipp C. Heckel
3709ea689a Merge pull request #1216 from wunter8/since-latest-param
feat: add since=latest subscribe param
2025-05-22 20:57:59 -04:00
Philipp C. Heckel
f4aba12546 Merge branch 'main' into since-latest-param 2025-05-22 20:57:52 -04:00
Philipp C. Heckel
521fe791b0 Merge pull request #1271 from RoboMagus/feat_1174
Add major and minor version tags to docker release flow
2025-05-22 20:51:00 -04:00
binwiederhier
6d15b9face Fix up APNs PR 2025-05-22 20:48:24 -04:00
Philipp C. Heckel
9fbe7804dd Merge pull request #1345 from tr4nt0r/homeassistant_official
Add official Home Assistant integration and async python library
2025-05-22 20:30:18 -04:00
Philipp C. Heckel
faa4dcbcee Merge pull request #1287 from barart/fix-anonymous-read-restriction
Handle anonymous read restrictions by sending a poll_request event
2025-05-22 20:22:58 -04:00
tr4nt0r
ad3e7960ce Add official Home Assistant integration and async python library 2025-05-23 01:31:16 +02:00
Philipp C. Heckel
3234189cd2 Merge pull request #1343 from ptmorris1/patch-2
Add NtfyPwsh integration and blog
2025-05-22 16:51:53 -04:00
Patrick Morris
e64a0bd8c9 Add NtfyPwsh integration and blog 2025-05-22 13:37:54 -05:00
Philipp C. Heckel
97a59f19e0 Merge pull request #1262 from rake5k/markdown-code-scroll
Make markdown code blocks scrollable
2025-05-21 20:57:54 -04:00
binwiederhier
7067d8aa77 Release notes 2025-05-21 20:55:54 -04:00
Philipp C. Heckel
5999653456 Merge pull request #1138 from nogweii/webpush-key-file
Teach ntfy webpush to write the keys to a file
2025-05-21 20:44:44 -04:00
Philipp C. Heckel
9ce6b03450 Merge branch 'main' into webpush-key-file 2025-05-21 20:39:12 -04:00
Philipp C. Heckel
7e916516e0 Merge pull request #1338 from wunter8/websockets-401
Websocket http error codes
2025-05-21 20:26:32 -04:00
Philipp C. Heckel
09c2b4bdca Merge pull request #1239 from thiswillbeyourgithub/integration_toc
docs: add ToC to integrations.md
2025-05-21 20:21:40 -04:00
Philipp C. Heckel
978ee81df3 Merge pull request #1199 from quantum5/ntfy-run
docs: add quantum5/ntfy-run to integrations and examples
2025-05-21 20:19:47 -04:00
Philipp C. Heckel
86f2ab8a55 Merge branch 'main' into ntfy-run 2025-05-21 20:19:39 -04:00
Philipp C. Heckel
e4aff00455 Merge pull request #1234 from ungeskriptet/fix-typo
docs: config.md: fix typo
2025-05-21 20:17:57 -04:00
Philipp C. Heckel
e88f24bae7 Merge pull request #1241 from thiswillbeyourgithub/integration_csv
docs: add integration: Ntfy_CSV_Reminders
2025-05-21 20:16:45 -04:00
Philipp C. Heckel
b4797ef212 Merge branch 'main' into integration_csv 2025-05-21 20:16:35 -04:00
Philipp C. Heckel
2f8c0e4d5d Merge pull request #1224 from dmitrygudkov/patch-1
Update integrations.md: Added EasyMorph
2025-05-21 20:14:07 -04:00
Philipp C. Heckel
56231f9288 Merge branch 'main' into patch-1 2025-05-21 20:13:56 -04:00
Philipp C. Heckel
ef7c7c7b09 Merge pull request #1209 from jim3692/patch-1
Add Clipboard IO to projects
2025-05-21 20:10:51 -04:00
Philipp C. Heckel
88e4b8f0e6 Merge branch 'main' into patch-1 2025-05-21 20:10:43 -04:00
Philipp C. Heckel
090bdd93ba Merge pull request #1187 from 13x1/patch-1
Fix typo
2025-05-21 20:09:18 -04:00
Philipp C. Heckel
790044e899 Merge pull request #1240 from thiswillbeyourgithub/integration_daily_fact
docs: add integration: Daily Fact Ntfy
2025-05-21 20:07:00 -04:00
dependabot[bot]
7aab7d387f Bump esbuild, vite and vite-plugin-pwa in /web
Bumps [esbuild](https://github.com/evanw/esbuild) to 0.25.4 and updates ancestor dependencies [esbuild](https://github.com/evanw/esbuild), [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) and [vite-plugin-pwa](https://github.com/vite-pwa/vite-plugin-pwa). These dependencies need to be updated together.


Updates `esbuild` from 0.18.20 to 0.25.4
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2023.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.18.20...v0.25.4)

Updates `vite` from 4.5.14 to 6.3.5
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.5/packages/vite)

Updates `vite-plugin-pwa` from 0.15.2 to 1.0.0
- [Release notes](https://github.com/vite-pwa/vite-plugin-pwa/releases)
- [Commits](https://github.com/vite-pwa/vite-plugin-pwa/compare/v0.15.2...v1.0.0)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.25.4
  dependency-type: indirect
- dependency-name: vite
  dependency-version: 6.3.5
  dependency-type: direct:development
- dependency-name: vite-plugin-pwa
  dependency-version: 1.0.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-22 00:04:33 +00:00
binwiederhier
a461aafb91 Merge branch 'main' of github.com:binwiederhier/ntfy 2025-05-21 20:02:19 -04:00
binwiederhier
1569c22a65 Fix some broken links in the docs 2025-05-21 20:02:06 -04:00
Philipp C. Heckel
2091ceb4d2 Merge pull request #1248 from pixitha/patch-1
Update quickstart example
2025-05-21 19:50:40 -04:00
Philipp C. Heckel
ec337b5de9 Merge pull request #1296 from sharjeelaziz/add-bashrc-example
Add Terminal Notifications for Long-Running Commands example
2025-05-21 19:33:54 -04:00
Philipp C. Heckel
5a245f889c Merge pull request #1260 from snex/main
add canary in the cage podcast coverage to integrations page
2025-05-21 19:31:27 -04:00
Philipp C. Heckel
ee595067ba Merge pull request #1307 from jlssmt/patch-1
set LABEL org.opencontainers.image.version
2025-05-21 19:24:43 -04:00
Philipp C. Heckel
bd4b5e9e1b Merge pull request #1312 from vkrause/work/vkrause/detect-encrypted-messages-as-unified-push
Consider aes128gcm content encoding as an indicator for UnifiedPush
2025-05-21 19:16:17 -04:00
Philipp C. Heckel
786e588397 Merge pull request #1325 from gitmotion/add-ntfy-me-mcp-integrations-projects
docs: add integration ntfy-me-mcp to integrations.md
2025-05-21 19:15:02 -04:00
Philipp C. Heckel
2dfb53ec53 Merge branch 'main' into add-ntfy-me-mcp-integrations-projects 2025-05-21 19:14:54 -04:00
Philipp C. Heckel
a30c5eb9cf Merge pull request #1310 from biodrone/biodrone/issue1309
docs: correct mountPath for server.yml
2025-05-21 19:13:51 -04:00
Philipp C. Heckel
d96d4b03c7 Merge pull request #1225 from sedlund/docs/fix_typo
docs: publish.md typo
2025-05-21 19:13:23 -04:00
Philipp C. Heckel
3257ce91ef Merge pull request #1264 from brian6932/patch-1
docs: Typo `wep` -> `web`
2025-05-21 19:12:44 -04:00
Philipp C. Heckel
f563b671c8 Merge pull request #1266 from mmatuska/fix/extractIPAddress
server/util.go: fix logic in extractIPAddress()
2025-05-21 19:11:13 -04:00
Philipp C. Heckel
ea1cda5f92 Merge pull request #1299 from cvilsmeier/main
docs: add integration: Monibot
2025-05-21 19:09:49 -04:00
Philipp C. Heckel
36ba27ba09 Merge pull request #1313 from patricksthannon/patch-1
Update integrations.md
2025-05-21 19:09:06 -04:00
Philipp C. Heckel
60eccba2fa Merge pull request #1320 from yassirh/main
docs: Added reference to UptimeObserver integration
2025-05-21 19:07:14 -04:00
Philipp C. Heckel
aec4b97fae Merge pull request #1319 from therobbielee/therobbielee-patch-1
Update integrations.md
2025-05-21 19:06:18 -04:00
Philipp C. Heckel
389ae682a5 Merge pull request #1341 from binwiederhier/security-updates
Security updates
2025-05-21 19:04:26 -04:00
binwiederhier
3f21da7768 Pipelines 2025-05-21 18:55:21 -04:00
binwiederhier
0ad266a495 Derp 2025-05-21 18:53:29 -04:00
binwiederhier
bd192edf1e Release notes 2025-05-21 18:52:45 -04:00
binwiederhier
d1ac8d03e0 Security updates 2025-05-21 18:49:19 -04:00
Tom Hubrecht
44b7c2f198 user: Allow changing the hashed password directly
This adds the detection of `NTFY_PASSWORD_HASH` when creating a user or
changing its passsword so that scripts don't have to manipulate the bare
password.
2025-05-21 16:34:14 +02:00
Hunter Kehoe
cdae5493e2 write http errors to websocket connection instead of always 200 2025-05-14 11:39:18 -06:00
Hunter Kehoe
f110472204 fix typo 2025-05-14 11:20:30 -06:00
gitmotion
3f1342c05b Add ntfy-me-mcp 2025-04-28 19:25:40 -07:00
Yassir Hannoun
8b95b1a213 docs: Added UptimeObserver integration 2025-04-26 21:13:12 +00:00
Robbie Björk
d4dfd3f657 Update integrations.md
Added alertmanager-ntfy-relay to integrations.md
2025-04-26 00:22:54 +02:00
patricksthannon
c1d718ee68 Update integrations.md
Added InvaderInformant integration to integration list. Thanks!
2025-04-09 09:53:17 -07:00
Volker Krause
bd08a120cd Consider aes128gcm content encoding as an indicator for UnifiedPush
Without this a UnifiedPush/Web Push message with encryption would be
turned into an attachment. That in itself isn't pretty but can still
work, but it requires attachments to be enabled in the first place.
2025-04-07 17:19:44 +02:00
Josh J
c9126e7aa9 docs: correct mountPath for server.yml
Fixed #1309
2025-04-07 09:26:52 +01:00
jlssmt
db9b974e47 set LABEL org.opencontainers.image.version 2025-04-06 11:35:42 +02:00
Christoph Vilsmeier
889a6f03f8 docs: add integration: Monibot 2025-03-25 16:56:47 +01:00
Sharjeel Aziz
6af8d03470 Add Terminal Notifications for Long-Running Commands example
Signed-off-by: Sharjeel Aziz <sharjeel.aziz@gmail.com>
2025-03-19 15:35:46 -04:00
barart
6b2cfb1d1d Handle anonymous read restrictions by sending a poll_request event
If a topic does not allow anonymous reads, this change ensures that we send a "poll_request" event instead of relaying the message via Firebase. Additionally, we include generic text in the title and body/message. This way, if the client cannot retrieve the actual message, the user will still receive a notification, prompting them to update the client manually.
2025-03-05 13:04:21 -06:00
RoboMagus
35458230a8 add major and minor version tags to docker release flow 2025-01-30 23:49:22 +01:00
Martin Matuska
bd39cf4b54 server/util.go: fix logic in extractIPAddress() 2025-01-26 00:00:06 +01:00
Brian
f739a3067e docs: Typo wep -> web 2025-01-24 20:28:29 -05:00
Christian Harke
2344eee2c6 Make markdown code blocks scrollable 2025-01-20 22:13:15 +01:00
David Havlicek
5822a2ec41 add canary in the cage podcast coverage to integrations page 2025-01-17 13:26:22 -08:00
Kyle Duren
a49cafbadb more correcting auto-formats 2025-01-06 02:55:03 +00:00
Kyle Duren
0aee6252bb fixing auto-format change 2025-01-06 01:09:40 +00:00
Kyle Duren
0e6a483b2f fixing auto-format change 2025-01-06 01:06:28 +00:00
Kyle Duren
20c014ba8d Adding test and some docs 2025-01-06 00:57:53 +00:00
Kyle Duren
926967b6e7 adding logic to specifcy client-ip header from proxy 2025-01-05 20:29:08 +00:00
Kyle Duren
6345e7f864 Update quickstart example
Just noticed the behind proxy was missing from the example that was supposed to include it.
2025-01-01 22:08:30 -05:00
thiswillbeyourgithub
80bc600ff0 docs: add integration: Ntfy_CSV_Reminders
Signed-off-by: thiswillbeyourgithub
<26625900+thiswillbeyourgithub@users.noreply.github.com>
2024-12-12 17:55:08 +01:00
thiswillbeyourgithub
758828e7aa docs: add integration: Daily Fact Ntfy
Signed-off-by: thiswillbeyourgithub <26625900+thiswillbeyourgithub@users.noreply.github.com>
2024-12-11 12:50:03 +01:00
thiswillbeyourgithub
4c179b7d9d docs: add ToC to integrations.md
Signed-off-by: thiswillbeyourgithub <26625900+thiswillbeyourgithub@users.noreply.github.com>
2024-12-11 12:40:36 +01:00
David Wronek
27398e7d72 docs: config.md: fix typo
Add a missing parenthesis.

Signed-off-by: David Wronek <david@mainlining.org>
2024-12-10 08:42:59 +01:00
Scott Edlund
19f8a35588 docs: publish.md typo 2024-11-23 13:24:24 +07:00
Dmitry Gudkov
8feb0f1a2e Update integrations.md: Added EasyMorph
EasyMorph (https://easymorph.com) is a visual workflow-based data preparation and automation tool. It has 180+ actions, including a dedicated action to send notifications to ntfy as a workflow step.

The proposed link leads to the official help page for the "Send message to ntfy" action.
2024-11-19 21:28:59 -05:00
Hunter Kehoe
9241b0550c feat: add subscribe param 2024-11-04 21:33:35 -07:00
KuroSetsuna29
136b656ccb fix descriptions 2024-11-02 08:50:57 -04:00
KuroSetsuna29
c844c24a16 allow configurable web push expiry duration 2024-11-02 00:46:31 -04:00
jim3692
90f21ba408 Add Clipboard IO to projects 2024-10-30 14:48:32 +02:00
Quantum
b843c69c16 docs: add quantum5/ntfy-run to integrations and examples 2024-10-16 22:00:48 -04:00
binwiederhier
630f2957de Merge branch 'main' of github.com:binwiederhier/ntfy 2024-09-29 13:20:45 -04:00
binwiederhier
d243c22510 Docs, fix lint 2024-09-29 13:20:36 -04:00
Philipp C. Heckel
fbf325a630 Merge pull request #1122 from stendler/docs
update links in integration docs
2024-09-29 13:19:21 -04:00
Philipp C. Heckel
84f421a464 Merge pull request #1175 from Measurity/patch-1
Added note to add ", chain=DOCKER-USER" to the fail2ban jail action if using docker networks
2024-09-29 13:14:26 -04:00
binwiederhier
d38c149263 Docs 2024-09-29 13:12:51 -04:00
Philipp C. Heckel
fc3624cd50 Merge pull request #1141 from pcouy/smtp-sender-date-header
Add Date header to sent e-mails
2024-09-29 12:41:29 -04:00
Philipp C. Heckel
78533e27fe Merge pull request #1133 from Walzen-Group/integrations-wlzntfy
Integrations wlzntfy
2024-09-29 12:40:46 -04:00
Philipp C. Heckel
02e46c1d03 Merge pull request #1137 from ShlomoCode/patch-1
macrodroid supports POST requests
2024-09-29 12:39:25 -04:00
Philipp C. Heckel
81f05b3f15 Merge pull request #1161 from OneWeekNotice/documentation-update
Updating Docs - configuration table, shoutrr, Scrutiny, etc
2024-09-29 12:38:07 -04:00
Philipp C. Heckel
eb700b4b6c Merge pull request #1164 from bishtawi/bishtawi/smtp-auth
Support SMTP Auth Plain for event publishing
2024-09-29 12:30:01 -04:00
Philipp C. Heckel
89c884ab4d Merge pull request #1172 from anirvan/patch-1
Fix typo in cli.md ("subscibe" → "subscribe")
2024-09-29 12:23:26 -04:00
Philipp C. Heckel
0fe0b0c9d7 Merge pull request #1177 from hoho4190/fix-config-md
docs(config.md): remove duplicate comment
2024-09-29 12:22:56 -04:00
binwiederhier
bad3ef43b7 BUmp 2024-09-29 12:22:16 -04:00
lexi
903ef71b6f Fix typo
"Firebase (FCM" -> "Firebase (FCM)"
2024-09-22 11:58:26 +02:00
hoho4190
5726f8e9ba docs(config.md): remove duplicate comment
removed links typed twice
2024-09-03 15:06:46 +09:00
Meas
5b10cd660b Update config.md
Added note to add ", chain=DOCKER-USER" to the fail2ban jail action if using docker networks

By default, the jail action chain is "INPUT", but "FORWARD" is used when using docker networks. "DOCKER-USER", available when using docker, is part of the "FORWARD" chain. Hence the note to use "DOCKER-USER".
2024-08-31 09:06:54 +02:00
Anirvan Chatterjee
333d901661 Fix typo in cli.md ("subscibe" → "subscribe") 2024-08-25 22:19:05 -07:00
Bishtawi
112efaae90 Support SMTP Auth Plain 2024-08-07 17:29:54 -07:00
OneWeekNotice
61bb8a0286 adding Scrutiny project to integrations documentation 2024-07-26 23:16:49 -04:00
OneWeekNotice
be2bebf517 adding logging configurations to Config options table 2024-07-26 23:01:35 -04:00
OneWeekNotice
a4cf40907b removing WATCHTOWER_NOTIFICATIONS as this is Legacy notifications 2024-07-26 22:50:27 -04:00
OneWeekNotice
6562ba6987 adding Watchtower - shoutrrr authentication with auth token utilizing ntfy url 2024-07-26 22:48:14 -04:00
OneWeekNotice
6da554d1e5 adding documentation for caddy WebSockets 2024-07-26 22:43:08 -04:00
Philipp C. Heckel
72f36f8296 Merge pull request #1149 from abhisheksoni27/patch-1
feat: add app store reference
2024-07-19 09:41:55 +02:00
binwiederhier
e8685baf15 Thank you @cdf-eagles for your generous donation 2024-07-19 09:34:14 +02:00
binwiederhier
f8085f8686 Thank you @user8446 for your donation 2024-07-19 09:33:45 +02:00
binwiederhier
3139c13e50 Thank you @chxseh for your donation 2024-07-19 09:33:18 +02:00
binwiederhier
4a2b5676d9 Thank you @wielandp for your donation 2024-07-19 09:32:47 +02:00
binwiederhier
a12195d3c7 Thank you @Proximus888 for your sponsorship 2024-07-19 09:32:03 +02:00
binwiederhier
412e78c4d0 Thank you @suhlig for your sponsorship 2024-07-19 09:31:02 +02:00
binwiederhier
22bdc91630 Thank you @dlt-green for your sponsorship 2024-07-19 09:29:46 +02:00
binwiederhier
e94c2fef52 Thank you @sheetd for your donation 2024-07-19 09:28:36 +02:00
binwiederhier
694363013d Thank you @TheCraiggers for your sponsorship 2024-07-19 09:28:03 +02:00
binwiederhier
fb6a408cca Thank you @avalentic for your donation 2024-07-19 09:26:55 +02:00
binwiederhier
89437019fb Bump 2024-07-19 09:26:44 +02:00
binwiederhier
a095ab56bb Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web 2024-07-19 09:24:49 +02:00
Jiri Grönroos
92905fd860 Translated using Weblate (Finnish)
Currently translated at 98.5% (399 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/fi/
2024-07-16 13:09:10 +00:00
Priit Jõerüüt
01c216d506 Translated using Weblate (Estonian)
Currently translated at 5.9% (24 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/et/
2024-07-15 08:09:25 +02:00
Priit Jõerüüt
999678565b Added translation using Weblate (Estonian) 2024-07-13 23:59:26 +02:00
Abhishek Soni
3454a5ca16 feat: add app store reference 2024-07-12 14:44:30 +05:30
wmbirken
63c96b4e80 Translated using Weblate (Swedish)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/sv/
2024-07-11 15:09:24 +02:00
Pierre Couy
003fec5f83 Add Date header to sent e-mails 2024-06-29 15:31:25 +02:00
Gian Andrea Casavecchia
f0d8f0ad8e Translated using Weblate (Italian)
Currently translated at 78.5% (318 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/it/
2024-06-26 10:09:27 +00:00
Nogweii
20cca8e888 update go.sum 2024-06-25 22:59:17 -07:00
Nogweii
49a548252c teach ntfy webpush to write the keys to a file 2024-06-25 22:58:36 -07:00
Shlomo
21dbcf65dc macrodroid supports POST requests 2024-06-24 23:50:56 +03:00
Sam
dee213d90c improve wording 2024-06-16 22:51:35 +02:00
Sam
19b99e8285 better wording 2024-06-16 22:50:08 +02:00
Sam
0c68b6a2c7 Update integrations.md 2024-06-16 22:47:41 +02:00
Sam
76b753062d fix url 2024-06-16 22:47:07 +02:00
Sam
ceec0bc71d Update integrations.md 2024-06-16 22:44:56 +02:00
aaron.frost@gmx.de
6ecd96cf6e Translated using Weblate (German)
Currently translated at 94.8% (384 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2024-06-14 15:09:15 +00:00
waclaw66
8d38672baf Translated using Weblate (Czech)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/cs/
2024-06-06 08:09:13 +02:00
Aiman Jalil
36a149dd7a Translated using Weblate (Malay)
Currently translated at 46.4% (188 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ms/
2024-06-04 05:58:08 +02:00
Aiman Jalil
1249d9473a Translated using Weblate (Malay)
Currently translated at 24.4% (99 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ms/
2024-06-02 22:09:21 +02:00
Ed
5941a8f2a6 Translated using Weblate (Portuguese)
Currently translated at 71.8% (291 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/pt/
2024-06-02 22:09:20 +02:00
Aiman Jalil
2e8daa962c Added translation using Weblate (Malay) 2024-06-02 12:24:59 +02:00
Ed
3f4d0ef3ea Translated using Weblate (Portuguese)
Currently translated at 59.0% (239 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/pt/
2024-06-01 21:21:59 +02:00
1024mb
0fba690d02 Translated using Weblate (Spanish)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/es/
2024-06-01 21:21:58 +02:00
stendler
5211d06f2c feat(server): add Cache and Firebase as keys to JSON publishing
https://github.com/binwiederhier/ntfy/issues/1119
2024-05-29 21:23:06 +02:00
stendler
7121d14bfa docs: add more links 2024-05-29 13:02:11 +02:00
stendler
d5a1e38082 docs: fix description of systemd-ntfy-poweronoff 2024-05-29 12:36:55 +02:00
Guilherme Puida
3ad61c4736 Translated using Weblate (Portuguese (Brazil))
Currently translated at 79.5% (322 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/pt_BR/
2024-05-28 05:09:33 +02:00
binwiederhier
9d3fc20e58 Thank you @madchr1st for your generous donation! 2024-05-18 12:19:33 -04:00
binwiederhier
0be467f809 Thank you @0x45796164 for your sponsorship 2024-05-13 20:46:37 -04:00
binwiederhier
ec75ce0787 Thank you @herzkerl for your sponsorship 2024-05-13 20:41:56 -04:00
binwiederhier
d11b1007ef Bump 2024-05-13 16:11:29 -04:00
binwiederhier
c542dd8c6f Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web 2024-05-13 16:09:04 -04:00
binwiederhier
37697aed27 Bump 2024-05-13 16:08:46 -04:00
bytequill
4360d157b2 Translated using Weblate (Polish)
Currently translated at 98.7% (400 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/pl/
2024-05-12 13:29:15 +02:00
Samuel
c3c4d65f99 Translated using Weblate (Portuguese (Brazil))
Currently translated at 78.2% (317 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/pt_BR/
2024-05-08 16:07:19 +02:00
binwiederhier
ffd7645c0b Releases 2024-05-07 21:24:39 -04:00
Philipp C. Heckel
043738a475 Merge pull request #1098 from wunter8/patch-6
don't cache config.js
2024-05-07 21:21:39 -04:00
binwiederhier
fb52ad6fdb Thank you @arnbrhm for your donation 2024-05-07 21:20:58 -04:00
binwiederhier
29318f9d61 Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web 2024-05-07 21:18:14 -04:00
binwiederhier
030f7266f7 Do not set rate visitor for non-eligible topics 2024-05-07 21:17:51 -04:00
ButterflyOfFire
9692de1469 Translated using Weblate (Arabic)
Currently translated at 88.1% (357 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ar/
2024-05-05 21:08:13 +02:00
Mohammad Habib
eab90a0275 Translated using Weblate (Arabic)
Currently translated at 85.9% (348 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ar/
2024-05-03 19:07:20 +02:00
Isaac Alves
e6f70f8e41 Translated using Weblate (Portuguese (Brazil))
Currently translated at 76.2% (309 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/pt_BR/
2024-04-26 21:07:29 +02:00
wunter8
499b0dd839 don't cache config.js 2024-04-25 16:09:27 -06:00
Philipp C. Heckel
31d0c812ce Merge pull request #1082 from wunter8/fix-username-regex
Fix username regex
2024-04-24 16:03:25 -04:00
Philipp C. Heckel
d37f861f6b Merge pull request #1086 from ibacalu/patch-1
docs(integrations): add proxmox-ntfy
2024-04-24 15:58:56 -04:00
binwiederhier
0a49a8d88b Thank you for your donation @Riolku 2024-04-24 14:55:51 -04:00
binwiederhier
b63ef0defb Thank you @rubund for your sponsorship 2024-04-24 14:55:18 -04:00
binwiederhier
f8068ef561 Thank you @IMarkoMC for your sponsorship 2024-04-24 14:54:52 -04:00
binwiederhier
2608687e98 Thank you @spudooli for your donation 2024-04-24 14:54:25 -04:00
binwiederhier
02564a40c7 Thank you @PTR-inc for your sponsorship 2024-04-24 14:53:45 -04:00
binwiederhier
bdd49f4e16 Thank you @afunworm for your sponsorship 2024-04-24 14:52:52 -04:00
binwiederhier
33b603def5 Re-add idx_topic to messages table 2024-04-24 14:38:05 -04:00
binwiederhier
6eff5553b5 Merge branch 'main' of github.com:binwiederhier/ntfy 2024-04-23 20:22:16 -04:00
binwiederhier
7cac03c1ec Bump 2024-04-23 20:22:09 -04:00
Iulian Bacalu
b33918f267 docs(integrations): add proxmox-ntfy
Python script that monitors Proxmox tasks and sends notifications using Ntfy.
More details [here](https://github.com/qtsone/proxmox-ntfy)
2024-04-14 12:24:55 +02:00
Jeroen Bulters
f68ad6acdf Translated using Weblate (Dutch)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/nl/
2024-04-12 11:02:04 +02:00
Alexey Smirnov
a533bf9efb Translated using Weblate (Russian)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ru/
2024-04-12 11:02:04 +02:00
Mohammad Parvin
66ea805cde Translated using Weblate (Persian)
Currently translated at 8.3% (34 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/fa/
2024-04-10 11:01:49 +02:00
Mohammad Parvin
7c3b6e4521 Added translation using Weblate (Persian) 2024-04-09 10:33:06 +02:00
Alexander Chekalin
9cb3d056fe Translated using Weblate (Russian)
Currently translated at 97.0% (393 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ru/
2024-04-08 12:02:14 +02:00
Hunter Kehoe
4111bee0c4 fix linting issue 2024-04-07 21:40:24 -06:00
Hunter Kehoe
e4c2b938d3 clean up invalid username code 2024-04-05 08:43:56 -06:00
Hunter Kehoe
fc7cf5933f fix error message for invalid username/password 2024-04-03 21:58:43 -06:00
Hunter Kehoe
e4d22ebd8b allow + in usernames 2024-04-03 21:58:29 -06:00
Fredrik
69d6e0f890 Translated using Weblate (Swedish)
Currently translated at 96.0% (389 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/sv/
2024-04-01 03:02:02 +02:00
grinningmosfet
ecab7fbf65 Translated using Weblate (French)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/fr/
2024-04-01 03:02:01 +02:00
Error504TimeOut
75887e4a62 Translated using Weblate (German)
Currently translated at 94.5% (383 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2024-04-01 03:02:00 +02:00
Philipp C. Heckel
130039f5c8 Merge pull request #1071 from truroshan/traccar-example-add-info
Update traccar examples
2024-03-27 12:21:24 -04:00
Roshan Rajak l byteio.ɪn
bec0d4807b Update traccar examples 2024-03-27 18:39:31 +05:30
binwiederhier
5ee62033b5 Bump 2024-03-25 17:21:23 -04:00
binwiederhier
3e02d7b0bb Bump 2024-03-25 17:20:27 -04:00
binwiederhier
290ed1124e Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web 2024-03-25 17:12:04 -04:00
binwiederhier
fc62682334 Thank you @jceloria for your donation 2024-03-25 09:42:09 -04:00
binwiederhier
28404565d2 Thank you @Circenn5130 for your donation 2024-03-25 09:41:37 -04:00
binwiederhier
f8548e9d46 Thank you @tomroth04 for your donation 2024-03-25 09:41:08 -04:00
Philipp C. Heckel
d90b290cd2 Merge pull request #1066 from binwiederhier/templating-disallow
Disallow certain templates
2024-03-25 09:38:33 -04:00
binwiederhier
21c6776269 Fix linter 2024-03-25 09:34:44 -04:00
binwiederhier
7fed392e0c Disallow certain templates 2024-03-24 23:19:16 -04:00
Philipp C. Heckel
913b59b5e3 Merge pull request #1064 from binwiederhier/templating-3
Message templating
2024-03-24 14:47:32 -04:00
binwiederhier
4692ca7b7f REvert parallel tests 2024-03-24 14:36:14 -04:00
binwiederhier
af16542d02 Removed unused vars 2024-03-24 14:28:10 -04:00
binwiederhier
5511812e30 JSON templating 2024-03-24 14:21:28 -04:00
binwiederhier
547b09a7e5 docs 2024-03-23 14:22:56 -04:00
binwiederhier
b9c176ddba Tests 2024-03-22 22:01:41 -04:00
Philipp C. Heckel
f971377cbb Merge pull request #1062 from theparadox1083/patch-1
Update publish.md (PowerShell 7+ Access Token Example)
2024-03-22 21:43:00 -04:00
binwiederhier
a04f2f9c9a Bla 2024-03-22 20:45:16 -04:00
Patrick
763eafd5dd Update publish.md (PowerShell 7+ Access Token Example)
Corrected PowerShell 7+ Parameter from Authorization to Authentication
Converted Token text to SecureString Inline - token must be of type SecureString
Added comment noting the Token parameter must be a SecureString
2024-03-21 09:38:55 -04:00
josé m
a4f5c8dee7 Translated using Weblate (Galician)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/gl/
2024-03-12 13:01:53 +01:00
Mazurky
0a589f6242 Translated using Weblate (Slovak)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/sk/
2024-03-10 17:01:44 +01:00
gallegonovato
ab2dd6136e Translated using Weblate (Spanish)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/es/
2024-03-10 17:01:44 +01:00
gallegonovato
4d64515e45 Translated using Weblate (Spanish)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/es/
2024-03-09 16:15:59 +01:00
LiLPandemio
411597ecc2 Translated using Weblate (Spanish)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/es/
2024-03-09 16:15:59 +01:00
Oğuz Ersen
1a426da913 Translated using Weblate (Turkish)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/tr/
2024-03-09 16:15:59 +01:00
Linerly
f4bf8fd9bb Translated using Weblate (Indonesian)
Currently translated at 99.7% (404 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/id/
2024-03-08 07:42:48 +01:00
109247019824
d866cb2fd9 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/bg/
2024-03-08 07:42:47 +01:00
da
b6983e6866 add ntfy-client.service as user service 2024-01-15 22:06:46 +01:00
109 changed files with 8259 additions and 3827 deletions

View File

@@ -9,7 +9,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: '1.22.x'
go-version: '1.24.x'
- name: Install node
uses: actions/setup-node@v3
with:

View File

@@ -12,7 +12,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: '1.22.x'
go-version: '1.24.x'
- name: Install node
uses: actions/setup-node@v3
with:

View File

@@ -9,7 +9,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: '1.22.x'
go-version: '1.24.x'
- name: Install node
uses: actions/setup-node@v3
with:

1
.gitignore vendored
View File

@@ -15,3 +15,4 @@ node_modules/
__pycache__
web/dev-dist/
venv/
cmd/key-file.yaml

View File

@@ -90,6 +90,8 @@ nfpms:
type: "config|noreplace"
- src: client/ntfy-client.service
dst: /lib/systemd/system/ntfy-client.service
- src: client/user/ntfy-client.service
dst: /lib/systemd/user/ntfy-client.service
- dst: /var/cache/ntfy
type: dir
- dst: /var/cache/ntfy/attachments
@@ -119,6 +121,7 @@ archives:
- server/ntfy.service
- client/client.yml
- client/ntfy-client.service
- client/user/ntfy-client.service
-
id: ntfy_windows
builds:
@@ -197,3 +200,15 @@ docker_manifests:
- *arm64v8_image
- *armv7_image
- *armv6_image
- name_template: "binwiederhier/ntfy:v{{ .Major }}"
image_templates:
- *amd64_image
- *arm64v8_image
- *armv7_image
- *armv6_image
- name_template: "binwiederhier/ntfy:v{{ .Major }}.{{ .Minor }}"
image_templates:
- *amd64_image
- *arm64v8_image
- *armv7_image
- *armv6_image

View File

@@ -1,4 +1,4 @@
FROM golang:1.22-bullseye as builder
FROM golang:1.24-bullseye as builder
ARG VERSION=dev
ARG COMMIT=unknown
@@ -44,6 +44,8 @@ RUN make VERSION=$VERSION COMMIT=$COMMIT cli-linux-server
FROM alpine
ARG VERSION=dev
LABEL org.opencontainers.image.authors="philipp.heckel@gmail.com"
LABEL org.opencontainers.image.url="https://ntfy.sh/"
LABEL org.opencontainers.image.documentation="https://docs.ntfy.sh/"
@@ -52,6 +54,7 @@ LABEL org.opencontainers.image.vendor="Philipp C. Heckel"
LABEL org.opencontainers.image.licenses="Apache-2.0, GPL-2.0"
LABEL org.opencontainers.image.title="ntfy"
LABEL org.opencontainers.image.description="Send push notifications to your phone or desktop using PUT/POST"
LABEL org.opencontainers.image.version="$VERSION"
COPY --from=builder /app/dist/ntfy_linux_server/ntfy /usr/bin/ntfy

View File

@@ -56,20 +56,18 @@ For announcements of new releases and cutting-edge beta versions, please subscri
topic. If you'd like to test the iOS app, join [TestFlight](https://testflight.apple.com/join/P1fFnAm9). For Android betas,
join Discord/Matrix (I'll eventually make a testing channel in Google Play).
## Contributing
I welcome any contributions. Just create a PR or an issue. For larger features/ideas, please reach out
on Discord/Matrix first to see if I'd accept them. 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>
## Sponsors
I have just very recently started accepting donations via [GitHub Sponsors](https://github.com/sponsors/binwiederhier),
and [Liberapay](https://liberapay.com/ntfy). I would be humbled if you helped me carry the server and developer
account costs. Even small donations are very much appreciated. A big fat **Thank You** to the folks who have sponsored ntfy in the past, or are still sponsoring ntfy:
If you'd like to support the ntfy maintainers, please consider donating to [GitHub Sponsors](https://github.com/sponsors/binwiederhier) or
and [Liberapay](https://liberapay.com/ntfy). We would be humbled if you helped carry the server and developer
account costs. Even small donations are very much appreciated.
Thank you to our commercial sponsors, who help keep the service running and the development going:
<a href="https://m.do.co/c/442b929528db"><img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="201px"></a>
<a href="https://www.magicbell.com/?utm_source=ntfy"><img src="assets/sponsors/magicbell.png" width="180px"></a>
And a big fat **Thank You** to the individuals who have sponsored ntfy in the past, or are still sponsoring ntfy:
<a href="https://github.com/neutralinsomniac"><img src="https://github.com/neutralinsomniac.png" width="40px" /></a>
<a href="https://github.com/aspyct"><img src="https://github.com/aspyct.png" width="40px" /></a>
@@ -186,14 +184,45 @@ account costs. Even small donations are very much appreciated. A big fat **Thank
<a href="https://github.com/stannynuytkens"><img src="https://github.com/stannynuytkens.png" width="40px" /></a>
<a href="https://github.com/danbartram"><img src="https://github.com/danbartram.png" width="40px" /></a>
<a href="https://github.com/arthurgleckler"><img src="https://github.com/arthurgleckler.png" width="40px" /></a>
<a href="https://github.com/tomroth04"><img src="https://github.com/tomroth04.png" width="40px" /></a>
<a href="https://github.com/Circenn5130"><img src="https://github.com/Circenn5130.png" width="40px" /></a>
<a href="https://github.com/jceloria"><img src="https://github.com/jceloria.png" width="40px" /></a>
<a href="https://github.com/afunworm"><img src="https://github.com/afunworm.png" width="40px" /></a>
<a href="https://github.com/PTR-inc"><img src="https://github.com/PTR-inc.png" width="40px" /></a>
<a href="https://github.com/spudooli"><img src="https://github.com/spudooli.png" width="40px" /></a>
<a href="https://github.com/IMarkoMC"><img src="https://github.com/IMarkoMC.png" width="40px" /></a>
<a href="https://github.com/rubund"><img src="https://github.com/rubund.png" width="40px" /></a>
<a href="https://github.com/Riolku"><img src="https://github.com/Riolku.png" width="40px" /></a>
<a href="https://github.com/arnbrhm"><img src="https://github.com/arnbrhm.png" width="40px" /></a>
<a href="https://github.com/herzkerl"><img src="https://github.com/herzkerl.png" width="40px" /></a>
<a href="https://github.com/0x45796164"><img src="https://github.com/0x45796164.png" width="40px" /></a>
<a href="https://github.com/madchr1st"><img src="https://github.com/madchr1st.png" width="40px" /></a>
<a href="https://github.com/avalentic"><img src="https://github.com/avalentic.png" width="40px" /></a>
<a href="https://github.com/TheCraiggers"><img src="https://github.com/TheCraiggers.png" width="40px" /></a>
<a href="https://github.com/sheetd"><img src="https://github.com/sheetd.png" width="40px" /></a>
<a href="https://github.com/dlt-green"><img src="https://github.com/dlt-green.png" width="40px" /></a>
<a href="https://github.com/suhlig"><img src="https://github.com/suhlig.png" width="40px" /></a>
<a href="https://github.com/Proximus888"><img src="https://github.com/Proximus888.png" width="40px" /></a>
<a href="https://github.com/wielandp"><img src="https://github.com/wielandp.png" width="40px" /></a>
<a href="https://github.com/chxseh"><img src="https://github.com/chxseh.png" width="40px" /></a>
<a href="https://github.com/user8446"><img src="https://github.com/user8446.png" width="40px" /></a>
<a href="https://github.com/cdf-eagles"><img src="https://github.com/cdf-eagles.png" width="40px" /></a>
I'd also like to thank JetBrains for their awesome [IntelliJ IDEA](https://www.jetbrains.com/idea/),
and [DigitalOcean](https://m.do.co/c/442b929528db) (*referral link*) for supporting the project:
## Contributing
I welcome any contributions. Just create a PR or an issue. For larger features/ideas, please reach out
on Discord/Matrix first to see if I'd accept them. 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://m.do.co/c/442b929528db"><img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="201px"></a>
<a href="https://hosted.weblate.org/engage/ntfy/">
<img src="https://hosted.weblate.org/widgets/ntfy/-/multi-blue.svg" alt="Translation status" />
</a>
## Code of Conduct
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for
everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity
and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste,
color, religion, or sexual identity and orientation.
**We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.**

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,10 @@
[Unit]
Description=ntfy client
After=network.target
[Service]
ExecStart=/usr/bin/ntfy subscribe --config "%h/.config/ntfy/client.yml" --from-config
Restart=on-failure
[Install]
WantedBy=default.target

View File

@@ -10,7 +10,6 @@ import (
)
func TestCLI_Access_Show(t *testing.T) {
t.Parallel()
s, conf, port := newTestServerWithAuth(t)
defer test.StopServer(t, s, port)
@@ -20,7 +19,6 @@ func TestCLI_Access_Show(t *testing.T) {
}
func TestCLI_Access_Grant_And_Publish(t *testing.T) {
t.Parallel()
s, conf, port := newTestServerWithAuth(t)
defer test.StopServer(t, s, port)

View File

@@ -8,7 +8,6 @@ import (
)
func TestNewYamlSourceFromFile(t *testing.T) {
t.Parallel()
filename := filepath.Join(t.TempDir(), "server.yml")
contents := `
# Normal options

View File

@@ -69,6 +69,7 @@ Examples:
ntfy pub --icon="http://some.tld/icon.png" 'Icon!' # Send notification with custom icon
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
echo 'message' | ntfy publish mytopic # Send message from stdin
ntfy pub -u phil:mypass secret Psst # Publish with username/password
ntfy pub --wait-pid 1234 mytopic # Wait for process 1234 to exit before publishing
ntfy pub --wait-cmd mytopic rsync -av ./ /tmp/a # Run command and publish after it completes
@@ -254,6 +255,15 @@ func parseTopicMessageCommand(c *cli.Context) (topic string, message string, com
if c.String("message") != "" {
message = c.String("message")
}
if message == "" && isStdinRedirected() {
var data []byte
data, err = io.ReadAll(io.LimitReader(c.App.Reader, 1024*1024))
if err != nil {
log.Debug("Failed to read from stdin: %s", err.Error())
return
}
message = strings.TrimSpace(string(data))
}
return
}
@@ -312,3 +322,12 @@ func runAndWaitForCommand(command []string) (message string, err error) {
log.Debug("Command succeeded after %s: %s", runtime, prettyCmd)
return fmt.Sprintf("Command succeeded after %s: %s", runtime, prettyCmd), nil
}
func isStdinRedirected() bool {
stat, err := os.Stdin.Stat()
if err != nil {
log.Debug("Failed to stat stdin: %s", err.Error())
return false
}
return (stat.Mode() & os.ModeCharDevice) == 0
}

View File

@@ -17,7 +17,6 @@ import (
)
func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) {
t.Parallel()
testMessage := util.RandomString(10)
app, _, _, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "publish", "ntfytest", "ntfy unit test " + testMessage}))
@@ -36,7 +35,6 @@ func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) {
}
func TestCLI_Publish_Subscribe_Poll(t *testing.T) {
t.Parallel()
s, port := test.StartServer(t)
defer test.StopServer(t, s, port)
topic := fmt.Sprintf("http://127.0.0.1:%d/mytopic", port)
@@ -53,7 +51,6 @@ func TestCLI_Publish_Subscribe_Poll(t *testing.T) {
}
func TestCLI_Publish_All_The_Things(t *testing.T) {
t.Parallel()
s, port := test.StartServer(t)
defer test.StopServer(t, s, port)
topic := fmt.Sprintf("http://127.0.0.1:%d/mytopic", port)

View File

@@ -5,13 +5,6 @@ package cmd
import (
"errors"
"fmt"
"github.com/stripe/stripe-go/v74"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/server"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util"
"io/fs"
"math"
"net"
@@ -22,6 +15,14 @@ import (
"strings"
"syscall"
"time"
"github.com/stripe/stripe-go/v74"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/server"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util"
)
func init() {
@@ -79,6 +80,7 @@ var flagsServe = append(
altsrc.NewStringFlag(&cli.StringFlag{Name: "message-delay-limit", Aliases: []string{"message_delay_limit"}, EnvVars: []string{"NTFY_MESSAGE_DELAY_LIMIT"}, Value: util.FormatDuration(server.DefaultMessageDelayMax), Usage: "max duration a message can be scheduled into the future"}),
altsrc.NewIntFlag(&cli.IntFlag{Name: "global-topic-limit", Aliases: []string{"global_topic_limit", "T"}, EnvVars: []string{"NTFY_GLOBAL_TOPIC_LIMIT"}, Value: server.DefaultTotalTopicLimit, Usage: "total number of topics allowed"}),
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-subscription-limit", Aliases: []string{"visitor_subscription_limit"}, EnvVars: []string{"NTFY_VISITOR_SUBSCRIPTION_LIMIT"}, Value: server.DefaultVisitorSubscriptionLimit, Usage: "number of subscriptions per visitor"}),
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "visitor-subscriber-rate-limiting", Aliases: []string{"visitor_subscriber_rate_limiting"}, EnvVars: []string{"NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING"}, Value: false, Usage: "enables subscriber-based rate limiting"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-attachment-total-size-limit", Aliases: []string{"visitor_attachment_total_size_limit"}, EnvVars: []string{"NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultVisitorAttachmentTotalSizeLimit), Usage: "total storage limit used for attachments per visitor"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-attachment-daily-bandwidth-limit", Aliases: []string{"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", Aliases: []string{"visitor_request_limit_burst"}, EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_BURST"}, Value: server.DefaultVisitorRequestLimitBurst, Usage: "initial limit of requests per visitor"}),
@@ -87,8 +89,11 @@ var flagsServe = append(
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-message-daily-limit", Aliases: []string{"visitor_message_daily_limit"}, EnvVars: []string{"NTFY_VISITOR_MESSAGE_DAILY_LIMIT"}, Value: server.DefaultVisitorMessageDailyLimit, Usage: "max messages per visitor per day, derived from request limit if unset"}),
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-email-limit-burst", Aliases: []string{"visitor_email_limit_burst"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_BURST"}, Value: server.DefaultVisitorEmailLimitBurst, Usage: "initial limit of e-mails per visitor"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-email-limit-replenish", Aliases: []string{"visitor_email_limit_replenish"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_REPLENISH"}, Value: util.FormatDuration(server.DefaultVisitorEmailLimitReplenish), Usage: "interval at which burst limit is replenished (one per x)"}),
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "visitor-subscriber-rate-limiting", Aliases: []string{"visitor_subscriber_rate_limiting"}, EnvVars: []string{"NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING"}, Value: false, Usage: "enables subscriber-based rate limiting"}),
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "behind-proxy", Aliases: []string{"behind_proxy", "P"}, EnvVars: []string{"NTFY_BEHIND_PROXY"}, Value: false, Usage: "if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting)"}),
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-prefix-bits-ipv4", Aliases: []string{"visitor_prefix_bits_ipv4"}, EnvVars: []string{"NTFY_VISITOR_PREFIX_BITS_IPV4"}, Value: server.DefaultVisitorPrefixBitsIPv4, Usage: "number of bits of the IPv4 address to use for rate limiting (default: 32, full address)"}),
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-prefix-bits-ipv6", Aliases: []string{"visitor_prefix_bits_ipv6"}, EnvVars: []string{"NTFY_VISITOR_PREFIX_BITS_IPV6"}, Value: server.DefaultVisitorPrefixBitsIPv6, Usage: "number of bits of the IPv6 address to use for rate limiting (default: 64, /64 subnet)"}),
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "behind-proxy", Aliases: []string{"behind_proxy", "P"}, EnvVars: []string{"NTFY_BEHIND_PROXY"}, Value: false, Usage: "if set, use forwarded header (e.g. X-Forwarded-For, X-Client-IP) to determine visitor IP address (for rate limiting)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "proxy-forwarded-header", Aliases: []string{"proxy_forwarded_header"}, EnvVars: []string{"NTFY_PROXY_FORWARDED_HEADER"}, Value: "X-Forwarded-For", Usage: "use specified header to determine visitor IP address (for rate limiting)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "proxy-trusted-hosts", Aliases: []string{"proxy_trusted_hosts"}, EnvVars: []string{"NTFY_PROXY_TRUSTED_HOSTS"}, Value: "", Usage: "comma-separated list of trusted IP addresses, hosts, or CIDRs to remove from forwarded header"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "stripe-secret-key", Aliases: []string{"stripe_secret_key"}, EnvVars: []string{"NTFY_STRIPE_SECRET_KEY"}, Value: "", Usage: "key used for the Stripe API communication, this enables payments"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "stripe-webhook-key", Aliases: []string{"stripe_webhook_key"}, EnvVars: []string{"NTFY_STRIPE_WEBHOOK_KEY"}, Value: "", Usage: "key required to validate the authenticity of incoming webhooks from Stripe"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "billing-contact", Aliases: []string{"billing_contact"}, EnvVars: []string{"NTFY_BILLING_CONTACT"}, Value: "", Usage: "e-mail or website to display in upgrade dialog (only if payments are enabled)"}),
@@ -100,6 +105,8 @@ var flagsServe = append(
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-file", Aliases: []string{"web_push_file"}, EnvVars: []string{"NTFY_WEB_PUSH_FILE"}, Usage: "file used to store web push subscriptions"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-email-address", Aliases: []string{"web_push_email_address"}, EnvVars: []string{"NTFY_WEB_PUSH_EMAIL_ADDRESS"}, Usage: "e-mail address of sender, required to use browser push services"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-startup-queries", Aliases: []string{"web_push_startup_queries"}, EnvVars: []string{"NTFY_WEB_PUSH_STARTUP_QUERIES"}, Usage: "queries run when the web push database is initialized"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-expiry-duration", Aliases: []string{"web_push_expiry_duration"}, EnvVars: []string{"NTFY_WEB_PUSH_EXPIRY_DURATION"}, Value: util.FormatDuration(server.DefaultWebPushExpiryDuration), Usage: "automatically expire unused subscriptions after this time"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-expiry-warning-duration", Aliases: []string{"web_push_expiry_warning_duration"}, EnvVars: []string{"NTFY_WEB_PUSH_EXPIRY_WARNING_DURATION"}, Value: util.FormatDuration(server.DefaultWebPushExpiryWarningDuration), Usage: "send web push warning notification after this time before expiring unused subscriptions"}),
)
var cmdServe = &cli.Command{
@@ -140,6 +147,8 @@ func execServe(c *cli.Context) error {
webPushFile := c.String("web-push-file")
webPushEmailAddress := c.String("web-push-email-address")
webPushStartupQueries := c.String("web-push-startup-queries")
webPushExpiryDurationStr := c.String("web-push-expiry-duration")
webPushExpiryWarningDurationStr := c.String("web-push-expiry-warning-duration")
cacheFile := c.String("cache-file")
cacheDurationStr := c.String("cache-duration")
cacheStartupQueries := c.String("cache-startup-queries")
@@ -185,7 +194,11 @@ func execServe(c *cli.Context) error {
visitorMessageDailyLimit := c.Int("visitor-message-daily-limit")
visitorEmailLimitBurst := c.Int("visitor-email-limit-burst")
visitorEmailLimitReplenishStr := c.String("visitor-email-limit-replenish")
visitorPrefixBitsIPv4 := c.Int("visitor-prefix-bits-ipv4")
visitorPrefixBitsIPv6 := c.Int("visitor-prefix-bits-ipv6")
behindProxy := c.Bool("behind-proxy")
proxyForwardedHeader := c.String("proxy-forwarded-header")
proxyTrustedHosts := util.SplitNoEmpty(c.String("proxy-trusted-hosts"), ",")
stripeSecretKey := c.String("stripe-secret-key")
stripeWebhookKey := c.String("stripe-webhook-key")
billingContact := c.String("billing-contact")
@@ -226,6 +239,14 @@ func execServe(c *cli.Context) error {
if err != nil {
return fmt.Errorf("invalid visitor email limit replenish: %s", visitorEmailLimitReplenishStr)
}
webPushExpiryDuration, err := util.ParseDuration(webPushExpiryDurationStr)
if err != nil {
return fmt.Errorf("invalid web push expiry duration: %s", webPushExpiryDurationStr)
}
webPushExpiryWarningDuration, err := util.ParseDuration(webPushExpiryWarningDurationStr)
if err != nil {
return fmt.Errorf("invalid web push expiry warning duration: %s", webPushExpiryWarningDurationStr)
}
// Convert sizes to bytes
messageSizeLimit, err := util.ParseSize(messageSizeLimitStr)
@@ -304,6 +325,14 @@ func execServe(c *cli.Context) error {
if messageSizeLimit > 5*1024*1024 {
return errors.New("message-size-limit cannot be higher than 5M")
}
} else if webPushExpiryWarningDuration > 0 && webPushExpiryWarningDuration > webPushExpiryDuration {
return errors.New("web push expiry warning duration cannot be higher than web push expiry duration")
} else if behindProxy && proxyForwardedHeader == "" {
return errors.New("if behind-proxy is set, proxy-forwarded-header must also be set")
} else if visitorPrefixBitsIPv4 < 1 || visitorPrefixBitsIPv4 > 32 {
return errors.New("visitor-prefix-bits-ipv4 must be between 1 and 32")
} else if visitorPrefixBitsIPv6 < 1 || visitorPrefixBitsIPv6 > 128 {
return errors.New("visitor-prefix-bits-ipv6 must be between 1 and 128")
}
// Backwards compatibility
@@ -329,14 +358,24 @@ func execServe(c *cli.Context) error {
}
// Resolve hosts
visitorRequestLimitExemptIPs := make([]netip.Prefix, 0)
visitorRequestLimitExemptPrefixes := make([]netip.Prefix, 0)
for _, host := range visitorRequestLimitExemptHosts {
ips, err := parseIPHostPrefix(host)
prefixes, err := parseIPHostPrefix(host)
if err != nil {
log.Warn("cannot resolve host %s: %s, ignoring visitor request exemption", host, err.Error())
continue
}
visitorRequestLimitExemptIPs = append(visitorRequestLimitExemptIPs, ips...)
visitorRequestLimitExemptPrefixes = append(visitorRequestLimitExemptPrefixes, prefixes...)
}
// Parse trusted prefixes
trustedProxyPrefixes := make([]netip.Prefix, 0)
for _, host := range proxyTrustedHosts {
prefixes, err := parseIPHostPrefix(host)
if err != nil {
return fmt.Errorf("cannot resolve trusted proxy host %s: %s", host, err.Error())
}
trustedProxyPrefixes = append(trustedProxyPrefixes, prefixes...)
}
// Stripe things
@@ -392,16 +431,20 @@ func execServe(c *cli.Context) error {
conf.MessageDelayMax = messageDelayLimit
conf.TotalTopicLimit = totalTopicLimit
conf.VisitorSubscriptionLimit = visitorSubscriptionLimit
conf.VisitorSubscriberRateLimiting = visitorSubscriberRateLimiting
conf.VisitorAttachmentTotalSizeLimit = visitorAttachmentTotalSizeLimit
conf.VisitorAttachmentDailyBandwidthLimit = visitorAttachmentDailyBandwidthLimit
conf.VisitorRequestLimitBurst = visitorRequestLimitBurst
conf.VisitorRequestLimitReplenish = visitorRequestLimitReplenish
conf.VisitorRequestExemptIPAddrs = visitorRequestLimitExemptIPs
conf.VisitorRequestExemptPrefixes = visitorRequestLimitExemptPrefixes
conf.VisitorMessageDailyLimit = visitorMessageDailyLimit
conf.VisitorEmailLimitBurst = visitorEmailLimitBurst
conf.VisitorEmailLimitReplenish = visitorEmailLimitReplenish
conf.VisitorSubscriberRateLimiting = visitorSubscriberRateLimiting
conf.VisitorPrefixBitsIPv4 = visitorPrefixBitsIPv4
conf.VisitorPrefixBitsIPv6 = visitorPrefixBitsIPv6
conf.BehindProxy = behindProxy
conf.ProxyForwardedHeader = proxyForwardedHeader
conf.ProxyTrustedPrefixes = trustedProxyPrefixes
conf.StripeSecretKey = stripeSecretKey
conf.StripeWebhookKey = stripeWebhookKey
conf.BillingContact = billingContact
@@ -411,12 +454,14 @@ func execServe(c *cli.Context) error {
conf.EnableMetrics = enableMetrics
conf.MetricsListenHTTP = metricsListenHTTP
conf.ProfileListenHTTP = profileListenHTTP
conf.Version = c.App.Version
conf.WebPushPrivateKey = webPushPrivateKey
conf.WebPushPublicKey = webPushPublicKey
conf.WebPushFile = webPushFile
conf.WebPushEmailAddress = webPushEmailAddress
conf.WebPushStartupQueries = webPushStartupQueries
conf.WebPushExpiryDuration = webPushExpiryDuration
conf.WebPushExpiryWarningDuration = webPushExpiryWarningDuration
conf.Version = c.App.Version
// Set up hot-reloading of config
go sigHandlerConfigReload(config)
@@ -424,9 +469,9 @@ func execServe(c *cli.Context) error {
// Run server
s, err := server.New(conf)
if err != nil {
log.Fatal(err.Error())
log.Fatal("%s", err.Error())
} else if err := s.Run(); err != nil {
log.Fatal(err.Error())
log.Fatal("%s", err.Error())
}
log.Info("Exiting.")
return nil
@@ -449,7 +494,7 @@ func sigHandlerConfigReload(config string) {
}
func parseIPHostPrefix(host string) (prefixes []netip.Prefix, err error) {
// Try parsing as prefix, e.g. 10.0.1.0/24
// Try parsing as prefix, e.g. 10.0.1.0/24 or 2001:db8::/32
prefix, err := netip.ParsePrefix(host)
if err == nil {
prefixes = append(prefixes, prefix.Masked())

View File

@@ -42,7 +42,7 @@ var cmdUser = &cli.Command{
Name: "add",
Aliases: []string{"a"},
Usage: "Adds a new user",
UsageText: "ntfy user add [--role=admin|user] USERNAME\nNTFY_PASSWORD=... ntfy user add [--role=admin|user] USERNAME",
UsageText: "ntfy user add [--role=admin|user] USERNAME\nNTFY_PASSWORD=... ntfy user add [--role=admin|user] USERNAME\nNTFY_PASSWORD_HASH=... ntfy user add [--role=admin|user] USERNAME",
Action: execUserAdd,
Flags: []cli.Flag{
&cli.StringFlag{Name: "role", Aliases: []string{"r"}, Value: string(user.RoleUser), Usage: "user role"},
@@ -55,12 +55,13 @@ granted otherwise by the auth-default-access setting). An admin user has read an
topics.
Examples:
ntfy user add phil # Add regular user phil
ntfy user add --role=admin phil # Add admin user phil
NTFY_PASSWORD=... ntfy user add phil # Add user, using env variable to set password (for scripts)
ntfy user add phil # Add regular user phil
ntfy user add --role=admin phil # Add admin user phil
NTFY_PASSWORD=... ntfy user add phil # Add user, using env variable to set password (for scripts)
NTFY_PASSWORD_HASH=... ntfy user add phil # Add user, using env variable to set password hash (for scripts)
You may set the NTFY_PASSWORD environment variable to pass the password. This is useful if
you are creating users via scripts.
You may set the NTFY_PASSWORD environment variable to pass the password, or NTFY_PASSWORD_HASH to pass
directly the bcrypt hash. This is useful if you are creating users via scripts.
`,
},
{
@@ -79,7 +80,7 @@ Example:
Name: "change-pass",
Aliases: []string{"chp"},
Usage: "Changes a user's password",
UsageText: "ntfy user change-pass USERNAME\nNTFY_PASSWORD=... ntfy user change-pass USERNAME",
UsageText: "ntfy user change-pass USERNAME\nNTFY_PASSWORD=... ntfy user change-pass USERNAME\nNTFY_PASSWORD_HASH=... ntfy user change-pass USERNAME",
Action: execUserChangePass,
Description: `Change the password for the given user.
@@ -89,9 +90,10 @@ it twice.
Example:
ntfy user change-pass phil
NTFY_PASSWORD=.. ntfy user change-pass phil
NTFY_PASSWORD_HASH=.. ntfy user change-pass phil
You may set the NTFY_PASSWORD environment variable to pass the new password. This is
useful if you are updating users via scripts.
You may set the NTFY_PASSWORD environment variable to pass the new password or NTFY_PASSWORD_HASH to pass
directly the bcrypt hash. This is useful if you are updating users via scripts.
`,
},
@@ -174,7 +176,12 @@ variable to pass the new password. This is useful if you are creating/updating u
func execUserAdd(c *cli.Context) error {
username := c.Args().Get(0)
role := user.Role(c.String("role"))
password := os.Getenv("NTFY_PASSWORD")
password, hashed := os.LookupEnv("NTFY_PASSWORD_HASH")
if !hashed {
password = os.Getenv("NTFY_PASSWORD")
}
if username == "" {
return errors.New("username expected, type 'ntfy user add --help' for help")
} else if username == userEveryone || username == user.Everyone {
@@ -200,7 +207,7 @@ func execUserAdd(c *cli.Context) error {
}
password = p
}
if err := manager.AddUser(username, password, role); err != nil {
if err := manager.AddUser(username, password, role, hashed); err != nil {
return err
}
fmt.Fprintf(c.App.ErrWriter, "user %s added with role %s\n", username, role)
@@ -230,7 +237,11 @@ func execUserDel(c *cli.Context) error {
func execUserChangePass(c *cli.Context) error {
username := c.Args().Get(0)
password := os.Getenv("NTFY_PASSWORD")
password, hashed := os.LookupEnv("NTFY_PASSWORD_HASH")
if !hashed {
password = os.Getenv("NTFY_PASSWORD")
}
if username == "" {
return errors.New("username expected, type 'ntfy user change-pass --help' for help")
} else if username == userEveryone || username == user.Everyone {
@@ -249,7 +260,7 @@ func execUserChangePass(c *cli.Context) error {
return err
}
}
if err := manager.ChangePassword(username, password); err != nil {
if err := manager.ChangePassword(username, password, hashed); err != nil {
return err
}
fmt.Fprintf(c.App.ErrWriter, "changed password for user %s\n", username)

View File

@@ -4,9 +4,16 @@ package cmd
import (
"fmt"
"os"
"github.com/SherClockHolmes/webpush-go"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
)
var flagsWebPush = append(
[]cli.Flag{},
altsrc.NewStringFlag(&cli.StringFlag{Name: "output-file", Aliases: []string{"f"}, Usage: "write VAPID keys to this file"}),
)
func init() {
@@ -26,6 +33,7 @@ var cmdWebPush = &cli.Command{
Usage: "Generate VAPID keys to enable browser background push notifications",
UsageText: "ntfy webpush keys",
Category: categoryServer,
Flags: flagsWebPush,
},
},
}
@@ -35,7 +43,19 @@ func generateWebPushKeys(c *cli.Context) error {
if err != nil {
return err
}
_, err = fmt.Fprintf(c.App.ErrWriter, `Web Push keys generated. Add the following lines to your config file:
if outputFile := c.String("output-file"); outputFile != "" {
contents := fmt.Sprintf(`---
web-push-public-key: %s
web-push-private-key: %s
`, publicKey, privateKey)
err = os.WriteFile(outputFile, []byte(contents), 0660)
if err != nil {
return err
}
_, err = fmt.Fprintf(c.App.ErrWriter, "Web Push keys written to %s.\n", outputFile)
} else {
_, err = fmt.Fprintf(c.App.ErrWriter, `Web Push keys generated. Add the following lines to your config file:
web-push-public-key: %s
web-push-private-key: %s
@@ -44,5 +64,6 @@ web-push-email-address: <email address>
See https://ntfy.sh/docs/config/#web-push for details.
`, publicKey, privateKey)
}
return err
}

View File

@@ -14,6 +14,13 @@ func TestCLI_WebPush_GenerateKeys(t *testing.T) {
require.Contains(t, stderr.String(), "Web Push keys generated.")
}
func TestCLI_WebPush_WriteKeysToFile(t *testing.T) {
app, _, _, stderr := newTestApp()
require.Nil(t, runWebPushCommand(app, server.NewConfig(), "keys", "--output-file=key-file.yaml"))
require.Contains(t, stderr.String(), "Web Push keys written to key-file.yaml")
require.FileExists(t, "key-file.yaml")
}
func runWebPushCommand(app *cli.App, conf *server.Config, args ...string) error {
webPushArgs := []string{
"ntfy",

View File

@@ -1,4 +1,3 @@
version: "2.1"
services:
ntfy:
image: binwiederhier/ntfy
@@ -14,4 +13,3 @@ services:
ports:
- 80:80
restart: unless-stopped

View File

@@ -18,8 +18,8 @@ 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.
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.
You may also want to look at how ntfy.sh is configured in the [ntfy-ansible](https://github.com/binwiederhier/ntfy-ansible) repository.
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.
@@ -50,6 +50,7 @@ Here are a few working sample configs using a `/etc/ntfy/server.yml` file:
listen-http: ":2586"
cache-file: "/var/cache/ntfy/cache.db"
attachment-cache-dir: "/var/cache/ntfy/attachments"
behind-proxy: true
```
=== "server.yml (ntfy.sh config)"
@@ -78,7 +79,6 @@ using Docker Compose (i.e. `docker-compose.yml`):
=== "Docker Compose (w/ auth, cache, attachments)"
``` yaml
version: '3'
services:
ntfy:
image: binwiederhier/ntfy
@@ -100,7 +100,6 @@ using Docker Compose (i.e. `docker-compose.yml`):
=== "Docker Compose (w/ auth, cache, web push, iOS)"
``` yaml
version: '3'
services:
ntfy:
image: binwiederhier/ntfy
@@ -294,7 +293,7 @@ want to use a dedicated token to publish from your backup host, and one from you
but not yet implemented.
The `ntfy token` command can be used to manage access tokens for users. Tokens can have labels, and they can expire
automatically (or never expire). Each user can have up to 20 tokens (hardcoded).
automatically (or never expire). Each user can have up to 60 tokens (hardcoded).
**Example commands** (type `ntfy token --help` or `ntfy token COMMAND --help` for more details):
```
@@ -552,17 +551,91 @@ It may be desirable to run ntfy behind a proxy (e.g. nginx, HAproxy or Apache),
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.
### IP-based rate limiting
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.
[rate limiting](#rate-limiting) logic to use the header configured in `proxy-forwarded-header` (default is `X-Forwarded-For`)
as the primary identifier for a visitor, as opposed to the remote IP address.
=== "/etc/ntfy/server.yml"
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.
Relevant flags to consider:
* `behind-proxy` makes it so that the real visitor IP address is extracted from the header defined in `proxy-forwarded-header`.
Without this, the remote address of the incoming connection is used (default: `false`).
* `proxy-forwarded-header` is the header to use to identify visitors (default: `X-Forwarded-For`). It may be a single IP address (e.g. `1.2.3.4`),
a comma-separated list of IP addresses (e.g. `1.2.3.4, 5.6.7.8`), or an [RFC 7239](https://datatracker.ietf.org/doc/html/rfc7239)-style
header (e.g. `for=1.2.3.4;by=proxy.example.com, for=5.6.7.8`).
* `proxy-trusted-hosts` is a comma-separated list of IP addresses, hosts or CIDRs that are removed from the forwarded header
to determine the real IP address. This is only useful if there are multiple proxies involved that add themselves to
the forwarded header (default: empty).
* `visitor-prefix-bits-ipv4` is the number of bits of the IPv4 address to use for rate limiting (default is `32`, which is the entire
IP address). In IPv4 environments, by default, a visitor's **full IPv4 address** is used as-is for rate limiting. This means that
if someone publishes messages from multiple IP addresses, they will be counted as separate visitors. You can adjust this by setting the `visitor-prefix-bits-ipv4` config option. To group visitors in a /24 subnet and count them as one, for instance,
set it to `24`. In that case, `1.2.3.4` and `1.2.3.99` are treated as the same visitor.
* `visitor-prefix-bits-ipv6` is the number of bits of the IPv6 address to use for rate limiting (default is `64`, which is a /64 subnet).
In IPv6 environments, by default, a visitor's IP address is **truncated to the /64 subnet**, meaning that `2001:db8:25:86:1::1` and
`2001:db8:25:86:2::1` are treated as the same visitor. Use the `visitor-prefix-bits-ipv6` config option to adjust this behavior.
See [IPv6 considerations](#ipv6-considerations) for more details.
=== "/etc/ntfy/server.yml (behind a proxy)"
``` yaml
# Tell ntfy to use "X-Forwarded-For" to identify visitors
# Tell ntfy to use "X-Forwarded-For" header to identify visitors for rate limiting
#
# Example: If "X-Forwarded-For: 9.9.9.9, 1.2.3.4" is set,
# the visitor IP will be 1.2.3.4 (right-most address).
#
behind-proxy: true
```
=== "/etc/ntfy/server.yml (X-Client-IP header)"
``` yaml
# Tell ntfy to use "X-Client-IP" header to identify visitors for rate limiting
#
# Example: If "X-Client-IP: 9.9.9.9" is set,
# the visitor IP will be 9.9.9.9.
#
behind-proxy: true
proxy-forwarded-header: "X-Client-IP"
```
=== "/etc/ntfy/server.yml (Forwarded header)"
``` yaml
# Tell ntfy to use "Forwarded" header (RFC 7239) to identify visitors for rate limiting
#
# Example: If "Forwarded: for=1.2.3.4;by=proxy.example.com, for=9.9.9.9" is set,
# the visitor IP will be 9.9.9.9.
#
behind-proxy: true
proxy-forwarded-header: "Forwarded"
```
=== "/etc/ntfy/server.yml (multiple proxies)"
``` yaml
# Tell ntfy to use "X-Forwarded-For" header to identify visitors for rate limiting,
# and to strip the IP addresses of the proxies 1.2.3.4 and 1.2.3.5
#
# Example: If "X-Forwarded-For: 9.9.9.9, 1.2.3.4" is set,
# the visitor IP will be 9.9.9.9 (right-most unknown address).
#
behind-proxy: true
proxy-trusted-hosts: "1.2.3.0/24, 1.2.2.2, 2001:db8::/64"
```
=== "/etc/ntfy/server.yml (adjusted IPv4/IPv6 prefixes proxies)"
``` yaml
# Tell ntfy to treat visitors as being in a /24 subnet (IPv4) or /48 subnet (IPv6)
# as one visitor, so that they are counted as one for rate limiting.
#
# Example 1: If 1.2.3.4 and 1.2.3.5 publish a message, the visitor 1.2.3.0 will have
# used 2 messages.
# Example 2: If 2001:db8:2500:1::1 and 2001:db8:2500:2::1 publish a message, the visitor
# 2001:db8:2500:: will have used 2 messages.
#
visitor-prefix-bits-ipv4: 24
visitor-prefix-bits-ipv6: 48
```
### 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).
@@ -631,7 +704,7 @@ or the root domain:
listen 443 ssl http2;
server_name ntfy.sh;
# See https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6see https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6
# See https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
ssl_session_tickets off;
@@ -698,7 +771,7 @@ or the root domain:
listen 443 ssl http2;
server_name ntfy.sh;
# See https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6see https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6
# See https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
ssl_session_tickets off;
@@ -777,6 +850,7 @@ or the root domain:
```
# 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.
# Note: Caddy automatically handles both HTTP and WebSockets with reverse_proxy
ntfy.sh, http://nfty.sh {
reverse_proxy 127.0.0.1:2586
@@ -864,7 +938,7 @@ it'll show `New message` as a popup.
## Web Push
[Web Push](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) ([RFC8030](https://datatracker.ietf.org/doc/html/rfc8030))
allows ntfy to receive push notifications, even when the ntfy web app (or even the browser, depending on the platform) is closed.
When enabled, the user can enable **background notifications** for their topics in the wep app under Settings. Once enabled by the
When enabled, the user can enable **background notifications** for their topics in the web app under Settings. Once enabled by the
user, ntfy will forward published messages to the push endpoint (browser-provided, e.g. fcm.googleapis.com), which will then
forward it to the browser.
@@ -875,7 +949,9 @@ a database to keep track of the browser's subscriptions, and an admin email addr
- `web-push-private-key` is the generated VAPID private key, e.g. AA2BB1234567890abcdefzxcvbnm1234567890
- `web-push-file` is a database file to keep track of browser subscription endpoints, e.g. `/var/cache/ntfy/webpush.db`
- `web-push-email-address` is the admin email address send to the push provider, e.g. `sysadmin@example.com`
- `web-push-startup-queries` is an optional list of queries to run on startup`
- `web-push-startup-queries` is an optional list of queries to run on startup`
- `web-push-expiry-warning-duration` defines the duration after which unused subscriptions are sent a warning (default is `55d`)
- `web-push-expiry-duration` defines the duration after which unused subscriptions will expire (default is `60d`)
Limitations:
@@ -902,8 +978,8 @@ web-push-file: /var/cache/ntfy/webpush.db
web-push-email-address: sysadmin@example.com
```
The `web-push-file` is used to store the push subscriptions. Unused subscriptions will send out a warning after 7 days,
and will automatically expire after 9 days (not configurable). If the gateway returns an error (e.g. 410 Gone when a user has unsubscribed),
The `web-push-file` is used to store the push subscriptions. Unused subscriptions will send out a warning after 55 days,
and will automatically expire after 60 days (default). If the gateway returns an error (e.g. 410 Gone when a user has unsubscribed),
subscriptions are also removed automatically.
The web app refreshes subscriptions on start and regularly on an interval, but this file should be persisted across restarts. If the subscription
@@ -1082,6 +1158,18 @@ If this ever happens, there will be a log message that looks something like this
WARN Firebase quota exceeded (likely for topic), temporarily denying Firebase access to visitor
```
### IPv6 considerations
By default, rate limiting for IPv6 is done using the `/64` subnet of the visitor's IPv6 address. This means that all visitors
in the same `/64` subnet are treated as one visitor. This is done to prevent abuse, as IPv6 subnet assignments are typically
much larger than IPv4 subnets (and much cheaper), and it is common for ISPs to assign large subnets to their customers.
Other than that, rate limiting for IPv6 is done the same way as for IPv4, using the visitor's IP address or subnet to identify them.
There are two options to configure the number of bits used for rate limiting (for IPv4 and IPv6):
- `visitor-prefix-bits-ipv4` is number of bits of the IPv4 address to use for rate limiting (default: 32, full address)
- `visitor-prefix-bits-ipv6` is number of bits of the IPv6 address to use for rate limiting (default: 64, /64 subnet)
### Subscriber-based rate limiting
By default, ntfy puts almost all rate limits on the message publisher, e.g. number of messages, requests, and attachment
size are all based on the visitor who publishes a message. **Subscriber-based rate limiting is a way to use the rate limits
@@ -1242,6 +1330,29 @@ and [here](https://easyengine.io/tutorials/nginx/block-wp-login-php-bruteforce-a
maxretry = 10
```
Note that if you run nginx in a container, append `, chain=DOCKER-USER` to the jail.local action. By default, the jail action chain
is `INPUT`, but `FORWARD` is used when using docker networks. `DOCKER-USER`, available when using docker, is part of the `FORWARD`
chain.
The official ntfy.sh server uses fail2ban to ban IPs. Check out ntfy.sh's [Ansible fail2ban role](https://github.com/binwiederhier/ntfy-ansible/tree/main/roles/fail2ban) for details. Ban actors are banned for 1 hour initially, and up to
4 hours at a time for repeated offenses. IPv4 addresses are banned individually, while IPv6 addresses are banned by their `/56` prefix.
## IPv6 support
ntfy fully supports IPv6, though there are a few things to keep in mind.
- **Listening on an IPv6 address**: By default, ntfy listens on `:80` (IPv4-only). If you want to listen on an IPv6 address, you need to
explicitly set the `listen-http` and/or `listen-https` options in your `server.yml` file to an IPv6 address, e.g. `[::]:80`. To listen on
IPv4 and IPv6, you must run ntfy behind a reverse proxy, e.g. `listen :80; listen [::]:80;` in nginx.
- **Rate limiting:** By default, ntfy uses the `/64` subnet of the visitor's IPv6 address for rate limiting. This means that all visitors in the same `/64`
subnet are treated as one visitor. If you want to change this, you can set the `visitor-prefix-bits-ipv6` option in your `server.yml` file to a different
value (e.g. `48` for `/48` subnets). See [IPv6 considerations](#ipv6-considerations) and [IP-based rate limiting](#ip-based-rate-limiting) for more details.
- **Banning IPs with fail2ban:** By default, if you're using the `iptables-multiport` action, fail2ban bans individual IPv4 and IPv6 addresses via `iptables` and `ip6tables`. While this behavior is fine for IPv4, it is not for IPv6, because every host can technically have up to 2^64 addresses. Please ensure that your `actionban` and `actionunban` commands
support IPv6 and also ban the entire prefix (e.g. `/48`). See [Banning bad actors](#banning-bad-actors-fail2ban) for details.
!!! info
The official ntfy.sh server supports IPv6. Check out ntfy.sh's [Ansible repository](https://github.com/binwiederhier/ntfy-ansible) for examples of how to
configure [ntfy](https://github.com/binwiederhier/ntfy-ansible/tree/main/roles/ntfy), [nginx](https://github.com/binwiederhier/ntfy-ansible/tree/main/roles/nginx) and [fail2ban](https://github.com/binwiederhier/ntfy-ansible/tree/main/roles/fail2ban).
## Health checks
A preliminary health check API endpoint is exposed at `/v1/health`. The endpoint returns a `json` response in the format shown below.
If a non-200 HTTP status code is returned or if the returned `healthy` field is `false` the ntfy service should be considered as unhealthy.
@@ -1374,15 +1485,17 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
| `listen-unix-mode` | `NTFY_LISTEN_UNIX_MODE` | *file mode* | *system default* | File mode of the Unix socket, e.g. 0700 or 0777 |
| `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). |
| `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. |
| `cache-startup-queries` | `NTFY_CACHE_STARTUP_QUERIES` | *string (SQL queries)* | - | SQL queries to run during database startup; this is useful for tuning and [enabling WAL mode](#wal-for-message-cache) |
| `cache-startup-queries` | `NTFY_CACHE_STARTUP_QUERIES` | *string (SQL queries)* | - | SQL queries to run during database startup; this is useful for tuning and [enabling WAL mode](#message-cache) |
| `cache-batch-size` | `NTFY_CACHE_BATCH_SIZE` | *int* | 0 | Max size of messages to batch together when writing to message cache (if zero, writes are synchronous) |
| `cache-batch-timeout` | `NTFY_CACHE_BATCH_TIMEOUT` | *duration* | 0s | Timeout for batched async writes to the message cache (if zero, writes are synchronous) |
| `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. |
| `behind-proxy` | `NTFY_BEHIND_PROXY` | *bool* | false | If set, use forwarded header (e.g. X-Forwarded-For, X-Client-IP) to determine visitor IP address (for rate limiting) |
| `proxy-forwarded-header` | `NTFY_PROXY_FORWARDED_HEADER` | *string* | `X-Forwarded-For` | Use specified header to determine visitor IP address (for rate limiting) |
| `proxy-trusted-hosts` | `NTFY_PROXY_TRUSTED_HOSTS` | *comma-separated host/IP/CIDR list* | - | Comma-separated list of trusted IP addresses, hosts, or CIDRs to remove from forwarded header |
| `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. |
@@ -1412,9 +1525,11 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
| `visitor-message-daily-limit` | `NTFY_VISITOR_MESSAGE_DAILY_LIMIT` | *number* | - | Rate limiting: Allowed number of messages per day per visitor, reset every day at midnight (UTC). By default, this value is unset. |
| `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-request-limit-exempt-hosts` | `NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS` | *comma-separated host/IP/CIDR list* | - | Rate limiting: List of hostnames and IPs to be exempt from request rate limiting |
| `visitor-subscription-limit` | `NTFY_VISITOR_SUBSCRIPTION_LIMIT` | *number* | 30 | Rate limiting: Number of subscriptions per visitor (IP address) |
| `visitor-subscriber-rate-limiting` | `NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING` | *bool* | `false` | Rate limiting: Enables subscriber-based rate limiting |
| `visitor-prefix-bits-ipv4` | `NTFY_VISITOR_PREFIX_BITS_IPV4` | *number* | 32 | Rate limiting: Number of bits to use for IPv4 visitor prefix, e.g. 24 for /24 |
| `visitor-prefix-bits-ipv6` | `NTFY_VISITOR_PREFIX_BITS_IPV6` | *number* | 64 | Rate limiting: Number of bits to use for IPv6 visitor prefix, e.g. 48 for /48 |
| `web-root` | `NTFY_WEB_ROOT` | *path*, e.g. `/` or `/app`, or `disable` | `/` | Sets root of the web app (e.g. /, or /app), or disables it entirely (disable) |
| `enable-signup` | `NTFY_ENABLE_SIGNUP` | *boolean* (`true` or `false`) | `false` | Allows users to sign up via the web app, or API |
| `enable-login` | `NTFY_ENABLE_LOGIN` | *boolean* (`true` or `false`) | `false` | Allows users to log in via the web app, or API |
@@ -1427,6 +1542,11 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
| `web-push-file` | `NTFY_WEB_PUSH_FILE` | *string* | - | Web Push: Database file that stores subscriptions |
| `web-push-email-address` | `NTFY_WEB_PUSH_EMAIL_ADDRESS` | *string* | - | Web Push: Sender email address |
| `web-push-startup-queries` | `NTFY_WEB_PUSH_STARTUP_QUERIES` | *string* | - | Web Push: SQL queries to run against subscription database at startup |
| `web-push-expiry-duration` | `NTFY_WEB_PUSH_EXPIRY_DURATION` | *duration* | 60d | Web Push: Duration after which a subscription is considered stale and will be deleted. This is to prevent stale subscriptions. |
| `web-push-expiry-warning-duration` | `NTFY_WEB_PUSH_EXPIRY_WARNING_DURATION` | *duration* | 55d | Web Push: Duration after which a warning is sent to subscribers that their subscription will expire soon. This is to prevent stale subscriptions. |
| `log-format` | `NTFY_LOG_FORMAT` | *string* | `text` | Defines the output format, can be text or json |
| `log-file` | `NTFY_LOG_FILE` | *string* | - | Defines the filename to write logs to. If this is not set, ntfy logs to stderr |
| `log-level` | `NTFY_LOG_LEVEL` | *string* | `info` | Defines the default log level, can be one of trace, debug, info, warn or error |
The format for a *duration* is: `<number>(smhd)`, e.g. 30s, 20m, 1h or 3d.
The format for a *size* is: `<number>(GMK)`, e.g. 1G, 200M or 4000k.
@@ -1505,6 +1625,7 @@ OPTIONS:
--message-delay-limit value, --message_delay_limit value max duration a message can be scheduled into the future (default: "3d") [$NTFY_MESSAGE_DELAY_LIMIT]
--global-topic-limit value, --global_topic_limit value, -T value total number of topics allowed (default: 15000) [$NTFY_GLOBAL_TOPIC_LIMIT]
--visitor-subscription-limit value, --visitor_subscription_limit value number of subscriptions per visitor (default: 30) [$NTFY_VISITOR_SUBSCRIPTION_LIMIT]
--visitor-subscriber-rate-limiting, --visitor_subscriber_rate_limiting enables subscriber-based rate limiting (default: false) [$NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING]
--visitor-attachment-total-size-limit value, --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, --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, --visitor_request_limit_burst value initial limit of requests per visitor (default: 60) [$NTFY_VISITOR_REQUEST_LIMIT_BURST]
@@ -1513,8 +1634,11 @@ OPTIONS:
--visitor-message-daily-limit value, --visitor_message_daily_limit value max messages per visitor per day, derived from request limit if unset (default: 0) [$NTFY_VISITOR_MESSAGE_DAILY_LIMIT]
--visitor-email-limit-burst value, --visitor_email_limit_burst value initial limit of e-mails per visitor (default: 16) [$NTFY_VISITOR_EMAIL_LIMIT_BURST]
--visitor-email-limit-replenish value, --visitor_email_limit_replenish value interval at which burst limit is replenished (one per x) (default: "1h") [$NTFY_VISITOR_EMAIL_LIMIT_REPLENISH]
--visitor-subscriber-rate-limiting, --visitor_subscriber_rate_limiting enables subscriber-based rate limiting (default: false) [$NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING]
--behind-proxy, --behind_proxy, -P if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting) (default: false) [$NTFY_BEHIND_PROXY]
--visitor-prefix-bits-ipv4 value, --visitor_prefix_bits_ipv4 value number of bits of the IPv4 address to use for rate limiting (default: 32, full address) (default: 32) [$NTFY_VISITOR_PREFIX_BITS_IPV4]
--visitor-prefix-bits-ipv6 value, --visitor_prefix_bits_ipv6 value number of bits of the IPv6 address to use for rate limiting (default: 64, /64 subnet) (default: 64) [$NTFY_VISITOR_PREFIX_BITS_IPV6]
--behind-proxy, --behind_proxy, -P if set, use forwarded header (e.g. X-Forwarded-For, X-Client-IP) to determine visitor IP address (for rate limiting) (default: false) [$NTFY_BEHIND_PROXY]
--proxy-forwarded-header value, --proxy_forwarded_header value use specified header to determine visitor IP address (for rate limiting) (default: "X-Forwarded-For") [$NTFY_PROXY_FORWARDED_HEADER]
--proxy-trusted-hosts value, --proxy_trusted_hosts value comma-separated list of trusted IP addresses, hosts, or CIDRs to remove from forwarded header [$NTFY_PROXY_TRUSTED_HOSTS]
--stripe-secret-key value, --stripe_secret_key value key used for the Stripe API communication, this enables payments [$NTFY_STRIPE_SECRET_KEY]
--stripe-webhook-key value, --stripe_webhook_key value key required to validate the authenticity of incoming webhooks from Stripe [$NTFY_STRIPE_WEBHOOK_KEY]
--billing-contact value, --billing_contact value e-mail or website to display in upgrade dialog (only if payments are enabled) [$NTFY_BILLING_CONTACT]
@@ -1526,5 +1650,7 @@ OPTIONS:
--web-push-file value, --web_push_file value file used to store web push subscriptions [$NTFY_WEB_PUSH_FILE]
--web-push-email-address value, --web_push_email_address value e-mail address of sender, required to use browser push services [$NTFY_WEB_PUSH_EMAIL_ADDRESS]
--web-push-startup-queries value, --web_push_startup_queries value queries run when the web push database is initialized [$NTFY_WEB_PUSH_STARTUP_QUERIES]
--help, -h show help
--web-push-expiry-duration value, --web_push_expiry_duration value automatically expire unused subscriptions after this time (default: "60d") [$NTFY_WEB_PUSH_EXPIRY_DURATION]
--web-push-expiry-warning-duration value, --web_push_expiry_warning_duration value send web push warning notification after this time before expiring unused subscriptions (default: "55d") [$NTFY_WEB_PUSH_EXPIRY_WARNING_DURATION]
--help, -h
```

View File

@@ -384,7 +384,7 @@ strictly based off of my development on this app. There may be other versions of
### Apple setup
!!! info
Along with this step, the [PLIST Deployment](#plist-deployment-and-configuration) step is also required
Along with this step, the [PLIST Deployment](#plist-config) step is also required
for these changes to take effect in the iOS app.
1. [Create a new key in Apple Developer Member Center](https://developer.apple.com/account/resources/authkeys/add)

View File

@@ -31,6 +31,12 @@ GitHub have been hopeless. In case it ever becomes available, I want to know imm
*/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
```
You can also use [`ntfy-run`](https://github.com/quantum5/ntfy-run) to send the output of your cronjob in the
notification, so that you know exactly why it failed:
```
0 0 * * * ntfy-run -n https://ntfy.sh/backups --success-priority low --failure-tags warning ~/backup-computer
```
## 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
@@ -161,7 +167,6 @@ services:
watchtower:
image: containrrr/watchtower
environment:
- WATCHTOWER_NOTIFICATIONS=shoutrrr
- WATCHTOWER_NOTIFICATION_SKIP_TITLE=True
- WATCHTOWER_NOTIFICATION_URL=ntfy://ntfy.sh/my_watchtower_topic?title=WatchtowerUpdates
```
@@ -173,7 +178,14 @@ Or, if you only want to send notifications using shoutrrr:
shoutrrr send -u "ntfy://ntfy.sh/my_watchtower_topic?title=WatchtowerUpdates" -m "testMessage"
```
Authentication tokens are also supported via the generic webhook and authorization header using this url format (replace the domain, topic and token with your own):
Authentication tokens are also supported:
- (Recommended) Ntfy url format (replace the domain, topic and token with your own):
```
ntfy://:TOKEN@DOMAIN/TOPIC
```
- Generic webhook and authorization header using this url format (replace the domain, topic and token with your own):
```
generic+https://DOMAIN/TOPIC?@authorization=Bearer+TOKEN`
@@ -607,6 +619,8 @@ This will only work on selfhosted [traccar](https://www.traccar.org/) ([Github](
The easiest way to integrate traccar with ntfy, is to configure ntfy as the SMS provider for your instance. You then can set your ntfy topic as your account's phone number in traccar. Sending the email notifications to ntfy will not work, as ntfy does not support HTML emails.
**Info:** Add a phone number to your traccar account not in device, as otherwise it will not try to send SMS.
**Caution:** JSON publishing is only possible, when POST-ing to the root URL of the ntfy instance. (see [documentation](publish.md#publish-as-json))
```xml
<entry key='sms.http.url'>https://ntfy.sh</entry>
@@ -626,3 +640,56 @@ or by simply providing traccar with a valid username/password combination.
<entry key='sms.http.user'>phil</entry>
<entry key='sms.http.password'>mypass</entry>
```
## Terminal Notifications for Long-Running Commands
This example provides a simple way to send notifications using [ntfy.sh](https://ntfy.sh) when a terminal command completes. It includes success or failure indicators based on the command's exit status.
Store your ntfy.sh bearer token securely if access control is enabled:
```sh
echo "your_bearer_token_here" > ~/.ntfy_token
chmod 600 ~/.ntfy_token
```
Add the following function and alias to your `.bashrc` or `.bash_profile`:
```sh
# Function for alert notifications using ntfy.sh
notify_via_ntfy() {
local exit_status=$? # Capture the exit status before doing anything else
local token=$(< ~/.ntfy_token) # Securely read the token
local status_icon="$([ $exit_status -eq 0 ] && echo magic_wand || echo warning)"
local last_command=$(history | tail -n1 | sed -e 's/^[[:space:]]*[0-9]\{1,\}[[:space:]]*//' -e 's/[;&|][[:space:]]*alert$//')
curl -s -X POST "https://n.example.dev/alerts" \
-H "Authorization: Bearer $token" \
-H "Title: Terminal" \
-H "X-Priority: 3" \
-H "Tags: $status_icon" \
-d "Command: $last_command (Exit: $exit_status)"
echo "Tags: $status_icon"
echo "$last_command (Exit: $exit_status)"
}
# Add an "alert" alias for long running commands using ntfy.sh
alias alert='notify_via_ntfy'
```
Now you can run any long-running command and append `alert` to notify when it completes:
```sh
sleep 10; alert
```
![ntfy notifications on mobile device](static/img/mobile-screenshot-notification.png)
**Notification Sent** with a success 🪄 (`magic_wand`) or failure ⚠️ (`warning`) tag.
To test failure notifications:
```sh
false; alert # Always fails (exit 1)
ls --invalid; alert # Invalid option
cat nonexistent_file; alert # File not found
```

View File

@@ -3,11 +3,11 @@ ntfy lets you **send push notifications to your phone or desktop via scripts fro
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://apps.apple.com/us/app/ntfy/id1625396347"><img src="static/img/badge-appstore.png"></a>
<a href="https://play.google.com/store/apps/details?id=io.heckel.ntfy"><img width="170" src="static/img/badge-googleplay.png"></a>
<a href="https://f-droid.org/en/packages/io.heckel.ntfy/"><img width="170" src="static/img/badge-fdroid.png"></a>
<a href="https://apps.apple.com/us/app/ntfy/id1625396347"><img width="150" 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.
To [receive notifications on your phone](subscribe/phone.md), install the app, either via Google Play, App Store 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.**

View File

@@ -30,37 +30,37 @@ deb/rpm packages.
=== "x86_64/amd64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_amd64.tar.gz
tar zxvf ntfy_2.9.0_linux_amd64.tar.gz
sudo cp -a ntfy_2.9.0_linux_amd64/ntfy /usr/local/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.9.0_linux_amd64/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_amd64.tar.gz
tar zxvf ntfy_2.12.0_linux_amd64.tar.gz
sudo cp -a ntfy_2.12.0_linux_amd64/ntfy /usr/local/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.12.0_linux_amd64/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "armv6"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_armv6.tar.gz
tar zxvf ntfy_2.9.0_linux_armv6.tar.gz
sudo cp -a ntfy_2.9.0_linux_armv6/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.9.0_linux_armv6/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv6.tar.gz
tar zxvf ntfy_2.12.0_linux_armv6.tar.gz
sudo cp -a ntfy_2.12.0_linux_armv6/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.12.0_linux_armv6/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "armv7/armhf"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_armv7.tar.gz
tar zxvf ntfy_2.9.0_linux_armv7.tar.gz
sudo cp -a ntfy_2.9.0_linux_armv7/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.9.0_linux_armv7/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv7.tar.gz
tar zxvf ntfy_2.12.0_linux_armv7.tar.gz
sudo cp -a ntfy_2.12.0_linux_armv7/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.12.0_linux_armv7/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "arm64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_arm64.tar.gz
tar zxvf ntfy_2.9.0_linux_arm64.tar.gz
sudo cp -a ntfy_2.9.0_linux_arm64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.9.0_linux_arm64/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_arm64.tar.gz
tar zxvf ntfy_2.12.0_linux_arm64.tar.gz
sudo cp -a ntfy_2.12.0_linux_arm64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.12.0_linux_arm64/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
@@ -110,7 +110,7 @@ Manually installing the .deb file:
=== "x86_64/amd64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_amd64.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_amd64.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -118,7 +118,7 @@ Manually installing the .deb file:
=== "armv6"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_armv6.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv6.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -126,7 +126,7 @@ Manually installing the .deb file:
=== "armv7/armhf"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_armv7.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv7.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -134,7 +134,7 @@ Manually installing the .deb file:
=== "arm64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_arm64.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_arm64.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -144,28 +144,28 @@ Manually installing the .deb file:
=== "x86_64/amd64"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_amd64.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_amd64.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "armv6"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_armv6.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv6.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "armv7/armhf"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_armv7.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv7.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "arm64"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_arm64.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_arm64.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
@@ -195,18 +195,18 @@ NixOS also supports [declarative setup of the ntfy server](https://search.nixos.
## macOS
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on macOS as well.
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_darwin_all.tar.gz),
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_darwin_all.tar.gz),
extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`).
If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For all other users, it'll look for it at
`~/Library/Application Support/ntfy/client.yml` (sample included in the tarball).
```bash
curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_darwin_all.tar.gz > ntfy_2.9.0_darwin_all.tar.gz
tar zxvf ntfy_2.9.0_darwin_all.tar.gz
sudo cp -a ntfy_2.9.0_darwin_all/ntfy /usr/local/bin/ntfy
curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_darwin_all.tar.gz > ntfy_2.12.0_darwin_all.tar.gz
tar zxvf ntfy_2.12.0_darwin_all.tar.gz
sudo cp -a ntfy_2.12.0_darwin_all/ntfy /usr/local/bin/ntfy
mkdir ~/Library/Application\ Support/ntfy
cp ntfy_2.9.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
cp ntfy_2.12.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
ntfy --help
```
@@ -224,7 +224,7 @@ brew install ntfy
## Windows
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well.
To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_windows_amd64.zip),
To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_windows_amd64.zip),
extract it and place the `ntfy.exe` binary somewhere in your `%Path%`.
The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file).
@@ -280,8 +280,6 @@ docker run \
Using docker-compose with non-root user and healthchecks enabled:
```yaml
version: "2.3"
services:
ntfy:
image: binwiederhier/ntfy
@@ -540,7 +538,7 @@ kubectl apply -k /ntfy
cpu: 150m
memory: 150Mi
volumeMounts:
- mountPath: /etc/ntfy/server.yml
- mountPath: /etc/ntfy
subPath: server.yml
name: config-volume # generated vie configMapGenerator from kustomization file
- mountPath: /var/cache/ntfy

View File

@@ -4,9 +4,21 @@ There are quite a few projects that work with ntfy, integrate ntfy, or have been
I've added a ⭐ to projects or posts that have a significant following, or had a lot of interaction by the community.
## Table of Contents
- [Official integrations](#official-integrations)
- [Integration via HTTP/SMTP/etc.](#integration-via-httpsmtpetc)
- [UnifiedPush integrations](#unifiedpush-integrations)
- [Libraries](#libraries)
- [CLIs + GUIs](#clis-guis)
- [Projects + scripts](#projects-scripts)
- [Blog + forum posts](#blog-forum-posts)
- [Alternative ntfy servers](#alternative-ntfy-servers)
## Official integrations
- [changedetection.io](https://changedetection.io) ⭐ - Website change detection and notification
- [Home Assistant](https://www.home-assistant.io/integrations/ntfy) ⭐ - Home Assistant is an open-source platform for automating and controlling smart home devices.
- [Healthchecks.io](https://healthchecks.io/) ⭐ - Online service for monitoring regularly running tasks such as cron jobs
- [Apprise](https://github.com/caronc/apprise/wiki/Notify_ntfy) ⭐ - Push notifications that work with just about every platform
- [Uptime Kuma](https://uptime.kuma.pet/) ⭐ - A self-hosted monitoring tool
@@ -26,14 +38,21 @@ I've added a ⭐ to projects or posts that have a significant following, or had
- [Cloudron](https://www.cloudron.io/store/sh.ntfy.cloudronapp.html) - Platform that makes it easy to manage web apps on your server
- [Xitoring](https://xitoring.com/docs/notifications/notification-roles/ntfy/) - Server and Uptime monitoring
- [HetrixTools](https://docs.hetrixtools.com/ntfy-sh-notifications/) - Uptime monitoring
- [EasyMorph](https://help.easymorph.com/doku.php?id=transformations:sendntfymessage) - Visual data transformation and automation tool
- [Monibot](https://monibot.io/) - Monibot monitors your websites, servers and applications and notifies you if something goes wrong.
- [Miniflux](https://miniflux.app/docs/ntfy.html) - Minimalist and opinionated feed reader
- [Beszel](https://beszel.dev/guide/notifications/ntfy) - Server monitoring platform
## Integration via HTTP/SMTP/etc.
- [Watchtower](https://containrrr.dev/watchtower/) ⭐ - Automating Docker container base image updates (see [integration example](examples.md#watchtower-shoutrrr))
- [Jellyfin](https://jellyfin.org/) ⭐ - The Free Software Media System (see [integration example](examples.md#))
- [Overseer](https://docs.overseerr.dev/using-overseerr/notifications/webhooks) ⭐ - a request management and media discovery tool for Plex (see [integration example](examples.md#jellyseerroverseerr-webhook))
- [Overseerr](https://docs.overseerr.dev/using-overseerr/notifications/webhooks) ⭐ - a request management and media discovery tool for Plex (see [integration example](examples.md#jellyseerroverseerr-webhook))
- [Tautulli](https://github.com/Tautulli/Tautulli) ⭐ - Monitoring and tracking tool for Plex (integration [via webhook](https://github.com/Tautulli/Tautulli/wiki/Notification-Agents-Guide#webhook))
- [Mailrise](https://github.com/YoRyan/mailrise) - An SMTP gateway (integration via [Apprise](https://github.com/caronc/apprise/wiki/Notify_ntfy))
- [Proxmox-Ntfy](https://github.com/qtsone/proxmox-ntfy) - Python script that monitors Proxmox tasks and sends notifications using the Ntfy service.
- [Scrutiny](https://github.com/AnalogJ/scrutiny) - WebUI for smartd S.M.A.R.T monitoring. Scrutiny includes shoutrrr/ntfy integration ([see integration README](https://github.com/AnalogJ/scrutiny?tab=readme-ov-file#notifications))
- [UptimeObserver](https://uptimeobserver.com) - Uptime Monitoring tool for Websites, APIs, SSL Certificates, DNS, Domain Names and Ports. [Integration Guide](https://support.uptimeobserver.com/integrations/ntfy/)
## [UnifiedPush](https://unifiedpush.org/users/apps/) integrations
@@ -61,16 +80,22 @@ I've added a ⭐ to projects or posts that have a significant following, or had
- [gotfy](https://github.com/AnthonyHewins/gotfy) - A Go wrapper for the ntfy API (Go)
- [symfony/ntfy-notifier](https://symfony.com/components/NtfyNotifier) ⭐ - Symfony Notifier integration for ntfy (PHP)
- [ntfy-java](https://github.com/MaheshBabu11/ntfy-java/) - A Java package to interact with a ntfy server (Java)
- [aiontfy](https://github.com/tr4nt0r/aiontfy) - Asynchronous client library for publishing and subscribing to ntfy (Python)
## CLIs + GUIs
- [ntfy.sh.sh](https://github.com/mininmobile/ntfy.sh.sh) - Run scripts on ntfy.sh events
- [ntfy Desktop client](https://codeberg.org/zvava/ntfy-desktop) - Cross-platform desktop application for ntfy
- [ntfy-desktop](https://codeberg.org/zvava/ntfy-desktop) - Cross-platform desktop application for ntfy
- [ntfy-desktop](https://github.com/Aetherinox/ntfy-desktop) - Desktop client for Windows, Linux, and MacOS with push notifications
- [ntfy svelte front-end](https://github.com/novatorem/Ntfy) - Front-end built with svelte
- [wio-ntfy-ticker](https://github.com/nachotp/wio-ntfy-ticker) - Ticker display for a ntfy.sh topic
- [ntfysh-windows](https://github.com/lucas-bortoli/ntfysh-windows) - A ntfy client for Windows Desktop
- [ntfyr](https://github.com/haxwithaxe/ntfyr) - A simple commandline tool to send notifications to ntfy
- [ntfy.py](https://github.com/ioqy/ntfy-client-python) - ntfy.py is a simple nfty.sh client for sending notifications
- [wlzntfy](https://github.com/Walzen-Group/ntfy-toaster) - A minimalistic, receive-only toast notification client for Windows 11
- [Ntfy_CSV_Reminders](https://github.com/thiswillbeyourgithub/Ntfy_CSV_Reminders) - A Python tool that sends random-timing phone notifications for recurring tasks by using daily probability checks based on CSV-defined frequencies.
- [Daily Fact Ntfy](https://github.com/thiswillbeyourgithub/Daily_Fact_Ntfy) - Generate [llm](https://github.com/simonw/llm) generated fact every day about any topic you're interested in.
- [ntfyexec](https://github.com/alecthomas/ntfyexec) - Send a notification through ntfy.sh if a command fails
## Projects + scripts
@@ -79,6 +104,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had
- [Grafana-to-ntfy](https://gitlab.com/Saibe1111/grafana-to-ntfy) - Grafana-to-ntfy alerts channel (Node Js)
- [ntfy-long-zsh-command](https://github.com/robfox92/ntfy-long-zsh-command) - Notifies you once a long-running command completes (zsh)
- [ntfy-shellscripts](https://github.com/nickexyz/ntfy-shellscripts) - A few scripts for the ntfy project (Shell)
- [alertmanager-ntfy-relay](https://github.com/therobbielee/alertmanager-ntfy-relay) - ntfy.sh relay for Alertmanager (Go)
- [QuickStatus](https://github.com/corneliusroot/QuickStatus) - A shell script to alert to any immediate problems upon login (Shell)
- [ntfy.el](https://github.com/shombando/ntfy) - Send notifications from Emacs (Emacs)
- [backup-projects](https://gist.github.com/anthonyaxenov/826ba65abbabd5b00196bc3e6af76002) - Stupidly simple backup script for own projects (Shell)
@@ -126,7 +152,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had
- [ntfyd](https://github.com/joachimschmidt557/ntfyd) - ntfy desktop daemon (Zig)
- [ntfy-browser](https://github.com/johman10/ntfy-browser) - browser extension to receive notifications without having the page open (TypeScript)
- [ntfy-electron](https://github.com/xdpirate/ntfy-electron) - Electron wrapper for the ntfy web app (JS)
- [systemd-ntfy-poweronoff](https://github.com/stendler/systemd-ntfy-poweronoff) - Systemd services to send notifications on system startup and shutdown (Go)
- [systemd-ntfy-poweronoff](https://github.com/stendler/systemd-ntfy-poweronoff) - Systemd services to send notifications on system startup, shutdown and service failure
- [msgdrop](https://github.com/jbrubake/msgdrop) - Send and receive encrypted messages (Bash)
- [vigilant](https://github.com/VerifiedJoseph/vigilant) - Monitor RSS/ATOM and JSON feeds, and send push notifications on new entries (PHP)
- [ansible-role-ntfy-alertmanager](https://github.com/bleetube/ansible-role-ntfy-alertmanager) - Ansible role to install xenrox/ntfy-alertmanager
@@ -141,9 +167,26 @@ I've added a ⭐ to projects or posts that have a significant following, or had
- [Notify](https://flathub.org/apps/com.ranfdev.Notify) - Native GTK4 client for ntfy (Rust)
- [notify-via-ntfy](https://exchange.checkmk.com/p/notify-via-ntfy) - Checkmk plugin to send notifications via ntfy (Python)
- [ntfy-java](https://github.com/MaheshBabu11/ntfy-java/) - A Java package to interact with a ntfy server (Java)
- [container-update-check](https://github.com/stendler/container-update-check) - Scripts to check and notify if a podman or docker container image can be updated (Podman/Shell)
- [ignition-combustion-template](https://github.com/stendler/ignition-combustion-template) - Templates and scripts to generate a configuration to automatically setup a system on first boot. Including systemd-ntfy-poweronoff (Shell)
- [ntfy-run](https://github.com/quantum5/ntfy-run) - Tool to run a command, capture its output, and send it to ntfy (Rust)
- [Clipboard IO](https://github.com/jim3692/clipboard-io) - End to end encrypted clipboard
- [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp) - An ntfy MCP server for sending/fetching ntfy notifications to your self-hosted ntfy server from AI Agents (supports secure token auth & more - use with npx or docker!) (Node/Typescript)
- [InvaderInformant](https://github.com/patricksthannon/InvaderInformant) - Script for Mac OS systems that monitors new or dropped connections to your network using ntfy (Shell)
- [NtfyPwsh](https://github.com/ptmorris1/NtfyPwsh) - PowerShell module to help send messages to ntfy (PowerShell)
- [ntfyrr](https://github.com/leukosaima/ntfyrr) - Currently an Overseerr webhook notification to ntfy helper service.
## Blog + forum posts
- [Device notifications via HTTP with ntfy](https://alistairshepherd.uk/writing/ntfy/) - alistairshepherd.uk - 6/2025
- [Notifications about (almost) anything with ntfy.sh](https://hamatti.org/posts/notifications-about-almost-anything-with-ntfy-sh/) - hamatti.org - 6/2025
- [I set up a self-hosted notification service for everything, and I'll never look back](https://www.xda-developers.com/set-up-self-hosted-notification-service/) ⭐ - xda-developers.com - 5/2025
- [How to Set Up Ntfy: Self-Hosted Push Notifications Made Easy](https://www.youtube.com/watch?v=wDJDiAYZ3H0) - youtube.com (sass drew) - 1/2025
- [The NTFY is a game-changer FREE solution for IT people](https://www.youtube.com/watch?v=NtlztHT-sRw) - youtube.com (Valters Tech Turf) - 1/2025
- [Notify: A Powerful Tool for Real-Time Notifications (ntfy.sh)](https://www.youtube.com/watch?v=XXTTeVfGBz0) - youtube.com (LinuxCloudHacks) - 12/2025
- [Push notifications with ntfy and n8n](https://www.youtube.com/watch?v=DKG1R3xYvwQ) - youtube.com (Oskar) - 10/2024
- [Setup ntfy for selfhosted notifications with Cloudflare Tunnel](https://medium.com/@svenvanginkel/setup-ntfy-for-selfhosted-notifications-with-cloudflare-tunnel-e342f470177d) - medium.com (Sven van Ginkel) - 10/2024
- [Self-Host NTFY - How It Works & Easy Setup Guide](https://www.youtube.com/watch?v=79wHc_jfrJE) ⭐ - youtube.com (Techdox)- 9/2024
- [ntfy / Emacs Lisp](https://speechcode.com/blog/ntfy/) - speechcode.com - 3/2024
- [Boost Your Productivity with ntfy.sh: The Ultimate Notification Tool for Command-Line Users](https://dev.to/archetypal/boost-your-productivity-with-ntfysh-the-ultimate-notification-tool-for-command-line-users-iil) - dev.to - 3/2024
- [Nextcloud Talk (F-Droid version) notifications using ntfy (ntfy.sh)](https://www.youtube.com/watch?v=0a6PpfN5PD8) - youtube.com - 2/2024
@@ -241,6 +284,8 @@ I've added a ⭐ to projects or posts that have a significant following, or had
- [ntfy otro sistema de notificaciones pub-sub simple basado en HTTP](https://ugeek.github.io/blog/post/2021-11-05-ntfy-sh-otro-sistema-de-notificaciones-pub-sub-simple-basado-en-http.html) - ugeek.github.io - 11/2021
- [Show HN: A tool to send push notifications to your phone, written in Go](https://news.ycombinator.com/item?id=29715464) ⭐ - news.ycombinator.com - 12/2021
- [Reddit selfhostable post](https://www.reddit.com/r/selfhosted/comments/qxlsm9/my_open_source_notification_android_app_and/) ⭐ - reddit.com - 11/2021
- [ntfy on The Canary in the Cage Podcast](https://odysee.com/@TheCanaryInTheCage:b/The-Canary-in-the-Cage-Episode-42:1?r=4gitYjTacQqPEjf22874USecDQYJ5y5E&t=3062) - odysee.com - 1/2025
- [NtfyPwsh - A PowerShell Module to Send Ntfy Messages](https://ptmorris1.github.io/posts/NtfyPwsh/) - github.io - 5/2025
## Alternative ntfy servers

File diff suppressed because one or more lines are too long

View File

@@ -2,13 +2,103 @@
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 server v2.12.0
Released May 29, 2025
This is mainly a maintenance release that updates dependencies, though since it's been over a year, there are a few
new features and bug fixes as well.
Thanks to everyone who contributed to this release, and special thanks to [@wunter8](https://github.com/wunter8) for his continued
user support in Discord/Matrix/GitHub! You rock, man!
**Features:**
* Add username/password auth to email publishing ([#1164](https://github.com/binwiederhier/ntfy/pull/1164), thanks to [@bishtawi](https://github.com/bishtawi))
* Write VAPID keys to file in `ntfy webpush --output-file` ([#1138](https://github.com/binwiederhier/ntfy/pull/1138), thanks to [@nogweii](https://github.com/nogweii))
* Add Docker major/minor version to image tags ([#1271](https://github.com/binwiederhier/ntfy/pull/1271), thanks to [@RoboMagus](https://github.com/RoboMagus))
* Add `latest` subscription param for grabbing just the most recent message ([#1216](https://github.com/binwiederhier/ntfy/pull/1216), thanks to [@wunter8](https://github.com/wunter8))
* Allow using `NTFY_PASSWORD_HASH` in `ntfy user` command instead of raw password ([#1340](https://github.com/binwiederhier/ntfy/pull/1340), thanks to [@Tom-Hubrecht](https://github.com/Tom-Hubrecht) for implementing)
* You can now change passwords via `v1/users` API ([#1267](https://github.com/binwiederhier/ntfy/pull/1267), thanks to [@wunter8](https://github.com/wunter8) for implementing)
* Make WebPush subscription warning/expiry configurable, increase default to 55/60 days ([#1212](https://github.com/binwiederhier/ntfy/pull/1212), thanks to [@KuroSetsuna29](https://github.com/KuroSetsuna29))
* Support [systemd user service](https://docs.ntfy.sh/subscribe/cli/#using-the-systemd-service) `ntfy-client.service` ([#1002](https://github.com/binwiederhier/ntfy/pull/1002), thanks to [@dandersch](https://github.com/dandersch))
**Bug fixes + maintenance:**
* Security updates for dependencies and Docker images ([#1341](https://github.com/binwiederhier/ntfy/pull/1341))
* Upgrade to Vite 6 ([#1342](https://github.com/binwiederhier/ntfy/pull/1342), thanks Dependabot)
* Fix iOS delivery issues for read-protected topics ([#1207](https://github.com/binwiederhier/ntfy/pull/1287), thanks a lot to [@barart](https://github.com/barart)!)
* Add `Date` header to outgoing emails to avoid rejection ([#1141](https://github.com/binwiederhier/ntfy/pull/1141), thanks to [@pcouy](https://github.com/pcouy))
* Fix IP address parsing when behind a proxy ([#1266](https://github.com/binwiederhier/ntfy/pull/1266), thanks to [@mmatuska](https://github.com/mmatuska))
* Make sure UnifiedPush messages are not treated as attachments ([#1312](https://github.com/binwiederhier/ntfy/pull/1312), thanks to [@vkrause](https://github.com/vkrause))
* Add OCI image version to Docker image ([#1307](https://github.com/binwiederhier/ntfy/pull/1307), thanks to [@jlssmt](https://github.com/jlssmt))
* WebSocket returning incorrect HTTP error code ([#1338](https://github.com/binwiederhier/ntfy/pull/1338) / [#1337](https://github.com/binwiederhier/ntfy/pull/1337), thanks to [@wunter8](https://github.com/wunter8) for debugging and implementing)
* Make Markdown in the web app scrollable horizontally ([#1262](https://github.com/binwiederhier/ntfy/pull/1262), thanks to [@rake5k](https://github.com/rake5k) for fixing)
* Make sure WebPush subscription topics are actually deleted (no ticket)
* Increase the number of access tokens per user to 60 ([#1308](https://github.com/binwiederhier/ntfy/issues/1308))
* Allow specifying `cache` and `firebase` via JSON publishing ([#1119](https://github.com/binwiederhier/ntfy/issues/1119)/[#1123](https://github.com/binwiederhier/ntfy/pull/1123), thanks to [@stendler](https://github.com/stendler))
**Documentation:**
* Lots of new integrations and projects. Amazing!
* [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp)
* [UptimeObserver](https://uptimeobserver.com)
* [alertmanager-ntfy-relay](https://github.com/therobbielee/alertmanager-ntfy-relay)
* [Monibot](https://monibot.io/)
* [Daily_Fact_Ntfy](https://github.com/thiswillbeyourgithub/Daily_Fact_Ntfy)
* [EasyMorph](https://help.easymorph.com/doku.php?id=transformations:sendntfymessage)
* [ntfy-run](https://github.com/quantum5/ntfy-run)
* [Clipboard IO](https://github.com/jim3692/clipboard-io)
* [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp)
* [InvaderInformant](https://github.com/patricksthannon/InvaderInformant)
* Various docs updates ([#1161](https://github.com/binwiederhier/ntfy/pull/1161), thanks to [@OneWeekNotice](https://github.com/OneWeekNotice))
* Typo in config docs ([#1177](https://github.com/binwiederhier/ntfy/pull/1177), thanks to [@hoho4190](https://github.com/hoho4190))
* Typo in CLI docs ([#1172](https://github.com/binwiederhier/ntfy/pull/1172), thanks to [@anirvan](https://github.com/anirvan))
* Correction about MacroDroid ([#1137](https://github.com/binwiederhier/ntfy/pull/1137), thanks to [@ShlomoCode](https://github.com/ShlomoCode))
* Note about fail2ban in Docker ([#1175](https://github.com/binwiederhier/ntfy/pull/1175)), thanks to [@Measurity](https://github.com/Measurity))
* Lots of other tiny docs updates, thanks to everyone who contributed!
**Languages**
* Update new languages from Weblate. Thanks to all the contributors!
* Added Tamil (தமிழ்) as a new language to the web app
### ntfy server v2.11.0
Released May 13, 2024
This is a tiny release that fixes a database index issue that caused performance issues on ntfy.sh. It also fixes a bug
in the rate visitor logic that caused rate visitors to be assigned to seemingly random topics. Nothing major this time.
❤️ Quick reminder that if you like ntfy, **please consider sponsoring us** via [GitHub Sponsors](https://github.com/sponsors/binwiederhier)
and [Liberapay](https://en.liberapay.com/ntfy/), or buying a [paid plan via the web app](https://ntfy.sh/app). ntfy will always remain open source.
**Bug fixes + maintenance:**
* Re-add database index `idx_topic` to the `messages` table to fix performance issues on ntfy.sh (no ticket, big thanks to [@tcaputi](https://github.com/tcaputi) for finding this issue)
* Do not set rate visitor for non-eligible topics (no ticket)
* Do not cache `config.js` ([#1098](https://github.com/binwiederhier/ntfy/pull/1098), thanks to [@wunter8](https://github.com/wunter8))
### ntfy server v2.10.0
Released Mar 27, 2024
This release adds support for **message templating** in the ntfy server, which allows you to include a message and/or
title template that will be filled with values from a JSON body (e.g. `curl -gd '{"alert":"Disk space low"}' "ntfy.sh/mytopic?tpl=1&m={{.alert}}"`).
This is great for services that let you specify a webhook URL but do not let you change the webhook body (such as GitHub, or Grafana).
**Features:**
* [Message templating](publish.md#message-templating): You can now include a message and/or title template that will be filled with values from a JSON body ([#724](https://github.com/binwiederhier/ntfy/issues/724), thanks to [@wunter8](https://github.com/wunter8) for implementing)
### ntfy server v2.9.0
Released Mar 7, 2024
A small release after a long pause (lots of day job work). This release adds for **larger messages** and **longer message delays** in scheduled delivery messages. The web app also now supports pasting images from the clipboard. Other than that, only a few bug fixes and documentation updates, and a teeny tiny breaking change 😬.
A small release after a long pause (lots of day job work). This release adds for **larger messages** and **longer
message delays** in scheduled delivery messages. The web app also now supports pasting images from the clipboard. Other
than that, only a few bug fixes and documentation updates, and a teeny tiny breaking change 😬.
!!! info
⚠️ **Breaking change**: The `Rate-Topics` header was removed due to a [DoS issue](https://github.com/binwiederhier/ntfy/issues/1048). This only affects installations with `visitor-subscriber-rate-limiting: true`, which is not the default and likely very rarely used. Normally I'd never remove a feature, but this is a security issue, and likely affects almost nobody.
⚠️ **Breaking change**: The `Rate-Topics` header was removed due to a [DoS issue](https://github.com/binwiederhier/ntfy/issues/1048). This only affects
installations with `visitor-subscriber-rate-limiting: true`, which is not the default and likely very rarely used.
Normally I'd never remove a feature, but this is a security issue, and likely affects almost nobody.
**Features:**
@@ -30,7 +120,9 @@ A small release after a long pause (lots of day job work). This release adds for
## ntfy iOS app v1.3
Released Nov 26, 2023
This release (hopefully) fixes the issues with the iOS UI not updating properly when new notifications arrive, as well as notifications not being received (anymore) after previously working. Both issues have been annoying and known bugs for a long time, and I hope that they are finally fixed.
This release (hopefully) fixes the issues with the iOS UI not updating properly when new notifications arrive, as well
as notifications not being received (anymore) after previously working. Both issues have been annoying and known bugs
for a long time, and I hope that they are finally fixed.
Many thanks to [@tcaputi](https://github.com/tcaputi) for fixing the issues, and to the anonymous donor for sponsoring these fixes.
@@ -41,7 +133,10 @@ Many thanks to [@tcaputi](https://github.com/tcaputi) for fixing the issues, and
## ntfy server v2.8.0
Released November 19, 2023
This release brings a handful of random bug fixes: two unrelated access control list fixes, a fix around web app crashes for languages with underscores in the language code (e.g. `zh_Hant`, `zh_Hans`, `pt_BR`, ...), a workaround for the `Priority` header (often used in Cloudflare setups), and support among others support for HTML-only emails (finally), web app crash fixes
This release brings a handful of random bug fixes: two unrelated access control list fixes, a fix around web app crashes
for languages with underscores in the language code (e.g. `zh_Hant`, `zh_Hans`, `pt_BR`, ...), a workaround for the
`Priority` header (often used in Cloudflare setups), and support among others support for HTML-only emails (finally),
web app crash fixes
**Bug fixes + maintenance:**
@@ -654,7 +749,7 @@ minute or so, due to competing stats gathering (personal installations will like
**Features:**
* Add `cache-startup-queries` option to allow custom [SQLite performance tuning](config.md#wal-for-message-cache) (no ticket)
* Add `cache-startup-queries` option to allow custom [SQLite performance tuning](config.md#message-cache) (no ticket)
* ntfy CLI can now [wait for a command or PID](subscribe/cli.md#wait-for-pidcommand) before publishing ([#263](https://github.com/binwiederhier/ntfy/issues/263), thanks to the [original ntfy](https://github.com/dschep/ntfy) for the idea)
* Trace: Log entire HTTP request to simplify debugging (no ticket)
* Allow setting user password via `NTFY_PASSWORD` env variable ([#327](https://github.com/binwiederhier/ntfy/pull/327), thanks to [@Kenix3](https://github.com/Kenix3))
@@ -1338,11 +1433,18 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
## Not released yet
### ntfy server v2.9.1 (UNRELEASED)
### ntfy server v2.13.0 (UNRELEASED)
**Features:**
* You can now include a message and/or title template that will be filled with values from a JSON body, great for services that let you specify a webhook URL but do not let you change the webhook body (such as Grafana). ([#724](https://github.com/binwiederhier/ntfy/issues/724), thanks to [@wunter8](https://github.com/wunter8) for implementing)
* Full [IPv6 support](config.md#ipv6-support) for ntfy and the official ntfy.sh server ([#519](https://github.com/binwiederhier/ntfy/issues/519)/[#1380](https://github.com/binwiederhier/ntfy/pull/1380)/[ansible#4](https://github.com/binwiederhier/ntfy-ansible/pull/4))
* Support `X-Client-IP`, `X-Real-IP`, `Forwarded` headers for [rate limiting](config.md#ip-based-rate-limiting) via `proxy-forwarded-header` and `proxy-trusted-hosts` ([#1360](https://github.com/binwiederhier/ntfy/pull/1360)/[#1252](https://github.com/binwiederhier/ntfy/pull/1252), thanks to [@pixitha](https://github.com/pixitha))
* Add STDIN support for `ntfy publish` ([#1382](https://github.com/binwiederhier/ntfy/pull/1382), thanks to [@srevn](https://github.com/srevn))
**Languages**
* Update new languages from Weblate. Thanks to all the contributors!
* Added Estonian (Esti), Galician (Galego), Romanian (Română), Slovak (Slovenčina) as new languages to the web app
### ntfy Android app v1.16.1 (UNRELEASED)

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

@@ -1,99 +1,103 @@
// Link tabs, as per https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/#linked-tabs
const savedCodeTab = localStorage.getItem('savedTab')
const codeTabs = document.querySelectorAll(".tabbed-set > input")
const savedCodeTab = localStorage.getItem("savedTab");
const codeTabs = document.querySelectorAll(".tabbed-set > input");
for (const tab of codeTabs) {
tab.addEventListener("click", () => {
const current = document.querySelector(`label[for=${tab.id}]`)
const pos = current.getBoundingClientRect().top
const labelContent = current.innerHTML
const labels = document.querySelectorAll('.tabbed-set > label, .tabbed-alternate > .tabbed-labels > label')
for (const label of labels) {
if (label.innerHTML === labelContent) {
document.querySelector(`input[id=${label.getAttribute('for')}]`).checked = true
}
}
// Preserve scroll position
const delta = (current.getBoundingClientRect().top) - pos
window.scrollBy(0, delta)
// Save
localStorage.setItem('savedTab', labelContent)
})
// Select saved tab
const current = document.querySelector(`label[for=${tab.id}]`)
const labelContent = current.innerHTML
if (savedCodeTab === labelContent) {
tab.checked = true
tab.addEventListener("click", () => {
const current = document.querySelector(`label[for=${tab.id}]`);
const pos = current.getBoundingClientRect().top;
const labelContent = current.innerHTML;
const labels = document.querySelectorAll(".tabbed-set > label, .tabbed-alternate > .tabbed-labels > label");
for (const label of labels) {
if (label.innerHTML === labelContent) {
document.querySelector(`input[id=${label.getAttribute("for")}]`).checked = true;
}
}
// Preserve scroll position
const delta = (current.getBoundingClientRect().top) - pos;
window.scrollBy(0, delta);
// Save
localStorage.setItem("savedTab", labelContent);
});
// Select saved tab
const current = document.querySelector(`label[for=${tab.id}]`);
const labelContent = current.innerHTML;
if (savedCodeTab === labelContent) {
tab.checked = true;
}
}
// Lightbox for screenshot
const lightbox = document.createElement('div');
lightbox.classList.add('lightbox');
const lightbox = document.createElement("div");
lightbox.classList.add("lightbox");
document.body.appendChild(lightbox);
const showScreenshotOverlay = (e, el, group, index) => {
lightbox.classList.add('show');
document.addEventListener('keydown', nextScreenshotKeyboardListener);
return showScreenshot(e, group, index);
lightbox.classList.add("show");
document.addEventListener("keydown", nextScreenshotKeyboardListener);
return showScreenshot(e, group, index);
};
const showScreenshot = (e, group, index) => {
const actualIndex = resolveScreenshotIndex(group, index);
lightbox.innerHTML = '<div class="close-lightbox"></div>' + screenshots[group][actualIndex].innerHTML;
lightbox.querySelector('img').onclick = (e) => { return showScreenshot(e, group, actualIndex+1); };
currentScreenshotGroup = group;
currentScreenshotIndex = actualIndex;
e.stopPropagation();
return false;
const actualIndex = resolveScreenshotIndex(group, index);
lightbox.innerHTML = "<div class=\"close-lightbox\"></div>" + screenshots[group][actualIndex].innerHTML;
lightbox.querySelector("img").onclick = (e) => {
return showScreenshot(e, group, actualIndex + 1);
};
currentScreenshotGroup = group;
currentScreenshotIndex = actualIndex;
e.stopPropagation();
return false;
};
const nextScreenshot = (e) => {
return showScreenshot(e, currentScreenshotGroup, currentScreenshotIndex+1);
return showScreenshot(e, currentScreenshotGroup, currentScreenshotIndex + 1);
};
const previousScreenshot = (e) => {
return showScreenshot(e, currentScreenshotGroup, currentScreenshotIndex-1);
return showScreenshot(e, currentScreenshotGroup, currentScreenshotIndex - 1);
};
const resolveScreenshotIndex = (group, index) => {
if (index < 0) {
return screenshots[group].length - 1;
} else if (index > screenshots[group].length - 1) {
return 0;
}
return index;
if (index < 0) {
return screenshots[group].length - 1;
} else if (index > screenshots[group].length - 1) {
return 0;
}
return index;
};
const hideScreenshotOverlay = (e) => {
lightbox.classList.remove('show');
document.removeEventListener('keydown', nextScreenshotKeyboardListener);
lightbox.classList.remove("show");
document.removeEventListener("keydown", nextScreenshotKeyboardListener);
};
const nextScreenshotKeyboardListener = (e) => {
switch (e.keyCode) {
case 37:
previousScreenshot(e);
break;
case 39:
nextScreenshot(e);
break;
}
switch (e.keyCode) {
case 37:
previousScreenshot(e);
break;
case 39:
nextScreenshot(e);
break;
}
};
let currentScreenshotGroup = '';
let currentScreenshotGroup = "";
let currentScreenshotIndex = 0;
let screenshots = {};
Array.from(document.getElementsByClassName('screenshots')).forEach((sg) => {
const group = sg.id;
screenshots[group] = [...sg.querySelectorAll('a')];
screenshots[group].forEach((el, index) => {
el.onclick = (e) => { return showScreenshotOverlay(e, el, group, index); };
});
Array.from(document.getElementsByClassName("screenshots")).forEach((sg) => {
const group = sg.id;
screenshots[group] = [...sg.querySelectorAll("a")];
screenshots[group].forEach((el, index) => {
el.onclick = (e) => {
return showScreenshotOverlay(e, el, group, index);
};
});
});
lightbox.onclick = hideScreenshotOverlay;

View File

@@ -132,7 +132,7 @@ easy to use. Here's what it looks like. You may also want to check out the [full
### Subscribe as raw stream
The `/raw` endpoint will output one line per message, and **will only include the message body**. It's useful for extremely
simple scripts, and doesn't include all the data. Additional fields such as [priority](../publish.md#message-priority),
[tags](../publish.md#tags--emojis--) or [message title](../publish.md#message-title) are not included in this output
[tags](../publish.md#tags-emojis) or [message title](../publish.md#message-title) are not included in this output
format. Keepalive messages are sent as empty lines.
=== "Command line (curl)"
@@ -257,6 +257,14 @@ curl -s "ntfy.sh/mytopic/json?since=1645970742"
curl -s "ntfy.sh/mytopic/json?since=nFS3knfcQ1xe"
```
### Fetch latest message
If you only want the most recent message sent to a topic and do not have a message ID or timestamp to use with
`since=`, you can use `since=latest` to grab the most recent message from the cache for a particular topic.
```
curl -s "ntfy.sh/mytopic/json?poll=1&since=latest"
```
### Fetch scheduled messages
Messages that are [scheduled to be delivered](../publish.md#scheduled-delivery) at a later date are not typically
returned when subscribing via the API, which makes sense, because after all, the messages have technically not been
@@ -305,7 +313,7 @@ Depending on whether the server is configured to support [access control](../con
may be read/write protected so that only users with the correct credentials can subscribe or publish to them.
To publish/subscribe to protected topics, you can:
* Use [basic auth](../publish.md#basic-auth), e.g. `Authorization: Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk`
* Use [basic auth](../publish.md#authentication), e.g. `Authorization: Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk`
* or use the [`auth` query parameter](../publish.md#query-param), e.g. `?auth=QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw`
Please refer to the [publishing documentation](../publish.md#authentication) for additional details.

View File

@@ -190,6 +190,10 @@ Here's an example config file that subscribes to three different topics, executi
=== "~/.config/ntfy/client.yml (Linux)"
```yaml
default-host: https://ntfy.sh
default-user: phill
default-password: mypass
subscribe:
- topic: echo-this
command: 'echo "Message received: $message"'
@@ -210,9 +214,12 @@ Here's an example config file that subscribes to three different topics, executi
fi
```
=== "~/Library/Application Support/ntfy/client.yml (macOS)"
```yaml
default-host: https://ntfy.sh
default-user: phill
default-password: mypass
subscribe:
- topic: echo-this
command: 'echo "Message received: $message"'
@@ -226,6 +233,10 @@ Here's an example config file that subscribes to three different topics, executi
=== "%AppData%\ntfy\client.yml (Windows)"
```yaml
default-host: https://ntfy.sh
default-user: phill
default-password: mypass
subscribe:
- topic: echo-this
command: 'echo Message received: %message%'
@@ -263,43 +274,31 @@ will be used, otherwise, the subscription settings will override the defaults.
require authentication), be sure that the servers/topics you subscribe to use HTTPS to prevent leaking the username and password.
### Using the systemd service
You can use the `ntfy-client` systemd service (see [ntfy-client.service](https://github.com/binwiederhier/ntfy/blob/main/client/ntfy-client.service))
to subscribe to multiple topics just like in the example above. The service is automatically installed (but not started)
if you install the deb/rpm package. To configure it, simply edit `/etc/ntfy/client.yml` and run `sudo systemctl restart ntfy-client`.
You can use the `ntfy-client` systemd services to subscribe to multiple topics just like in the example above.
!!! info
The `ntfy-client.service` runs as user `ntfy`, meaning that typical Linux permission restrictions apply. See below
for how to fix this.
You have the option of either enabling `ntfy-client` as a **system service** (see [here](https://github.com/binwiederhier/ntfy/blob/main/client/ntfy-client.service))
or **user service** (see [here](https://github.com/binwiederhier/ntfy/blob/main/client/user/ntfy-client.service)). Neither system service nor user service are enabled or started by default, so you have to do that yourself.
If the service runs on your personal desktop machine, you may want to override the service user/group (`User=` and `Group=`), and
adjust the `DISPLAY` and `DBUS_SESSION_BUS_ADDRESS` environment variables. This will allow you to run commands in your X session
as the primary machine user.
You can either manually override these systemd service entries with `sudo systemctl edit ntfy-client`, and add this
(assuming your user is `phil`). Don't forget to run `sudo systemctl daemon-reload` and `sudo systemctl restart ntfy-client`
after editing the service file:
=== "/etc/systemd/system/ntfy-client.service.d/override.conf"
```
[Service]
User=phil
Group=phil
Environment="DISPLAY=:0" "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus"
```
Or you can run the following script that creates this override config for you:
**System service:** The `ntfy-client` systemd system service runs as the `ntfy` user. When enabled, it is started at system boot. To configure it as a system
service, edit `/etc/ntfy/client.yml` and then enable/start the service (as root), like so:
```
sudo sh -c 'cat > /etc/systemd/system/ntfy-client.service.d/override.conf' <<EOF
[Service]
User=$USER
Group=$USER
Environment="DISPLAY=:0" "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus"
EOF
sudo systemctl daemon-reload
sudo systemctl enable ntfy-client
sudo systemctl restart ntfy-client
```
The system service runs as user `ntfy`, meaning that typical Linux permission restrictions apply. It also means that the system service cannot run commands in your X session as the primary machine user (unlike the user service).
**User service:** The `ntfy-client` user service is run when the user logs into their desktop environment. To enable/start it, edit `~/.config/ntfy/client.yml` and
run the following commands (without sudo!):
```
systemctl --user enable ntfy-client
systemctl --user restart ntfy-client
```
Unlike the system service, the user service can interact with the user's desktop environment, and run commands like `notify-send` to display desktop notifications.
It can also run commands that require access to the user's home directory, such as `gnome-calculator`.
### Authentication
Depending on whether the server is configured to support [access control](../config.md#access-control), some topics
@@ -317,7 +316,7 @@ You can either add your username and password to the configuration file:
password: mypass
```
Or with the `ntfy subscibe` command:
Or with the `ntfy subscribe` command:
```
ntfy subscribe \
-u phil:mypass \

View File

@@ -4,9 +4,9 @@ to receive notifications directly on your phone. Just like the server, this app
on GitHub ([Android](https://github.com/binwiederhier/ntfy-android), [iOS](https://github.com/binwiederhier/ntfy-ios)). Feel free to
contribute, or [build your own](../develop.md).
<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://apps.apple.com/us/app/ntfy/id1625396347"><img src="../../static/img/badge-appstore.png"></a>
<a href="https://play.google.com/store/apps/details?id=io.heckel.ntfy"><img width="170" src="../../static/img/badge-googleplay.png"></a>
<a href="https://f-droid.org/en/packages/io.heckel.ntfy/"><img width="170" src="../../static/img/badge-fdroid.png"></a>
<a href="https://apps.apple.com/us/app/ntfy/id1625396347"><img width="150" src="../../static/img/badge-appstore.png"></a>
You can get the Android app from both [Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy) and
from [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/). Both are largely identical, with the one exception that

137
go.mod
View File

@@ -1,27 +1,27 @@
module heckel.io/ntfy/v2
go 1.21
go 1.24
toolchain go1.21.3
toolchain go1.24.0
require (
cloud.google.com/go/firestore v1.15.0 // indirect
cloud.google.com/go/storage v1.39.1 // indirect
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
cloud.google.com/go/firestore v1.18.0 // indirect
cloud.google.com/go/storage v1.55.0 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/emersion/go-smtp v0.18.0
github.com/gabriel-vasile/mimetype v1.4.3
github.com/gorilla/websocket v1.5.1
github.com/mattn/go-sqlite3 v1.14.22
github.com/olebedev/when v1.0.0
github.com/stretchr/testify v1.8.4
github.com/urfave/cli/v2 v2.27.1
golang.org/x/crypto v0.21.0
golang.org/x/oauth2 v0.18.0 // indirect
golang.org/x/sync v0.6.0
golang.org/x/term v0.18.0
golang.org/x/time v0.5.0
google.golang.org/api v0.170.0
github.com/gabriel-vasile/mimetype v1.4.9
github.com/gorilla/websocket v1.5.3
github.com/mattn/go-sqlite3 v1.14.28
github.com/olebedev/when v1.1.0
github.com/stretchr/testify v1.10.0
github.com/urfave/cli/v2 v2.27.7
golang.org/x/crypto v0.39.0
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.15.0
golang.org/x/term v0.32.0
golang.org/x/time v0.12.0
google.golang.org/api v0.240.0
gopkg.in/yaml.v2 v2.4.0
)
@@ -30,64 +30,75 @@ replace github.com/emersion/go-smtp => github.com/emersion/go-smtp v0.17.0 // Pi
require github.com/pkg/errors v0.9.1 // indirect
require (
firebase.google.com/go/v4 v4.13.0
github.com/SherClockHolmes/webpush-go v1.3.0
github.com/microcosm-cc/bluemonday v1.0.26
github.com/prometheus/client_golang v1.19.0
firebase.google.com/go/v4 v4.16.1
github.com/SherClockHolmes/webpush-go v1.4.0
github.com/microcosm-cc/bluemonday v1.0.27
github.com/prometheus/client_golang v1.22.0
github.com/stripe/stripe-go/v74 v74.30.0
github.com/tidwall/gjson v1.17.1
)
require (
cloud.google.com/go v0.112.1 // indirect
cloud.google.com/go/compute v1.25.1 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v1.1.7 // indirect
cloud.google.com/go/longrunning v0.5.6 // indirect
cel.dev/expr v0.24.0 // indirect
cloud.google.com/go v0.121.3 // indirect
cloud.google.com/go/auth v0.16.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.7.0 // indirect
cloud.google.com/go/iam v1.5.2 // indirect
cloud.google.com/go/longrunning v0.6.7 // indirect
cloud.google.com/go/monitoring v1.24.2 // indirect
github.com/AlekSi/pointer v1.2.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect
github.com/MicahParks/keyfunc v1.9.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-jose/go-jose/v4 v4.1.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.3 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.0 // indirect
github.com/prometheus/common v0.50.0 // indirect
github.com/prometheus/procfs v0.13.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/appengine/v2 v2.0.5 // indirect
google.golang.org/genproto v0.0.0-20240318140521-94a12d6c2237 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect
google.golang.org/grpc v1.62.1 // indirect
google.golang.org/protobuf v1.33.0 // indirect
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
github.com/zeebo/errs v1.4.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.37.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
google.golang.org/appengine/v2 v2.0.6 // indirect
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/grpc v1.73.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

402
go.sum
View File

@@ -1,214 +1,219 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM=
cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4=
cloud.google.com/go/compute v1.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU=
cloud.google.com/go/compute v1.25.1/go.mod h1:oopOIR53ly6viBYxaDhBfJwzUAxf1zE//uf3IB011ls=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/firestore v1.15.0 h1:/k8ppuWOtNuDHt2tsRV42yI21uaGnKDEQnRFeBpbFF8=
cloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk=
cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM=
cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA=
cloud.google.com/go/longrunning v0.5.6 h1:xAe8+0YaWoCKr9t1+aWe+OeQgN/iJK1fEgZSXmjuEaE=
cloud.google.com/go/longrunning v0.5.6/go.mod h1:vUaDrWYOMKRuhiv6JBnn49YxCPz2Ayn9GqyjaBT8/mA=
cloud.google.com/go/storage v1.39.1 h1:MvraqHKhogCOTXTlct/9C3K3+Uy2jBmFYb3/Sp6dVtY=
cloud.google.com/go/storage v1.39.1/go.mod h1:xK6xZmxZmo+fyP7+DEF6FhNc24/JAe95OLyOHCXFH1o=
firebase.google.com/go/v4 v4.13.0 h1:meFz9nvDNh/FDyrEykoAzSfComcQbmnQSjoHrePRqeI=
firebase.google.com/go/v4 v4.13.0/go.mod h1:e1/gaR6EnbQfsmTnAMx1hnz+ninJIrrr/RAh59Tpfn8=
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cloud.google.com/go v0.121.3 h1:84RD+hQXNdY5Sw/MWVAx5O9Aui/rd5VQ9HEcdN19afo=
cloud.google.com/go v0.121.3/go.mod h1:6vWF3nJWRrEUv26mMB3FEIU/o1MQNVPG1iHdisa2SJc=
cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4=
cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
cloud.google.com/go/firestore v1.18.0 h1:cuydCaLS7Vl2SatAeivXyhbhDEIR8BDmtn4egDhIn2s=
cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU=
cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc=
cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA=
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM=
cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U=
cloud.google.com/go/storage v1.55.0 h1:NESjdAToN9u1tmhVqhXCaCwYBuvEhZLLv0gBr+2znf0=
cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY=
cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4=
cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI=
firebase.google.com/go/v4 v4.16.1 h1:Kl5cgXmM0VOWDGT1UAx6b0T2UFWa14ak0CvYqeI7Py4=
firebase.google.com/go/v4 v4.16.1/go.mod h1:aAPJq/bOyb23tBlc1K6GR+2E8sOGAeJSc8wIJVgl9SM=
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0/go.mod h1:jUZ5LYlw40WMd07qxcQJD5M40aUxrfwqQX1g7zxYnrQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo=
github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
github.com/SherClockHolmes/webpush-go v1.3.0 h1:CAu3FvEE9QS4drc3iKNgpBWFfGqNthKlZhp5QpYnu6k=
github.com/SherClockHolmes/webpush-go v1.3.0/go.mod h1:AxRHmJuYwKGG1PVgYzToik1lphQvDnqFYDqimHvwhIw=
github.com/SherClockHolmes/webpush-go v1.4.0 h1:ocnzNKWN23T9nvHi6IfyrQjkIc0oJWv1B1pULsf9i3s=
github.com/SherClockHolmes/webpush-go v1.4.0/go.mod h1:XSq8pKX11vNV8MJEMwjrlTkxhAj1zKfxmyhdV7Pd6UA=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 h1:hH4PQfOndHDlpzYfLAAfl63E8Le6F2+EL/cdhlkyRJY=
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.17.0 h1:tq90evlrcyqRfE6DSXaWVH54oX6OuZOQECEmhWBMEtI=
github.com/emersion/go-smtp v0.17.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI=
github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA=
github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4=
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0=
github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
github.com/olebedev/when v1.0.0 h1:T2DZCj8HxUhOVxcqaLOmzuTr+iZLtMHsZEim7mjIA2w=
github.com/olebedev/when v1.0.0/go.mod h1:T0THb4kP9D3NNqlvCwIG4GyUioTAzEhB4RNVzig/43E=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/olebedev/when v1.1.0 h1:dlpoRa7huImhNtEx4yl0WYfTHVEWmJmIWd7fEkTHayc=
github.com/olebedev/when v1.1.0/go.mod h1:T0THb4kP9D3NNqlvCwIG4GyUioTAzEhB4RNVzig/43E=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
github.com/prometheus/common v0.50.0 h1:YSZE6aa9+luNa2da6/Tik0q0A5AbR+U003TItK57CPQ=
github.com/prometheus/common v0.50.0/go.mod h1:wHFBCEVWVmHMUpg7pYcOm2QUR/ocQdYSJVQJKnHc3xQ=
github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE=
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stripe/stripe-go/v74 v74.30.0 h1:0Kf0KkeFnY7iRhOwvTerX0Ia1BRw+eV1CVJ51mGYAUY=
github.com/stripe/stripe-go/v74 v74.30.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw=
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk=
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw=
go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/detectors/gcp v1.37.0 h1:B+WbN9RPsvobe6q4vP6KgM8/9plR/HNjgGBrfcOlweA=
go.opentelemetry.io/contrib/detectors/gcp v1.37.0/go.mod h1:K5zQ3TT7p2ru9Qkzk0bKtCql0RGkPj9pRjpXgZJZ+rU=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 h1:rbRJ8BBoVMsQShESYZ0FkvcITu8X8QNwJogcLUmDNNw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -216,14 +221,23 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -231,60 +245,38 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/api v0.170.0 h1:zMaruDePM88zxZBG+NG8+reALO2rfLhe/JShitLyT48=
google.golang.org/api v0.170.0/go.mod h1:/xql9M2btF85xac/VAm4PsLMTLVGUOpq4BE9R8jyNy8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/appengine/v2 v2.0.5 h1:4C+F3Cd3L2nWEfSmFEZDPjQvDwL8T0YCeZBysZifP3k=
google.golang.org/appengine/v2 v2.0.5/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20240318140521-94a12d6c2237 h1:PgNlNSx2Nq2/j4juYzQBG0/Zdr+WP4z5N01Vk4VYBCY=
google.golang.org/genproto v0.0.0-20240318140521-94a12d6c2237/go.mod h1:9sVD8c25Af3p0rGs7S7LLsxWKFiJt/65LdSyqXBkX/Y=
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4=
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/api v0.240.0 h1:PxG3AA2UIqT1ofIzWV2COM3j3JagKTKSwy7L6RHNXNU=
google.golang.org/api v0.240.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw=
google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI=
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -293,5 +285,3 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -198,7 +198,7 @@ func (w *peekLogWriter) Write(p []byte) (n int, err error) {
if len(p) == 0 || p[0] == '{' || CurrentFormat() == TextFormat {
return w.w.Write(p)
}
m := newEvent().Tag(tagStdLog).Render(InfoLevel, strings.TrimSpace(string(p)))
m := newEvent().Tag(tagStdLog).Render(InfoLevel, "%s", strings.TrimSpace(string(p)))
if m == "" {
return 0, nil
}

View File

@@ -23,7 +23,7 @@ If you want to chat, simply join the Discord server (https://discord.gg/cT7ECsZj
the Matrix room (https://matrix.to/#/#ntfy:matrix.org).
ntfy %s (%s), runtime %s, built at %s
Copyright (C) 2022 Philipp C. Heckel, licensed under Apache License 2.0 & GPLv2
Copyright (C) Philipp C. Heckel, licensed under Apache License 2.0 & GPLv2
`, version, commit[:7], runtime.Version(), date)
app := cmd.New()

View File

@@ -33,7 +33,7 @@ if [ "$1" = "configure" ] || [ "$1" -ge 1 ]; then
fi
fi
if systemctl is-active -q ntfy-client.service; then
echo "Restarting ntfy-client.service ..."
echo "Restarting ntfy-client.service (system) ..."
if [ -x /usr/bin/deb-systemd-invoke ]; then
deb-systemd-invoke try-restart ntfy-client.service >/dev/null || true
else

View File

@@ -26,8 +26,8 @@ const (
// Defines default Web Push settings
const (
DefaultWebPushExpiryWarningDuration = 7 * 24 * time.Hour
DefaultWebPushExpiryDuration = 9 * 24 * time.Hour
DefaultWebPushExpiryWarningDuration = 55 * 24 * time.Hour
DefaultWebPushExpiryDuration = 60 * 24 * time.Hour
)
// Defines all global and per-visitor limits
@@ -61,6 +61,8 @@ const (
DefaultVisitorAuthFailureLimitReplenish = time.Minute
DefaultVisitorAttachmentTotalSizeLimit = 100 * 1024 * 1024 // 100 MB
DefaultVisitorAttachmentDailyBandwidthLimit = 500 * 1024 * 1024 // 500 MB
DefaultVisitorPrefixBitsIPv4 = 32 // Use the entire IPv4 address for rate limiting
DefaultVisitorPrefixBitsIPv6 = 64 // Use /64 for IPv6 rate limiting
)
var (
@@ -133,7 +135,7 @@ type Config struct {
VisitorAttachmentDailyBandwidthLimit int64
VisitorRequestLimitBurst int
VisitorRequestLimitReplenish time.Duration
VisitorRequestExemptIPAddrs []netip.Prefix
VisitorRequestExemptPrefixes []netip.Prefix
VisitorMessageDailyLimit int
VisitorEmailLimitBurst int
VisitorEmailLimitReplenish time.Duration
@@ -141,9 +143,13 @@ type Config struct {
VisitorAccountCreationLimitReplenish time.Duration
VisitorAuthFailureLimitBurst int
VisitorAuthFailureLimitReplenish time.Duration
VisitorStatsResetTime time.Time // Time of the day at which to reset visitor stats
VisitorSubscriberRateLimiting bool // Enable subscriber-based rate limiting for UnifiedPush topics
BehindProxy bool
VisitorStatsResetTime time.Time // Time of the day at which to reset visitor stats
VisitorSubscriberRateLimiting bool // Enable subscriber-based rate limiting for UnifiedPush topics
VisitorPrefixBitsIPv4 int // Number of bits for IPv4 rate limiting (default: 32)
VisitorPrefixBitsIPv6 int // Number of bits for IPv6 rate limiting (default: 64)
BehindProxy bool // If true, the server will trust the proxy client IP header to determine the client IP address (IPv4 and IPv6 supported)
ProxyForwardedHeader string // The header field to read the real/client IP address from, if BehindProxy is true, defaults to "X-Forwarded-For" (IPv4 and IPv6 supported)
ProxyTrustedPrefixes []netip.Prefix // List of trusted proxy networks (IPv4 or IPv6) that will be stripped from the Forwarded header if BehindProxy is true
StripeSecretKey string
StripeWebhookKey string
StripePriceCacheDuration time.Duration
@@ -153,7 +159,6 @@ type Config struct {
EnableReservations bool // Allow users with role "user" to own/reserve topics
EnableMetrics bool
AccessControlAllowOrigin string // CORS header field to restrict access from web clients
Version string // injected by App
WebPushPrivateKey string
WebPushPublicKey string
WebPushFile string
@@ -161,6 +166,7 @@ type Config struct {
WebPushStartupQueries string
WebPushExpiryDuration time.Duration
WebPushExpiryWarningDuration time.Duration
Version string // injected by App
}
// NewConfig instantiates a default new server config
@@ -218,11 +224,12 @@ func NewConfig() *Config {
TotalTopicLimit: DefaultTotalTopicLimit,
TotalAttachmentSizeLimit: 0,
VisitorSubscriptionLimit: DefaultVisitorSubscriptionLimit,
VisitorSubscriberRateLimiting: false,
VisitorAttachmentTotalSizeLimit: DefaultVisitorAttachmentTotalSizeLimit,
VisitorAttachmentDailyBandwidthLimit: DefaultVisitorAttachmentDailyBandwidthLimit,
VisitorRequestLimitBurst: DefaultVisitorRequestLimitBurst,
VisitorRequestLimitReplenish: DefaultVisitorRequestLimitReplenish,
VisitorRequestExemptIPAddrs: make([]netip.Prefix, 0),
VisitorRequestExemptPrefixes: make([]netip.Prefix, 0),
VisitorMessageDailyLimit: DefaultVisitorMessageDailyLimit,
VisitorEmailLimitBurst: DefaultVisitorEmailLimitBurst,
VisitorEmailLimitReplenish: DefaultVisitorEmailLimitReplenish,
@@ -231,8 +238,10 @@ func NewConfig() *Config {
VisitorAuthFailureLimitBurst: DefaultVisitorAuthFailureLimitBurst,
VisitorAuthFailureLimitReplenish: DefaultVisitorAuthFailureLimitReplenish,
VisitorStatsResetTime: DefaultVisitorStatsResetTime,
VisitorSubscriberRateLimiting: false,
BehindProxy: false,
VisitorPrefixBitsIPv4: DefaultVisitorPrefixBitsIPv4, // Default: use full IPv4 address
VisitorPrefixBitsIPv6: DefaultVisitorPrefixBitsIPv6, // Default: use /64 for IPv6
BehindProxy: false, // If true, the server will trust the proxy client IP header to determine the client IP address
ProxyForwardedHeader: "X-Forwarded-For", // Default header for reverse proxy client IPs
StripeSecretKey: "",
StripeWebhookKey: "",
StripePriceCacheDuration: DefaultStripePriceCacheDuration,

View File

@@ -89,7 +89,7 @@ var (
errHTTPBadRequestSinceInvalid = &errHTTP{40008, http.StatusBadRequest, "invalid since parameter", "https://ntfy.sh/docs/subscribe/api/#fetch-cached-messages", nil}
errHTTPBadRequestTopicInvalid = &errHTTP{40009, http.StatusBadRequest, "invalid request: topic invalid", "", nil}
errHTTPBadRequestTopicDisallowed = &errHTTP{40010, http.StatusBadRequest, "invalid request: topic name is not allowed", "", nil}
errHTTPBadRequestMessageNotUTF8 = &errHTTP{40011, http.StatusBadRequest, "invalid message: message must be UTF-8 encoded", "", nil}
errHTTPBadRequestMessageNotUTF8 = &errHTTP{40011, http.StatusBadRequest, "invalid request: message must be UTF-8 encoded", "", nil}
errHTTPBadRequestAttachmentURLInvalid = &errHTTP{40013, http.StatusBadRequest, "invalid request: attachment URL is invalid", "https://ntfy.sh/docs/publish/#attachments", nil}
errHTTPBadRequestAttachmentsDisallowed = &errHTTP{40014, http.StatusBadRequest, "invalid request: attachments not allowed", "https://ntfy.sh/docs/config/#attachments", nil}
errHTTPBadRequestAttachmentsExpiryBeforeDelivery = &errHTTP{40015, http.StatusBadRequest, "invalid request: attachment expiry before delayed delivery date", "https://ntfy.sh/docs/publish/#scheduled-delivery", nil}
@@ -113,12 +113,16 @@ var (
errHTTPBadRequestPhoneNumberNotVerified = &errHTTP{40034, http.StatusBadRequest, "invalid request: phone number not verified, or no matching verified numbers found", "https://ntfy.sh/docs/publish/#phone-calls", nil}
errHTTPBadRequestAnonymousCallsNotAllowed = &errHTTP{40035, http.StatusBadRequest, "invalid request: anonymous phone calls are not allowed", "https://ntfy.sh/docs/publish/#phone-calls", nil}
errHTTPBadRequestPhoneNumberVerifyChannelInvalid = &errHTTP{40036, http.StatusBadRequest, "invalid request: verification channel must be 'sms' or 'call'", "https://ntfy.sh/docs/publish/#phone-calls", nil}
errHTTPBadRequestDelayNoCall = &errHTTP{40037, http.StatusBadRequest, "delayed call notifications are not supported", "", nil}
errHTTPBadRequestDelayNoCall = &errHTTP{40037, http.StatusBadRequest, "invalid request: delayed call notifications are not supported", "", nil}
errHTTPBadRequestWebPushSubscriptionInvalid = &errHTTP{40038, http.StatusBadRequest, "invalid request: web push payload malformed", "", nil}
errHTTPBadRequestWebPushEndpointUnknown = &errHTTP{40039, http.StatusBadRequest, "invalid request: web push endpoint unknown", "", nil}
errHTTPBadRequestWebPushTopicCountTooHigh = &errHTTP{40040, http.StatusBadRequest, "invalid request: too many web push topic subscriptions", "", nil}
errHTTPBadRequestTemplatedMessageTooLarge = &errHTTP{40041, http.StatusBadRequest, "invalid request: message or title is too large after replacing template", "", nil}
errHTTPBadRequestTemplatedMessageNotJSON = &errHTTP{40042, http.StatusBadRequest, "invalid request: message body must be JSON if templating is enabled", "", nil}
errHTTPBadRequestTemplateMessageTooLarge = &errHTTP{40041, http.StatusBadRequest, "invalid request: message or title is too large after replacing template", "https://ntfy.sh/docs/publish/#message-templating", nil}
errHTTPBadRequestTemplateMessageNotJSON = &errHTTP{40042, http.StatusBadRequest, "invalid request: message body must be JSON if templating is enabled", "https://ntfy.sh/docs/publish/#message-templating", nil}
errHTTPBadRequestTemplateInvalid = &errHTTP{40043, http.StatusBadRequest, "invalid request: could not parse template", "https://ntfy.sh/docs/publish/#message-templating", nil}
errHTTPBadRequestTemplateDisallowedFunctionCalls = &errHTTP{40044, http.StatusBadRequest, "invalid request: template contains disallowed function calls, e.g. template, call, or define", "https://ntfy.sh/docs/publish/#message-templating", nil}
errHTTPBadRequestTemplateExecuteFailed = &errHTTP{40045, http.StatusBadRequest, "invalid request: template execution failed", "https://ntfy.sh/docs/publish/#message-templating", nil}
errHTTPBadRequestInvalidUsername = &errHTTP{40046, http.StatusBadRequest, "invalid request: invalid username", "", nil}
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil}
errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil}
errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil}

View File

@@ -99,6 +99,13 @@ const (
WHERE topic = ? AND (id > ? OR published = 0)
ORDER BY time, id
`
selectMessagesLatestQuery = `
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
FROM messages
WHERE topic = ? AND published = 1
ORDER BY time DESC, id DESC
LIMIT 1
`
selectMessagesDueQuery = `
SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding
FROM messages
@@ -122,7 +129,7 @@ const (
// Schema management queries
const (
currentSchemaVersion = 12
currentSchemaVersion = 13
createSchemaVersionTableQuery = `
CREATE TABLE IF NOT EXISTS schemaVersion (
id INT PRIMARY KEY,
@@ -246,6 +253,11 @@ const (
migrate11To12AlterMessagesTableQuery = `
ALTER TABLE messages ADD COLUMN content_type TEXT NOT NULL DEFAULT('');
`
// 12 -> 13
migrate12To13AlterMessagesTableQuery = `
CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic);
`
)
var (
@@ -262,6 +274,7 @@ var (
9: migrateFrom9,
10: migrateFrom10,
11: migrateFrom11,
12: migrateFrom12,
}
)
@@ -410,6 +423,8 @@ func (c *messageCache) addMessages(ms []*message) error {
func (c *messageCache) Messages(topic string, since sinceMarker, scheduled bool) ([]*message, error) {
if since.IsNone() {
return make([]*message, 0), nil
} else if since.IsLatest() {
return c.messagesLatest(topic)
} else if since.IsID() {
return c.messagesSinceID(topic, since, scheduled)
}
@@ -456,6 +471,14 @@ func (c *messageCache) messagesSinceID(topic string, since sinceMarker, schedule
return readMessages(rows)
}
func (c *messageCache) messagesLatest(topic string) ([]*message, error) {
rows, err := c.db.Query(selectMessagesLatestQuery, topic)
if err != nil {
return nil, err
}
return readMessages(rows)
}
func (c *messageCache) MessagesDue() ([]*message, error) {
rows, err := c.db.Query(selectMessagesDueQuery, time.Now().Unix())
if err != nil {
@@ -970,3 +993,19 @@ func migrateFrom11(db *sql.DB, _ time.Duration) error {
}
return tx.Commit()
}
func migrateFrom12(db *sql.DB, _ time.Duration) error {
log.Tag(tagMessageCache).Info("Migrating cache database schema: from 12 to 13")
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.Exec(migrate12To13AlterMessagesTableQuery); err != nil {
return err
}
if _, err := tx.Exec(updateSchemaVersion, 13); err != nil {
return err
}
return tx.Commit()
}

View File

@@ -8,7 +8,6 @@ import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -67,6 +66,11 @@ func testCacheMessages(t *testing.T, c *messageCache) {
require.Equal(t, 1, len(messages))
require.Equal(t, "my other message", messages[0].Message)
// mytopic: latest
messages, _ = c.Messages("mytopic", sinceLatestMessage, false)
require.Equal(t, 1, len(messages))
require.Equal(t, "my other message", messages[0].Message)
// example: count
counts, err = c.MessageCounts()
require.Nil(t, err)
@@ -509,6 +513,14 @@ func TestSqliteCache_Migration_From1(t *testing.T) {
messages, err = c.Messages("mytopic", sinceAllMessages, true)
require.Nil(t, err)
require.Equal(t, 11, len(messages))
// Check that index "idx_topic" exists
rows, err := c.db.Query(`SELECT name FROM sqlite_master WHERE type='index' AND name='idx_topic'`)
require.Nil(t, err)
require.True(t, rows.Next())
var indexName string
require.Nil(t, rows.Scan(&indexName))
require.Equal(t, "idx_topic", indexName)
}
func TestSqliteCache_Migration_From9(t *testing.T) {
@@ -675,15 +687,15 @@ func checkSchemaVersion(t *testing.T, db *sql.DB) {
func TestMemCache_NopCache(t *testing.T) {
c, _ := newNopCache()
assert.Nil(t, c.AddMessage(newDefaultMessage("mytopic", "my message")))
require.Nil(t, c.AddMessage(newDefaultMessage("mytopic", "my message")))
messages, err := c.Messages("mytopic", sinceAllMessages, false)
assert.Nil(t, err)
assert.Empty(t, messages)
require.Nil(t, err)
require.Empty(t, messages)
topics, err := c.Topics()
assert.Nil(t, err)
assert.Empty(t, topics)
require.Nil(t, err)
require.Empty(t, topics)
}
func newSqliteTestCache(t *testing.T) *messageCache {
@@ -700,16 +712,12 @@ func newSqliteTestCacheFile(t *testing.T) string {
func newSqliteTestCacheFromFile(t *testing.T, filename, startupQueries string) *messageCache {
c, err := newSqliteCache(filename, startupQueries, time.Hour, 0, 0, false)
if err != nil {
t.Fatal(err)
}
require.Nil(t, err)
return c
}
func newMemTestCache(t *testing.T) *messageCache {
c, err := newMemCache()
if err != nil {
t.Fatal(err)
}
require.Nil(t, err)
return c
}

View File

@@ -23,13 +23,13 @@ import (
"strconv"
"strings"
"sync"
"text/template"
"time"
"unicode/utf8"
"github.com/emersion/go-smtp"
"github.com/gorilla/websocket"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/tidwall/gjson"
"golang.org/x/sync/errgroup"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/user"
@@ -110,8 +110,6 @@ var (
fileRegex = regexp.MustCompile(`^/file/([-_A-Za-z0-9]{1,64})(?:\.[A-Za-z0-9]{1,16})?$`)
urlRegex = regexp.MustCompile(`^https?://`)
phoneNumberRegex = regexp.MustCompile(`^\+\d{1,100}$`)
templateVarRegex = regexp.MustCompile(`\${([^}]+)}`)
templateVarFormat = "${%s}"
//go:embed site
webFs embed.FS
@@ -135,6 +133,13 @@ const (
unifiedPushTopicPrefix = "up" // Temporarily, we rate limit all "up*" topics based on the subscriber
unifiedPushTopicLength = 14 // Length of UnifiedPush topics, including the "up" part
messagesHistoryMax = 10 // Number of message count values to keep in memory
templateMaxExecutionTime = 100 * time.Millisecond
)
var (
// templateDisallowedRegex tests a template for disallowed expressions. While not really dangerous, they
// are not useful, and seem potentially troublesome.
templateDisallowedRegex = regexp.MustCompile(`(?m)\{\{-?\s*(call|template|define)\b`)
)
// WebSocket constants
@@ -408,7 +413,8 @@ func (s *Server) handleError(w http.ResponseWriter, r *http.Request, v *visitor,
} else {
ev.Info("WebSocket error: %s", err.Error())
}
return // Do not attempt to write to upgraded connection
w.WriteHeader(httpErr.HTTPCode)
return // Do not attempt to write any body to upgraded connection
}
if isNormalError {
ev.Debug("Connection closed with HTTP %d (ntfy error %d)", httpErr.HTTPCode, httpErr.Code)
@@ -440,8 +446,10 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
return s.ensureWebPushEnabled(s.handleWebManifest)(w, r, v)
} else if r.Method == http.MethodGet && r.URL.Path == apiUsersPath {
return s.ensureAdmin(s.handleUsersGet)(w, r, v)
} else if r.Method == http.MethodPut && r.URL.Path == apiUsersPath {
} else if r.Method == http.MethodPost && r.URL.Path == apiUsersPath {
return s.ensureAdmin(s.handleUsersAdd)(w, r, v)
} else if r.Method == http.MethodPut && r.URL.Path == apiUsersPath {
return s.ensureAdmin(s.handleUsersUpdate)(w, r, v)
} else if r.Method == http.MethodDelete && r.URL.Path == apiUsersPath {
return s.ensureAdmin(s.handleUsersDelete)(w, r, v)
} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && r.URL.Path == apiUsersAccessPath {
@@ -588,6 +596,7 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi
return err
}
w.Header().Set("Content-Type", "text/javascript")
w.Header().Set("Cache-Control", "no-cache")
_, err = io.WriteString(w, fmt.Sprintf("// Generated server configuration\nvar config = %s;\n", string(b)))
return err
}
@@ -751,7 +760,7 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e
// the subscription as invalid if any 400-499 code (except 429/408) is returned.
// See https://github.com/mastodon/mastodon/blob/730bb3e211a84a2f30e3e2bbeae3f77149824a68/app/workers/web/push_notification_worker.rb#L35-L46
return nil, errHTTPInsufficientStorageUnifiedPush.With(t)
} else if !util.ContainsIP(s.config.VisitorRequestExemptIPAddrs, v.ip) && !vrate.MessageAllowed() {
} else if !util.ContainsIP(s.config.VisitorRequestExemptPrefixes, v.ip) && !vrate.MessageAllowed() {
return nil, errHTTPTooManyRequestsLimitMessages.With(t)
} else if email != "" && !vrate.EmailAllowed() {
return nil, errHTTPTooManyRequestsLimitEmails.With(t)
@@ -1010,7 +1019,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
if actionsStr != "" {
m.Actions, e = parseActions(actionsStr)
if e != nil {
return false, false, "", "", false, false, errHTTPBadRequestActionsInvalid.Wrap(e.Error())
return false, false, "", "", false, false, errHTTPBadRequestActionsInvalid.Wrap("%s", e.Error())
}
}
contentType, markdown := readParam(r, "content-type", "content_type"), readBoolParam(r, false, "x-markdown", "markdown", "md")
@@ -1019,7 +1028,8 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
}
template = readBoolParam(r, false, "x-template", "template", "tpl")
unifiedpush = readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see GET too!
if unifiedpush {
contentEncoding := readParam(r, "content-encoding")
if unifiedpush || contentEncoding == "aes128gcm" {
firebase = false
unifiedpush = true
}
@@ -1095,32 +1105,42 @@ func (s *Server) handleBodyAsTextMessage(m *message, body *util.PeekedReadCloser
}
func (s *Server) handleBodyAsTemplatedTextMessage(m *message, body *util.PeekedReadCloser) error {
body, err := util.Peek(body, jsonBodyBytesLimit)
body, err := util.Peek(body, max(s.config.MessageSizeLimit, jsonBodyBytesLimit))
if err != nil {
return err
} else if body.LimitReached {
return errHTTPEntityTooLargeJSONBody
}
peekedBody := strings.TrimSpace(string(body.PeekedBytes))
if !gjson.Valid(peekedBody) {
return errHTTPBadRequestTemplatedMessageNotJSON
if m.Message, err = replaceTemplate(m.Message, peekedBody); err != nil {
return err
}
if m.Title, err = replaceTemplate(m.Title, peekedBody); err != nil {
return err
}
m.Message = replaceGJSONTemplate(m.Message, peekedBody)
m.Title = replaceGJSONTemplate(m.Title, peekedBody)
if len(m.Message) > s.config.MessageSizeLimit {
return errHTTPBadRequestTemplatedMessageTooLarge
return errHTTPBadRequestTemplateMessageTooLarge
}
return nil
}
func replaceGJSONTemplate(template string, source string) string {
matches := templateVarRegex.FindAllStringSubmatch(template, -1)
for _, m := range matches {
if result := gjson.Get(source, m[1]); result.Exists() {
template = strings.ReplaceAll(template, fmt.Sprintf(templateVarFormat, m[1]), result.String())
}
func replaceTemplate(tpl string, source string) (string, error) {
if templateDisallowedRegex.MatchString(tpl) {
return "", errHTTPBadRequestTemplateDisallowedFunctionCalls
}
return template
var data any
if err := json.Unmarshal([]byte(source), &data); err != nil {
return "", errHTTPBadRequestTemplateMessageNotJSON
}
t, err := template.New("").Parse(tpl)
if err != nil {
return "", errHTTPBadRequestTemplateInvalid
}
var buf bytes.Buffer
if err := t.Execute(util.NewTimeoutWriter(&buf, templateMaxExecutionTime), data); err != nil {
return "", errHTTPBadRequestTemplateExecuteFailed
}
return buf.String(), nil
}
func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser) error {
@@ -1484,6 +1504,9 @@ func (s *Server) maybeSetRateVisitors(r *http.Request, v *visitor, topics []*top
// - topic is not reserved, and v.user has write access
writableRateTopics := make([]*topic, 0)
for _, t := range topics {
if !util.Contains(eligibleRateTopics, t) {
continue
}
ownerUserID, err := s.userManager.ReservationOwner(t.ID)
if err != nil {
return err
@@ -1537,8 +1560,8 @@ func (s *Server) sendOldMessages(topics []*topic, since sinceMarker, scheduled b
// parseSince returns a timestamp identifying the time span from which cached messages should be received.
//
// Values in the "since=..." parameter can be either a unix timestamp or a duration (e.g. 12h), or
// "all" for all messages.
// Values in the "since=..." parameter can be either a unix timestamp or a duration (e.g. 12h),
// "all" for all messages, or "latest" for the most recent message for a topic
func parseSince(r *http.Request, poll bool) (sinceMarker, error) {
since := readParam(r, "x-since", "since", "si")
@@ -1550,6 +1573,8 @@ func parseSince(r *http.Request, poll bool) (sinceMarker, error) {
return sinceNoMessages, nil
} else if since == "all" {
return sinceAllMessages, nil
} else if since == "latest" {
return sinceLatestMessage, nil
} else if since == "none" {
return sinceNoMessages, nil
}
@@ -1809,7 +1834,7 @@ func (s *Server) transformBodyJSON(next handleFunc) handleFunc {
if m.Priority != 0 {
r.Header.Set("X-Priority", fmt.Sprintf("%d", m.Priority))
}
if m.Tags != nil && len(m.Tags) > 0 {
if len(m.Tags) > 0 {
r.Header.Set("X-Tags", strings.Join(m.Tags, ","))
}
if m.Attach != "" {
@@ -1843,6 +1868,12 @@ func (s *Server) transformBodyJSON(next handleFunc) handleFunc {
if m.Call != "" {
r.Header.Set("X-Call", m.Call)
}
if m.Cache != "" {
r.Header.Set("X-Cache", m.Cache)
}
if m.Firebase != "" {
r.Header.Set("X-Firebase", m.Firebase)
}
return next(w, r, v)
}
}
@@ -1866,14 +1897,14 @@ func (s *Server) transformMatrixJSON(next handleFunc) handleFunc {
}
func (s *Server) authorizeTopicWrite(next handleFunc) handleFunc {
return s.autorizeTopic(next, user.PermissionWrite)
return s.authorizeTopic(next, user.PermissionWrite)
}
func (s *Server) authorizeTopicRead(next handleFunc) handleFunc {
return s.autorizeTopic(next, user.PermissionRead)
return s.authorizeTopic(next, user.PermissionRead)
}
func (s *Server) autorizeTopic(next handleFunc, perm user.Permission) handleFunc {
func (s *Server) authorizeTopic(next handleFunc, perm user.Permission) handleFunc {
return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
if s.userManager == nil {
return next(w, r, v)
@@ -1905,8 +1936,8 @@ func (s *Server) autorizeTopic(next handleFunc, perm user.Permission) handleFunc
// This function will ALWAYS return a visitor, even if an error occurs (e.g. unauthorized), so
// that subsequent logging calls still have a visitor context.
func (s *Server) maybeAuthenticate(r *http.Request) (*visitor, error) {
// Read "Authorization" header value, and exit out early if it's not set
ip := extractIPAddress(r, s.config.BehindProxy)
// Read the "Authorization" header value and exit out early if it's not set
ip := extractIPAddress(r, s.config.BehindProxy, s.config.ProxyForwardedHeader, s.config.ProxyTrustedPrefixes)
vip := s.visitor(ip, nil)
if s.userManager == nil {
return vip, nil
@@ -1981,7 +2012,7 @@ func (s *Server) authenticateBearerAuth(r *http.Request, token string) (*user.Us
if err != nil {
return nil, err
}
ip := extractIPAddress(r, s.config.BehindProxy)
ip := extractIPAddress(r, s.config.BehindProxy, s.config.ProxyForwardedHeader, s.config.ProxyTrustedPrefixes)
go s.userManager.EnqueueTokenUpdate(token, &user.TokenUpdate{
LastAccess: time.Now(),
LastOrigin: ip,
@@ -1992,7 +2023,7 @@ func (s *Server) authenticateBearerAuth(r *http.Request, token string) (*user.Us
func (s *Server) visitor(ip netip.Addr, user *user.User) *visitor {
s.mu.Lock()
defer s.mu.Unlock()
id := visitorID(ip, user)
id := visitorID(ip, user, s.config)
v, exists := s.visitors[id]
if !exists {
s.visitors[id] = newVisitor(s.config, s.messageCache, s.userManager, ip, user)

View File

@@ -95,13 +95,23 @@
# auth-default-access: "read-write"
# auth-startup-queries:
# If set, the X-Forwarded-For header is used to determine the visitor IP address
# instead of the remote address of the connection.
# If set, the X-Forwarded-For header (or whatever is configured in proxy-forwarded-header) is used to determine
# the visitor IP address instead of the remote address of the connection.
#
# WARNING: If you are behind a proxy, you must set this, otherwise all visitors are rate limited
# WARNING: If you are behind a proxy, you must set this, otherwise all visitors are rate-limited
# as if they are one.
#
# - behind-proxy makes it so that the real visitor IP address is extracted from the header defined in
# proxy-forwarded-header. Without this, the remote address of the incoming connection is used.
# - proxy-forwarded-header is the header to use to identify visitors. It may be a single IP address (e.g. 1.2.3.4),
# a comma-separated list of IP addresses (e.g. "1.2.3.4, 5.6.7.8"), or an RFC 7239-style header (e.g. "for=1.2.3.4;by=proxy.example.com, for=5.6.7.8").
# - proxy-trusted-hosts is a comma-separated list of IP addresses, hostnames or CIDRs that are removed from the forwarded header
# to determine the real IP address. This is only useful if there are multiple proxies involved that add themselves to
# the forwarded header.
#
# behind-proxy: false
# proxy-forwarded-header: "X-Forwarded-For"
# proxy-trusted-hosts:
# If enabled, clients can attach files to notifications as attachments. Minimum settings to enable attachments
# are "attachment-cache-dir" and "base-url".
@@ -138,7 +148,7 @@
# - smtp-server-domain is the e-mail domain, e.g. ntfy.sh
# - 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).
# $topic@ntfy.sh will be accepted (which may be a spam problem).
#
# smtp-server-listen:
# smtp-server-domain:
@@ -146,7 +156,7 @@
# Web Push support (background notifications for browsers)
#
# If enabled, allows ntfy to receive push notifications, even when the ntfy web app is closed. When enabled, users
# If enabled, allows the ntfy web app to receive push notifications, even when the web app is closed. When enabled, users
# can enable background notifications in the web app. Once enabled, ntfy will forward published messages to the push
# endpoint, which will then forward it to the browser.
#
@@ -155,15 +165,19 @@
#
# - web-push-public-key is the generated VAPID public key, e.g. AA1234BBCCddvveekaabcdfqwertyuiopasdfghjklzxcvbnm1234567890
# - web-push-private-key is the generated VAPID private key, e.g. AA2BB1234567890abcdefzxcvbnm1234567890
# - web-push-file is a database file to keep track of browser subscription endpoints, e.g. `/var/cache/ntfy/webpush.db`
# - web-push-email-address is the admin email address send to the push provider, e.g. `sysadmin@example.com`
# - web-push-file is a database file to keep track of browser subscription endpoints, e.g. /var/cache/ntfy/webpush.db
# - web-push-email-address is the admin email address send to the push provider, e.g. sysadmin@example.com
# - web-push-startup-queries is an optional list of queries to run on startup`
# - web-push-expiry-warning-duration defines the duration after which unused subscriptions are sent a warning (default is 55d`)
# - web-push-expiry-duration defines the duration after which unused subscriptions will expire (default is 60d)
#
# web-push-public-key:
# web-push-private-key:
# web-push-file:
# web-push-email-address:
# web-push-startup-queries:
# web-push-expiry-warning-duration: "55d"
# web-push-expiry-duration: "60d"
# If enabled, ntfy can perform voice calls via Twilio via the "X-Call" header.
#
@@ -278,6 +292,18 @@
# visitor-email-limit-burst: 16
# visitor-email-limit-replenish: "1h"
# Rate limiting: IPv4/IPv6 address prefix bits used for rate limiting
# - visitor-prefix-bits-ipv4: number of bits of the IPv4 address to use for rate limiting (default: 32, full address)
# - visitor-prefix-bits-ipv6: number of bits of the IPv6 address to use for rate limiting (default: 64, /64 subnet)
#
# This is used to group visitors by their IP address or subnet. For example, if you set visitor-prefix-bits-ipv4 to 24,
# all visitors in the 1.2.3.0/24 network are treated as one.
#
# By default, ntfy uses the full IPv4 address (32 bits) and the /64 subnet of the IPv6 address (64 bits).
#
# visitor-prefix-bits-ipv4: 32
# visitor-prefix-bits-ipv6: 64
# Rate limiting: Attachment size and bandwidth limits per visitor:
# - visitor-attachment-total-size-limit is the total storage limit used for attachments per visitor
# - visitor-attachment-daily-bandwidth-limit is the total daily attachment download/upload traffic limit per visitor

View File

@@ -2,6 +2,7 @@ package server
import (
"encoding/json"
"errors"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util"
@@ -36,7 +37,10 @@ func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v *
return errHTTPConflictUserExists
}
logvr(v, r).Tag(tagAccount).Field("user_name", newAccount.Username).Info("Creating user %s", newAccount.Username)
if err := s.userManager.AddUser(newAccount.Username, newAccount.Password, user.RoleUser); err != nil {
if err := s.userManager.AddUser(newAccount.Username, newAccount.Password, user.RoleUser, false); err != nil {
if errors.Is(err, user.ErrInvalidArgument) {
return errHTTPBadRequestInvalidUsername
}
return err
}
v.AccountCreated()
@@ -203,7 +207,7 @@ func (s *Server) handleAccountPasswordChange(w http.ResponseWriter, r *http.Requ
return errHTTPBadRequestIncorrectPasswordConfirmation
}
logvr(v, r).Tag(tagAccount).Debug("Changing password for user %s", u.Name)
if err := s.userManager.ChangePassword(u.Name, req.NewPassword); err != nil {
if err := s.userManager.ChangePassword(u.Name, req.NewPassword, false); err != nil {
return err
}
return s.writeJSON(w, newSuccessResponse())

View File

@@ -87,9 +87,9 @@ func TestAccount_Signup_AsUser(t *testing.T) {
defer s.closeDatabases()
log.Info("1")
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
log.Info("2")
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false))
log.Info("3")
rr := request(t, s, "POST", "/v1/account", `{"username":"emma", "password":"emma"}`, map[string]string{
"Authorization": util.BasicAuth("phil", "phil"),
@@ -174,7 +174,7 @@ func TestAccount_ChangeSettings(t *testing.T) {
s := newTestServer(t, newTestConfigWithAuthFile(t))
defer s.closeDatabases()
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
u, _ := s.userManager.User("phil")
token, _ := s.userManager.CreateToken(u.ID, "", time.Unix(0, 0), netip.IPv4Unspecified())
@@ -203,7 +203,7 @@ func TestAccount_Subscription_AddUpdateDelete(t *testing.T) {
s := newTestServer(t, newTestConfigWithAuthFile(t))
defer s.closeDatabases()
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
rr := request(t, s, "POST", "/v1/account/subscription", `{"base_url": "http://abc.com", "topic": "def"}`, map[string]string{
"Authorization": util.BasicAuth("phil", "phil"),
@@ -254,7 +254,7 @@ func TestAccount_ChangePassword(t *testing.T) {
s := newTestServer(t, newTestConfigWithAuthFile(t))
defer s.closeDatabases()
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
rr := request(t, s, "POST", "/v1/account/password", `{"password": "WRONG", "new_password": ""}`, map[string]string{
"Authorization": util.BasicAuth("phil", "phil"),
@@ -296,7 +296,7 @@ func TestAccount_ExtendToken(t *testing.T) {
s := newTestServer(t, newTestConfigWithAuthFile(t))
defer s.closeDatabases()
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
rr := request(t, s, "POST", "/v1/account/token", "", map[string]string{
"Authorization": util.BasicAuth("phil", "phil"),
@@ -332,7 +332,7 @@ func TestAccount_ExtendToken_NoTokenProvided(t *testing.T) {
s := newTestServer(t, newTestConfigWithAuthFile(t))
defer s.closeDatabases()
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
rr := request(t, s, "PATCH", "/v1/account/token", "", map[string]string{
"Authorization": util.BasicAuth("phil", "phil"), // Not Bearer!
@@ -345,7 +345,7 @@ func TestAccount_DeleteToken(t *testing.T) {
s := newTestServer(t, newTestConfigWithAuthFile(t))
defer s.closeDatabases()
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
rr := request(t, s, "POST", "/v1/account/token", "", map[string]string{
"Authorization": util.BasicAuth("phil", "phil"),
@@ -455,14 +455,14 @@ func TestAccount_Reservation_AddAdminSuccess(t *testing.T) {
Code: "pro",
ReservationLimit: 2,
}))
require.Nil(t, s.userManager.AddUser("noadmin1", "pass", user.RoleUser))
require.Nil(t, s.userManager.AddUser("noadmin1", "pass", user.RoleUser, false))
require.Nil(t, s.userManager.ChangeTier("noadmin1", "pro"))
require.Nil(t, s.userManager.AddReservation("noadmin1", "mytopic", user.PermissionDenyAll))
require.Nil(t, s.userManager.AddUser("noadmin2", "pass", user.RoleUser))
require.Nil(t, s.userManager.AddUser("noadmin2", "pass", user.RoleUser, false))
require.Nil(t, s.userManager.ChangeTier("noadmin2", "pro"))
require.Nil(t, s.userManager.AddUser("phil", "adminpass", user.RoleAdmin))
require.Nil(t, s.userManager.AddUser("phil", "adminpass", user.RoleAdmin, false))
// Admin can reserve topic
rr := request(t, s, "POST", "/v1/account/reservation", `{"topic":"sometopic","everyone":"deny-all"}`, map[string]string{
@@ -624,7 +624,7 @@ func TestAccount_Reservation_Delete_Messages_And_Attachments(t *testing.T) {
s := newTestServer(t, conf)
// Create user with tier
require.Nil(t, s.userManager.AddUser("phil", "mypass", user.RoleUser))
require.Nil(t, s.userManager.AddUser("phil", "mypass", user.RoleUser, false))
require.Nil(t, s.userManager.AddTier(&user.Tier{
Code: "pro",
MessageLimit: 20,

View File

@@ -39,11 +39,11 @@ func (s *Server) handleUsersGet(w http.ResponseWriter, r *http.Request, v *visit
}
func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visitor) error {
req, err := readJSONWithLimit[apiUserAddRequest](r.Body, jsonBodyBytesLimit, false)
req, err := readJSONWithLimit[apiUserAddOrUpdateRequest](r.Body, jsonBodyBytesLimit, false)
if err != nil {
return err
} else if !user.AllowedUsername(req.Username) || req.Password == "" {
return errHTTPBadRequest.Wrap("username invalid, or password missing")
} else if !user.AllowedUsername(req.Username) || (req.Password == "" && req.Hash == "") {
return errHTTPBadRequest.Wrap("username invalid, or password/password_hash missing")
}
u, err := s.userManager.User(req.Username)
if err != nil && !errors.Is(err, user.ErrUserNotFound) {
@@ -60,7 +60,11 @@ func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visit
return err
}
}
if err := s.userManager.AddUser(req.Username, req.Password, user.RoleUser); err != nil {
password, hashed := req.Password, false
if req.Hash != "" {
password, hashed = req.Hash, true
}
if err := s.userManager.AddUser(req.Username, password, user.RoleUser, hashed); err != nil {
return err
}
if tier != nil {
@@ -71,6 +75,53 @@ func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visit
return s.writeJSON(w, newSuccessResponse())
}
func (s *Server) handleUsersUpdate(w http.ResponseWriter, r *http.Request, v *visitor) error {
req, err := readJSONWithLimit[apiUserAddOrUpdateRequest](r.Body, jsonBodyBytesLimit, false)
if err != nil {
return err
} else if !user.AllowedUsername(req.Username) {
return errHTTPBadRequest.Wrap("username invalid")
} else if req.Password == "" && req.Hash == "" && req.Tier == "" {
return errHTTPBadRequest.Wrap("need to provide at least one of \"password\", \"password_hash\" or \"tier\"")
}
u, err := s.userManager.User(req.Username)
if err != nil && !errors.Is(err, user.ErrUserNotFound) {
return err
} else if u != nil {
if u.IsAdmin() {
return errHTTPForbidden
}
if req.Hash != "" {
if err := s.userManager.ChangePassword(req.Username, req.Hash, true); err != nil {
return err
}
} else if req.Password != "" {
if err := s.userManager.ChangePassword(req.Username, req.Password, false); err != nil {
return err
}
}
} else {
password, hashed := req.Password, false
if req.Hash != "" {
password, hashed = req.Hash, true
}
if err := s.userManager.AddUser(req.Username, password, user.RoleUser, hashed); err != nil {
return err
}
}
if req.Tier != "" {
if _, err = s.userManager.Tier(req.Tier); errors.Is(err, user.ErrTierNotFound) {
return errHTTPBadRequestTierInvalid
} else if err != nil {
return err
}
if err := s.userManager.ChangeTier(req.Username, req.Tier); err != nil {
return err
}
}
return s.writeJSON(w, newSuccessResponse())
}
func (s *Server) handleUsersDelete(w http.ResponseWriter, r *http.Request, v *visitor) error {
req, err := readJSONWithLimit[apiUserDeleteRequest](r.Body, jsonBodyBytesLimit, false)
if err != nil {

View File

@@ -14,13 +14,13 @@ func TestUser_AddRemove(t *testing.T) {
defer s.closeDatabases()
// Create admin, tier
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
require.Nil(t, s.userManager.AddTier(&user.Tier{
Code: "tier1",
}))
// Create user via API
rr := request(t, s, "PUT", "/v1/users", `{"username": "ben", "password":"ben"}`, map[string]string{
rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password":"ben"}`, map[string]string{
"Authorization": util.BasicAuth("phil", "phil"),
})
require.Equal(t, 200, rr.Code)
@@ -49,6 +49,226 @@ func TestUser_AddRemove(t *testing.T) {
"Authorization": util.BasicAuth("phil", "phil"),
})
require.Equal(t, 200, rr.Code)
// Check user was deleted
users, err = s.userManager.Users()
require.Nil(t, err)
require.Equal(t, 3, len(users))
require.Equal(t, "phil", users[0].Name)
require.Equal(t, "emma", users[1].Name)
require.Equal(t, user.Everyone, users[2].Name)
// Reject invalid user change
rr = request(t, s, "PUT", "/v1/users", `{"username": "ben"}`, map[string]string{
"Authorization": util.BasicAuth("phil", "phil"),
})
require.Equal(t, 400, rr.Code)
}
func TestUser_AddWithPasswordHash(t *testing.T) {
s := newTestServer(t, newTestConfigWithAuthFile(t))
defer s.closeDatabases()
// Create admin
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
// Create user via API
rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "hash":"$2a$04$2aPIIqPXQU16OfkSUZH1XOzpu1gsPRKkrfVdFLgWQ.tqb.vtTCuVe"}`, map[string]string{
"Authorization": util.BasicAuth("phil", "phil"),
})
require.Equal(t, 200, rr.Code)
// Check that user can login with password
rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{
"Authorization": util.BasicAuth("ben", "ben"),
})
require.Equal(t, 200, rr.Code)
// Check users
users, err := s.userManager.Users()
require.Nil(t, err)
require.Equal(t, 3, len(users))
require.Equal(t, "phil", users[0].Name)
require.Equal(t, user.RoleAdmin, users[0].Role)
require.Equal(t, "ben", users[1].Name)
require.Equal(t, user.RoleUser, users[1].Role)
}
func TestUser_ChangeUserPassword(t *testing.T) {
s := newTestServer(t, newTestConfigWithAuthFile(t))
defer s.closeDatabases()
// Create admin
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
// Create user via API
rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password": "ben"}`, map[string]string{
"Authorization": util.BasicAuth("phil", "phil"),
})
require.Equal(t, 200, rr.Code)
// Try to login with first password
rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{
"Authorization": util.BasicAuth("ben", "ben"),
})
require.Equal(t, 200, rr.Code)
// Change password via API
rr = request(t, s, "PUT", "/v1/users", `{"username": "ben", "password": "ben-two"}`, map[string]string{
"Authorization": util.BasicAuth("phil", "phil"),
})
require.Equal(t, 200, rr.Code)
// Make sure first password fails
rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{
"Authorization": util.BasicAuth("ben", "ben"),
})
require.Equal(t, 401, rr.Code)
// Try to login with second password
rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{
"Authorization": util.BasicAuth("ben", "ben-two"),
})
require.Equal(t, 200, rr.Code)
}
func TestUser_ChangeUserTier(t *testing.T) {
s := newTestServer(t, newTestConfigWithAuthFile(t))
defer s.closeDatabases()
// Create admin, tier
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
require.Nil(t, s.userManager.AddTier(&user.Tier{
Code: "tier1",
}))
require.Nil(t, s.userManager.AddTier(&user.Tier{
Code: "tier2",
}))
// Create user with tier via API
rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password":"ben", "tier": "tier1"}`, map[string]string{
"Authorization": util.BasicAuth("phil", "phil"),
})
require.Equal(t, 200, rr.Code)
// Check users
users, err := s.userManager.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, user.RoleUser, users[1].Role)
require.Equal(t, "tier1", users[1].Tier.Code)
// Change user tier via API
rr = request(t, s, "PUT", "/v1/users", `{"username": "ben", "tier": "tier2"}`, map[string]string{
"Authorization": util.BasicAuth("phil", "phil"),
})
require.Equal(t, 200, rr.Code)
// Check users again
users, err = s.userManager.Users()
require.Nil(t, err)
require.Equal(t, "tier2", users[1].Tier.Code)
}
func TestUser_ChangeUserPasswordAndTier(t *testing.T) {
s := newTestServer(t, newTestConfigWithAuthFile(t))
defer s.closeDatabases()
// Create admin, tier
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
require.Nil(t, s.userManager.AddTier(&user.Tier{
Code: "tier1",
}))
require.Nil(t, s.userManager.AddTier(&user.Tier{
Code: "tier2",
}))
// Create user with tier via API
rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password":"ben", "tier": "tier1"}`, map[string]string{
"Authorization": util.BasicAuth("phil", "phil"),
})
require.Equal(t, 200, rr.Code)
// Check users
users, err := s.userManager.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, user.RoleUser, users[1].Role)
require.Equal(t, "tier1", users[1].Tier.Code)
// Change user password and tier via API
rr = request(t, s, "PUT", "/v1/users", `{"username": "ben", "password":"ben-two", "tier": "tier2"}`, map[string]string{
"Authorization": util.BasicAuth("phil", "phil"),
})
require.Equal(t, 200, rr.Code)
// Make sure first password fails
rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{
"Authorization": util.BasicAuth("ben", "ben"),
})
require.Equal(t, 401, rr.Code)
// Try to login with second password
rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{
"Authorization": util.BasicAuth("ben", "ben-two"),
})
require.Equal(t, 200, rr.Code)
// Check new tier
users, err = s.userManager.Users()
require.Nil(t, err)
require.Equal(t, "tier2", users[1].Tier.Code)
}
func TestUser_ChangeUserPasswordWithHash(t *testing.T) {
s := newTestServer(t, newTestConfigWithAuthFile(t))
defer s.closeDatabases()
// Create admin
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
// Create user with tier via API
rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password":"not-ben"}`, map[string]string{
"Authorization": util.BasicAuth("phil", "phil"),
})
require.Equal(t, 200, rr.Code)
// Try to login with first password
rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{
"Authorization": util.BasicAuth("ben", "not-ben"),
})
require.Equal(t, 200, rr.Code)
// Change user password and tier via API
rr = request(t, s, "PUT", "/v1/users", `{"username": "ben", "hash":"$2a$04$2aPIIqPXQU16OfkSUZH1XOzpu1gsPRKkrfVdFLgWQ.tqb.vtTCuVe"}`, map[string]string{
"Authorization": util.BasicAuth("phil", "phil"),
})
require.Equal(t, 200, rr.Code)
// Try to login with second password
rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{
"Authorization": util.BasicAuth("ben", "ben"),
})
require.Equal(t, 200, rr.Code)
}
func TestUser_DontChangeAdminPassword(t *testing.T) {
s := newTestServer(t, newTestConfigWithAuthFile(t))
defer s.closeDatabases()
// Create admin
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
require.Nil(t, s.userManager.AddUser("admin", "admin", user.RoleAdmin, false))
// Try to change password via API
rr := request(t, s, "PUT", "/v1/users", `{"username": "admin", "password": "admin-new"}`, map[string]string{
"Authorization": util.BasicAuth("phil", "phil"),
})
require.Equal(t, 403, rr.Code)
}
func TestUser_AddRemove_Failures(t *testing.T) {
@@ -56,23 +276,23 @@ func TestUser_AddRemove_Failures(t *testing.T) {
defer s.closeDatabases()
// Create admin
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false))
// Cannot create user with invalid username
rr := request(t, s, "PUT", "/v1/users", `{"username": "not valid", "password":"ben"}`, map[string]string{
rr := request(t, s, "POST", "/v1/users", `{"username": "not valid", "password":"ben"}`, map[string]string{
"Authorization": util.BasicAuth("phil", "phil"),
})
require.Equal(t, 400, rr.Code)
// Cannot create user if user already exists
rr = request(t, s, "PUT", "/v1/users", `{"username": "phil", "password":"phil"}`, map[string]string{
rr = request(t, s, "POST", "/v1/users", `{"username": "phil", "password":"phil"}`, map[string]string{
"Authorization": util.BasicAuth("phil", "phil"),
})
require.Equal(t, 40901, toHTTPError(t, rr.Body.String()).Code)
// Cannot create user with invalid tier
rr = request(t, s, "PUT", "/v1/users", `{"username": "emma", "password":"emma", "tier": "invalid"}`, map[string]string{
rr = request(t, s, "POST", "/v1/users", `{"username": "emma", "password":"emma", "tier": "invalid"}`, map[string]string{
"Authorization": util.BasicAuth("phil", "phil"),
})
require.Equal(t, 40030, toHTTPError(t, rr.Body.String()).Code)
@@ -97,8 +317,8 @@ func TestAccess_AllowReset(t *testing.T) {
defer s.closeDatabases()
// User and admin
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false))
// Subscribing not allowed
rr := request(t, s, "GET", "/gold/json?poll=1", "", map[string]string{
@@ -138,7 +358,7 @@ func TestAccess_AllowReset_NonAdminAttempt(t *testing.T) {
defer s.closeDatabases()
// User
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false))
// Grant access fails, because non-admin
rr := request(t, s, "POST", "/v1/users/access", `{"username": "ben", "topic":"gold", "permission":"ro"}`, map[string]string{
@@ -154,8 +374,8 @@ func TestAccess_AllowReset_KillConnection(t *testing.T) {
defer s.closeDatabases()
// User and admin, grant access to "gol*" topics
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false))
require.Nil(t, s.userManager.AllowAccess("ben", "gol*", user.PermissionRead)) // Wildcard!
start, timeTaken := time.Now(), atomic.Int64{}

View File

@@ -50,7 +50,7 @@ func (c *firebaseClient) Send(v *visitor, m *message) error {
ev.Field("firebase_message", util.MaybeMarshalJSON(fbm)).Trace("Firebase message")
}
err = c.sender.Send(fbm)
if err == errFirebaseQuotaExceeded {
if errors.Is(err, errFirebaseQuotaExceeded) {
logvm(v, m).
Tag(tagFirebase).
Err(err).
@@ -133,56 +133,55 @@ func toFirebaseMessage(m *message, auther user.Auther) (*messaging.Message, erro
"time": fmt.Sprintf("%d", m.Time),
"event": m.Event,
"topic": m.Topic,
"message": m.Message,
"message": newMessageBody,
"poll_id": m.PollID,
}
apnsConfig = createAPNSAlertConfig(m, data)
case messageEvent:
allowForward := true
if auther != nil {
allowForward = auther.Authorize(nil, m.Topic, user.PermissionRead) == nil
}
if allowForward {
data = map[string]string{
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
"event": m.Event,
"topic": m.Topic,
"priority": fmt.Sprintf("%d", m.Priority),
"tags": strings.Join(m.Tags, ","),
"click": m.Click,
"icon": m.Icon,
"title": m.Title,
"message": m.Message,
"content_type": m.ContentType,
"encoding": m.Encoding,
}
if len(m.Actions) > 0 {
actions, err := json.Marshal(m.Actions)
if err != nil {
return nil, err
}
data["actions"] = string(actions)
}
if m.Attachment != nil {
data["attachment_name"] = m.Attachment.Name
data["attachment_type"] = m.Attachment.Type
data["attachment_size"] = fmt.Sprintf("%d", m.Attachment.Size)
data["attachment_expires"] = fmt.Sprintf("%d", m.Attachment.Expires)
data["attachment_url"] = m.Attachment.URL
}
apnsConfig = createAPNSAlertConfig(m, data)
} else {
// If anonymous read for a topic is not allowed, we cannot send the message along
// If "anonymous read" for a topic is not allowed, we cannot send the message along
// via Firebase. Instead, we send a "poll_request" message, asking the client to poll.
data = map[string]string{
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
"event": pollRequestEvent,
"topic": m.Topic,
//
// The data map needs to contain all the fields for it to function properly. If not all
// fields are set, the iOS app fails to decode the message.
//
// See https://github.com/binwiederhier/ntfy/pull/1345
if err := auther.Authorize(nil, m.Topic, user.PermissionRead); err != nil {
m = toPollRequest(m)
}
// TODO Handle APNS?
}
data = map[string]string{
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
"event": m.Event,
"topic": m.Topic,
"priority": fmt.Sprintf("%d", m.Priority),
"tags": strings.Join(m.Tags, ","),
"click": m.Click,
"icon": m.Icon,
"title": m.Title,
"message": m.Message,
"content_type": m.ContentType,
"encoding": m.Encoding,
}
if len(m.Actions) > 0 {
actions, err := json.Marshal(m.Actions)
if err != nil {
return nil, err
}
data["actions"] = string(actions)
}
if m.Attachment != nil {
data["attachment_name"] = m.Attachment.Name
data["attachment_type"] = m.Attachment.Type
data["attachment_size"] = fmt.Sprintf("%d", m.Attachment.Size)
data["attachment_expires"] = fmt.Sprintf("%d", m.Attachment.Expires)
data["attachment_url"] = m.Attachment.URL
}
if m.PollID != "" {
data["poll_id"] = m.PollID
}
apnsConfig = createAPNSAlertConfig(m, data)
}
var androidConfig *messaging.AndroidConfig
if m.Priority >= 4 {
@@ -276,3 +275,17 @@ func maybeTruncateAPNSBodyMessage(s string) string {
}
return s
}
// toPollRequest converts a message to a poll request message.
//
// This empties all the fields that are not needed for a poll request and just sets the required fields,
// most importantly, the PollID.
func toPollRequest(m *message) *message {
pr := newPollRequestMessage(m.Topic, m.ID)
pr.ID = m.ID
pr.Time = m.Time
pr.Priority = m.Priority // Keep priority
pr.ContentType = m.ContentType
pr.Encoding = m.Encoding
return pr
}

View File

@@ -223,14 +223,25 @@ func TestToFirebaseMessage_Message_Normal_Not_Allowed(t *testing.T) {
require.Equal(t, &messaging.AndroidConfig{
Priority: "high",
}, fbm.Android)
require.Equal(t, "", fbm.Data["message"])
require.Equal(t, "", fbm.Data["priority"])
require.Equal(t, "New message", fbm.Data["message"])
require.Equal(t, "5", fbm.Data["priority"])
require.Equal(t, map[string]string{
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
"event": "poll_request",
"topic": "mytopic",
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
"event": "poll_request",
"topic": "mytopic",
"message": "New message",
"title": "",
"tags": "",
"click": "",
"icon": "",
"priority": "5",
"encoding": "",
"content_type": "",
"poll_id": m.ID,
}, fbm.Data)
require.Equal(t, "", fbm.APNS.Payload.Aps.Alert.Title)
require.Equal(t, "New message", fbm.APNS.Payload.Aps.Alert.Body)
}
func TestToFirebaseMessage_PollRequest(t *testing.T) {

View File

@@ -16,7 +16,7 @@ const (
func (s *Server) limitRequests(next handleFunc) handleFunc {
return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
if util.ContainsIP(s.config.VisitorRequestExemptIPAddrs, v.ip) {
if util.ContainsIP(s.config.VisitorRequestExemptPrefixes, v.ip) {
return next(w, r, v)
} else if !v.RequestAllowed() {
return errHTTPTooManyRequestsLimitRequests
@@ -40,7 +40,7 @@ func (s *Server) limitRequestsWithTopic(next handleFunc) handleFunc {
contextRateVisitor: vrate,
contextTopic: t,
})
if util.ContainsIP(s.config.VisitorRequestExemptIPAddrs, v.ip) {
if util.ContainsIP(s.config.VisitorRequestExemptPrefixes, v.ip) {
return next(w, r, v)
} else if !vrate.RequestAllowed() {
return errHTTPTooManyRequestsLimitRequests

View File

@@ -148,7 +148,7 @@ func TestPayments_SubscriptionCreate_NotAStripeCustomer_Success(t *testing.T) {
Code: "pro",
StripeMonthlyPriceID: "price_123",
}))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
// Create subscription
response := request(t, s, "POST", "/v1/account/billing/subscription", `{"tier": "pro", "interval": "month"}`, map[string]string{
@@ -184,7 +184,7 @@ func TestPayments_SubscriptionCreate_StripeCustomer_Success(t *testing.T) {
Code: "pro",
StripeMonthlyPriceID: "price_123",
}))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
u, err := s.userManager.User("phil")
require.Nil(t, err)
@@ -226,7 +226,7 @@ func TestPayments_AccountDelete_Cancels_Subscription(t *testing.T) {
Code: "pro",
StripeMonthlyPriceID: "price_123",
}))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
u, err := s.userManager.User("phil")
require.Nil(t, err)
@@ -280,7 +280,7 @@ func TestPayments_Checkout_Success_And_Increase_Rate_Limits_Reset_Visitor(t *tes
MessageLimit: 220, // 220 * 5% = 11 requests before rate limiting kicks in
MessageExpiryDuration: time.Hour,
}))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) // No tier
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) // No tier
u, err := s.userManager.User("phil")
require.Nil(t, err)
@@ -461,7 +461,7 @@ func TestPayments_Webhook_Subscription_Updated_Downgrade_From_PastDue_To_Active(
AttachmentTotalSizeLimit: 1000000,
AttachmentBandwidthLimit: 1000000,
}))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
require.Nil(t, s.userManager.AddReservation("phil", "atopic", user.PermissionDenyAll))
require.Nil(t, s.userManager.AddReservation("phil", "ztopic", user.PermissionDenyAll))
@@ -570,7 +570,7 @@ func TestPayments_Webhook_Subscription_Deleted(t *testing.T) {
StripeMonthlyPriceID: "price_1234",
ReservationLimit: 1,
}))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
require.Nil(t, s.userManager.AddReservation("phil", "atopic", user.PermissionDenyAll))
@@ -658,7 +658,7 @@ func TestPayments_Subscription_Update_Different_Tier(t *testing.T) {
StripeMonthlyPriceID: "price_456",
StripeYearlyPriceID: "price_457",
}))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
require.Nil(t, s.userManager.ChangeBilling("phil", &user.Billing{
StripeCustomerID: "acct_123",
@@ -690,7 +690,7 @@ func TestPayments_Subscription_Delete_At_Period_End(t *testing.T) {
Return(&stripe.Subscription{}, nil)
// Create user
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
require.Nil(t, s.userManager.ChangeBilling("phil", &user.Billing{
StripeCustomerID: "acct_123",
StripeSubscriptionID: "sub_123",
@@ -724,7 +724,7 @@ func TestPayments_CreatePortalSession(t *testing.T) {
}, nil)
// Create user
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
require.Nil(t, s.userManager.ChangeBilling("phil", &user.Billing{
StripeCustomerID: "acct_123",
StripeSubscriptionID: "sub_123",

File diff suppressed because one or more lines are too long

View File

@@ -63,7 +63,7 @@ func TestServer_Twilio_Call_Add_Verify_Call_Delete_Success(t *testing.T) {
MessageLimit: 10,
CallLimit: 1,
}))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
u, err := s.userManager.User("phil")
require.Nil(t, err)
@@ -140,7 +140,7 @@ func TestServer_Twilio_Call_Success(t *testing.T) {
MessageLimit: 10,
CallLimit: 1,
}))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
u, err := s.userManager.User("phil")
require.Nil(t, err)
@@ -185,7 +185,7 @@ func TestServer_Twilio_Call_Success_With_Yes(t *testing.T) {
MessageLimit: 10,
CallLimit: 1,
}))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
u, err := s.userManager.User("phil")
require.Nil(t, err)
@@ -216,7 +216,7 @@ func TestServer_Twilio_Call_UnverifiedNumber(t *testing.T) {
MessageLimit: 10,
CallLimit: 1,
}))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
// Do the thing

View File

@@ -96,7 +96,7 @@ func TestServer_WebPush_TopicSubscribeProtected_Allowed(t *testing.T) {
config.AuthDefault = user.PermissionDenyAll
s := newTestServer(t, config)
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false))
require.Nil(t, s.userManager.AllowAccess("ben", "test-topic", user.PermissionReadWrite))
response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), map[string]string{
@@ -126,7 +126,7 @@ func TestServer_WebPush_DeleteAccountUnsubscribe(t *testing.T) {
config := configureAuth(t, newTestConfigWithWebPush(t))
s := newTestServer(t, config)
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false))
require.Nil(t, s.userManager.AllowAccess("ben", "test-topic", user.PermissionReadWrite))
response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), map[string]string{
@@ -212,7 +212,7 @@ func TestServer_WebPush_Expiry(t *testing.T) {
addSubscription(t, s, pushService.URL+"/push-receive", "test-topic")
requireSubscriptionCount(t, s, "test-topic", 1)
_, err := s.webPush.db.Exec("UPDATE subscription SET updated_at = ?", time.Now().Add(-7*24*time.Hour).Unix())
_, err := s.webPush.db.Exec("UPDATE subscription SET updated_at = ?", time.Now().Add(-55*24*time.Hour).Unix())
require.Nil(t, err)
s.pruneAndNotifyWebPushSubscriptions()
@@ -222,7 +222,7 @@ func TestServer_WebPush_Expiry(t *testing.T) {
return received.Load()
})
_, err = s.webPush.db.Exec("UPDATE subscription SET updated_at = ?", time.Now().Add(-9*24*time.Hour).Unix())
_, err = s.webPush.db.Exec("UPDATE subscription SET updated_at = ?", time.Now().Add(-60*24*time.Hour).Unix())
require.Nil(t, err)
s.pruneAndNotifyWebPushSubscriptions()

View File

@@ -110,9 +110,11 @@ func formatMail(baseURL, senderIP, from, to string, m *message) (string, error)
if trailer != "" {
message += "\n\n" + trailer
}
date := time.Unix(m.Time, 0).UTC().Format(time.RFC1123Z)
subject = mime.BEncoding.Encode("utf-8", subject)
body := `From: "{shortTopicURL}" <{from}>
To: {to}
Date: {date}
Subject: {subject}
Content-Type: text/plain; charset="utf-8"
@@ -122,6 +124,7 @@ Content-Type: text/plain; charset="utf-8"
This message was sent by {ip} at {time} via {topicURL}`
body = strings.ReplaceAll(body, "{from}", from)
body = strings.ReplaceAll(body, "{to}", to)
body = strings.ReplaceAll(body, "{date}", date)
body = strings.ReplaceAll(body, "{subject}", subject)
body = strings.ReplaceAll(body, "{message}", message)
body = strings.ReplaceAll(body, "{topicURL}", topicURL)

View File

@@ -15,6 +15,7 @@ func TestFormatMail_Basic(t *testing.T) {
})
expected := `From: "ntfy.sh/alerts" <ntfy@ntfy.sh>
To: phil@example.com
Date: Fri, 24 Dec 2021 21:43:24 +0000
Subject: A simple message
Content-Type: text/plain; charset="utf-8"
@@ -36,6 +37,7 @@ func TestFormatMail_JustEmojis(t *testing.T) {
})
expected := `From: "ntfy.sh/alerts" <ntfy@ntfy.sh>
To: phil@example.com
Date: Fri, 24 Dec 2021 21:43:24 +0000
Subject: =?utf-8?b?8J+YgCBBIHNpbXBsZSBtZXNzYWdl?=
Content-Type: text/plain; charset="utf-8"
@@ -57,6 +59,7 @@ func TestFormatMail_JustOtherTags(t *testing.T) {
})
expected := `From: "ntfy.sh/alerts" <ntfy@ntfy.sh>
To: phil@example.com
Date: Fri, 24 Dec 2021 21:43:24 +0000
Subject: A simple message
Content-Type: text/plain; charset="utf-8"
@@ -80,6 +83,7 @@ func TestFormatMail_JustPriority(t *testing.T) {
})
expected := `From: "ntfy.sh/alerts" <ntfy@ntfy.sh>
To: phil@example.com
Date: Fri, 24 Dec 2021 21:43:24 +0000
Subject: A simple message
Content-Type: text/plain; charset="utf-8"
@@ -103,6 +107,7 @@ func TestFormatMail_UTF8Subject(t *testing.T) {
})
expected := `From: "ntfy.sh/alerts" <ntfy@ntfy.sh>
To: phil@example.com
Date: Fri, 24 Dec 2021 21:43:24 +0000
Subject: =?utf-8?b?IDo6IEEgbm90IHNvIHNpbXBsZSB0aXRsZSDDtsOkw7zDnyDCoUhvbGEsIHNl?= =?utf-8?b?w7FvciE=?=
Content-Type: text/plain; charset="utf-8"
@@ -126,6 +131,7 @@ func TestFormatMail_WithAllTheThings(t *testing.T) {
})
expected := `From: "ntfy.sh/alerts" <ntfy@ntfy.sh>
To: phil@example.com
Date: Fri, 24 Dec 2021 21:43:24 +0000
Subject: =?utf-8?b?4pqg77iPIPCfkoAgT2ggbm8g8J+ZiCBUaGlzIGlzIGEgbWVzc2FnZSBhY3Jv?= =?utf-8?b?c3MgbXVsdGlwbGUgbGluZXM=?=
Content-Type: text/plain; charset="utf-8"

View File

@@ -5,8 +5,6 @@ import (
"encoding/base64"
"errors"
"fmt"
"github.com/emersion/go-smtp"
"github.com/microcosm-cc/bluemonday"
"io"
"mime"
"mime/multipart"
@@ -18,6 +16,9 @@ import (
"regexp"
"strings"
"sync"
"github.com/emersion/go-smtp"
"github.com/microcosm-cc/bluemonday"
)
var (
@@ -70,15 +71,19 @@ func (b *smtpBackend) Counts() (total int64, success int64, failure int64) {
// smtpSession is returned after EHLO.
type smtpSession struct {
backend *smtpBackend
conn *smtp.Conn
topic string
token string
mu sync.Mutex
backend *smtpBackend
conn *smtp.Conn
topic string
token string // If email address contains token, e.g. topic+token@domain
basicAuth string // If SMTP AUTH PLAIN was used
mu sync.Mutex
}
func (s *smtpSession) AuthPlain(username, _ string) error {
func (s *smtpSession) AuthPlain(username, password string) error {
logem(s.conn).Field("smtp_username", username).Debug("AUTH PLAIN (with username %s)", username)
s.mu.Lock()
s.basicAuth = base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password)))
s.mu.Unlock()
return nil
}
@@ -187,9 +192,9 @@ func (s *smtpSession) publishMessage(m *message) error {
// Call HTTP handler with fake HTTP request
url := fmt.Sprintf("%s/%s", s.backend.config.BaseURL, m.Topic)
req, err := http.NewRequest("POST", url, strings.NewReader(m.Message))
req.RequestURI = "/" + m.Topic // just for the logs
req.RemoteAddr = remoteAddr // rate limiting!!
req.Header.Set("X-Forwarded-For", remoteAddr)
req.RequestURI = "/" + m.Topic // just for the logs
req.RemoteAddr = remoteAddr // rate limiting!!
req.Header.Set(s.backend.config.ProxyForwardedHeader, remoteAddr) // Set X-Forwarded-For header
if err != nil {
return err
}
@@ -198,6 +203,8 @@ func (s *smtpSession) publishMessage(m *message) error {
}
if s.token != "" {
req.Header.Add("Authorization", "Bearer "+s.token)
} else if s.basicAuth != "" {
req.Header.Add("Authorization", "Basic "+s.basicAuth)
}
rr := httptest.NewRecorder()
s.backend.handler(rr, req)
@@ -214,6 +221,9 @@ func (s *smtpSession) Reset() {
}
func (s *smtpSession) Logout() error {
s.mu.Lock()
s.basicAuth = ""
s.mu.Unlock()
return nil
}

View File

@@ -1386,6 +1386,28 @@ what's up
writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued")
}
func TestSmtpBackend_PlaintextWithPlainAuth(t *testing.T) {
email := `EHLO example.com
AUTH PLAIN dGVzdAB0ZXN0ADEyMzQ=
MAIL FROM: phil@example.com
RCPT TO: ntfy-mytopic@ntfy.sh
DATA
Subject: Very short mail
what's up
.
`
s, c, _, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/mytopic", r.URL.Path)
require.Equal(t, "Very short mail", r.Header.Get("Title"))
require.Equal(t, "Basic dGVzdDoxMjM0", r.Header.Get("Authorization"))
require.Equal(t, "what's up", readAll(t, r.Body))
})
defer s.Close()
defer c.Close()
writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued")
}
type smtpHandlerFunc func(http.ResponseWriter, *http.Request)
func newTestSMTPServer(t *testing.T, handler smtpHandlerFunc) (s *smtp.Server, c net.Conn, conf *Config, scanner *bufio.Scanner) {

View File

@@ -10,8 +10,6 @@ import (
)
func TestTopic_CancelSubscribersExceptUser(t *testing.T) {
t.Parallel()
subFn := func(v *visitor, msg *message) error {
return nil
}

View File

@@ -105,6 +105,8 @@ type publishMessage struct {
Filename string `json:"filename"`
Email string `json:"email"`
Call string `json:"call"`
Cache string `json:"cache"` // use string as it defaults to true (or use &bool instead)
Firebase string `json:"firebase"` // use string as it defaults to true (or use &bool instead)
Delay string `json:"delay"`
}
@@ -169,8 +171,12 @@ func (t sinceMarker) IsNone() bool {
return t == sinceNoMessages
}
func (t sinceMarker) IsLatest() bool {
return t == sinceLatestMessage
}
func (t sinceMarker) IsID() bool {
return t.id != ""
return t.id != "" && t.id != "latest"
}
func (t sinceMarker) Time() time.Time {
@@ -182,8 +188,9 @@ func (t sinceMarker) ID() string {
}
var (
sinceAllMessages = sinceMarker{time.Unix(0, 0), ""}
sinceNoMessages = sinceMarker{time.Unix(1, 0), ""}
sinceAllMessages = sinceMarker{time.Unix(0, 0), ""}
sinceNoMessages = sinceMarker{time.Unix(1, 0), ""}
sinceLatestMessage = sinceMarker{time.Unix(0, 0), "latest"}
)
type queryFilter struct {
@@ -248,9 +255,10 @@ type apiStatsResponse struct {
MessagesRate float64 `json:"messages_rate"` // Average number of messages per second
}
type apiUserAddRequest struct {
type apiUserAddOrUpdateRequest struct {
Username string `json:"username"`
Password string `json:"password"`
Hash string `json:"hash"`
Tier string `json:"tier"`
// Do not add 'role' here. We don't want to add admins via the API.
}

View File

@@ -4,18 +4,30 @@ import (
"context"
"errors"
"fmt"
"heckel.io/ntfy/v2/util"
"io"
"mime"
"net/http"
"net/netip"
"regexp"
"strings"
"heckel.io/ntfy/v2/util"
)
var (
mimeDecoder mime.WordDecoder
mimeDecoder mime.WordDecoder
// priorityHeaderIgnoreRegex matches specific patterns of the "Priority" header (RFC 9218), so that it can be ignored
priorityHeaderIgnoreRegex = regexp.MustCompile(`^u=\d,\s*(i|\d)$|^u=\d$`)
// forwardedHeaderRegex parses IPv4 and IPv6 addresses from the "Forwarded" header (RFC 7239)
// IPv6 addresses in Forwarded header are enclosed in square brackets. The port is optional.
//
// Examples:
// for="1.2.3.4"
// for="[2001:db8::1]"; for=1.2.3.4:8080, by=phil
// for="1.2.3.4:8080"
forwardedHeaderRegex = regexp.MustCompile(`(?i)\bfor="?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[[0-9a-f:]+])(?::\d+)?"?`)
)
func readBoolParam(r *http.Request, defaultValue bool, names ...string) bool {
@@ -34,15 +46,11 @@ func toBool(value string) bool {
return value == "1" || value == "yes" || value == "true"
}
func readCommaSeparatedParam(r *http.Request, names ...string) (params []string) {
paramStr := readParam(r, names...)
if paramStr != "" {
params = make([]string, 0)
for _, s := range util.SplitNoEmpty(paramStr, ",") {
params = append(params, strings.TrimSpace(s))
}
func readCommaSeparatedParam(r *http.Request, names ...string) []string {
if paramStr := readParam(r, names...); paramStr != "" {
return util.Map(util.SplitNoEmpty(paramStr, ","), strings.TrimSpace)
}
return params
return []string{}
}
func readParam(r *http.Request, names ...string) string {
@@ -73,34 +81,68 @@ func readQueryParam(r *http.Request, names ...string) string {
return ""
}
func extractIPAddress(r *http.Request, behindProxy bool) netip.Addr {
remoteAddr := r.RemoteAddr
addrPort, err := netip.ParseAddrPort(remoteAddr)
ip := addrPort.Addr()
// extractIPAddress extracts the IP address of the visitor from the request,
// either from the TCP socket or from a proxy header.
func extractIPAddress(r *http.Request, behindProxy bool, proxyForwardedHeader string, proxyTrustedPrefixes []netip.Prefix) netip.Addr {
if behindProxy && proxyForwardedHeader != "" {
if addr, err := extractIPAddressFromHeader(r, proxyForwardedHeader, proxyTrustedPrefixes); err == nil {
return addr
}
// Fall back to the remote address if the header is not found or invalid
}
addrPort, err := netip.ParseAddrPort(r.RemoteAddr)
if err != nil {
// This should not happen in real life; only in tests. So, using falling back to 0.0.0.0 if address unspecified
ip, err = netip.ParseAddr(remoteAddr)
if err != nil {
ip = netip.IPv4Unspecified()
if remoteAddr != "@" || !behindProxy { // RemoteAddr is @ when unix socket is used
logr(r).Err(err).Warn("unable to parse IP (%s), new visitor with unspecified IP (0.0.0.0) created", remoteAddr)
logr(r).Err(err).Warn("unable to parse IP (%s), new visitor with unspecified IP (0.0.0.0) created", r.RemoteAddr)
return netip.IPv4Unspecified()
}
return addrPort.Addr()
}
// extractIPAddressFromHeader extracts the last IP address from the specified header.
//
// It supports multiple formats:
// - single IP address
// - comma-separated list
// - RFC 7239-style list (Forwarded header)
//
// If there are multiple addresses, we first remove the trusted IP addresses from the list, and
// then take the right-most address in the list (as this is the one added by our proxy server).
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For for details.
func extractIPAddressFromHeader(r *http.Request, forwardedHeader string, trustedPrefixes []netip.Prefix) (netip.Addr, error) {
value := strings.TrimSpace(strings.ToLower(r.Header.Get(forwardedHeader)))
if value == "" {
return netip.IPv4Unspecified(), fmt.Errorf("no %s header found", forwardedHeader)
}
// Extract valid addresses
addrsStrs := util.Map(util.SplitNoEmpty(value, ","), strings.TrimSpace)
var validAddrs []netip.Addr
for _, addrStr := range addrsStrs {
// Handle Forwarded header with for="[IPv6]" or for="IPv4"
if m := forwardedHeaderRegex.FindStringSubmatch(addrStr); len(m) == 2 {
addrRaw := m[1]
if strings.HasPrefix(addrRaw, "[") && strings.HasSuffix(addrRaw, "]") {
addrRaw = addrRaw[1 : len(addrRaw)-1]
}
if addr, err := netip.ParseAddr(addrRaw); err == nil {
validAddrs = append(validAddrs, addr)
}
} else if addr, err := netip.ParseAddr(addrStr); err == nil {
validAddrs = append(validAddrs, addr)
}
}
// Filter out proxy addresses
clientAddrs := util.Filter(validAddrs, func(addr netip.Addr) bool {
for _, prefix := range trustedPrefixes {
if prefix.Contains(addr) {
return false // Address is in the trusted range, ignore it
}
}
return true
})
if len(clientAddrs) == 0 {
return netip.IPv4Unspecified(), fmt.Errorf("no client IP address found in %s header: %s", forwardedHeader, value)
}
if behindProxy && strings.TrimSpace(r.Header.Get("X-Forwarded-For")) != "" {
// X-Forwarded-For can contain multiple addresses (see #328). If we are behind a proxy,
// only the right-most address can be trusted (as this is the one added by our proxy server).
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For for details.
ips := util.SplitNoEmpty(r.Header.Get("X-Forwarded-For"), ",")
realIP, err := netip.ParseAddr(strings.TrimSpace(util.LastString(ips, remoteAddr)))
if err != nil {
logr(r).Err(err).Error("invalid IP address %s received in X-Forwarded-For header", ip)
// Fall back to regular remote address if X-Forwarded-For is damaged
} else {
ip = realIP
}
}
return ip
return clientAddrs[len(clientAddrs)-1], nil
}
func readJSONWithLimit[T any](r io.ReadCloser, limit int, allowEmpty bool) (*T, error) {
@@ -133,7 +175,7 @@ func fromContext[T any](r *http.Request, key contextKey) (T, error) {
// maybeDecodeHeader decodes the given header value if it is MIME encoded, e.g. "=?utf-8?q?Hello_World?=",
// or returns the original header value if it is not MIME encoded. It also calls maybeIgnoreSpecialHeader
// to ignore new HTTP "Priority" header.
// to ignore the new HTTP "Priority" header.
func maybeDecodeHeader(name, value string) string {
decoded, err := mimeDecoder.DecodeHeader(value)
if err != nil {
@@ -142,7 +184,7 @@ func maybeDecodeHeader(name, value string) string {
return maybeIgnoreSpecialHeader(name, decoded)
}
// maybeIgnoreSpecialHeader ignores new HTTP "Priority" header (see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-priority)
// maybeIgnoreSpecialHeader ignores the new HTTP "Priority" header (RFC 9218, see https://datatracker.ietf.org/doc/html/rfc9218)
//
// Cloudflare (and potentially other providers) add this to requests when forwarding to the backend (ntfy),
// so we just ignore it. If the "Priority" header is set to "u=*, i" or "u=*" (by Cloudflare), the header will be ignored.

View File

@@ -4,10 +4,13 @@ import (
"bytes"
"crypto/rand"
"fmt"
"github.com/stretchr/testify/require"
"heckel.io/ntfy/v2/user"
"net/http"
"net/netip"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestReadBoolParam(t *testing.T) {
@@ -88,3 +91,74 @@ func TestMaybeDecodeHeaders(t *testing.T) {
r.Header.Set("X-Priority", "5") // ntfy priority header
require.Equal(t, "5", readHeaderParam(r, "x-priority", "priority", "p"))
}
func TestExtractIPAddress(t *testing.T) {
r, _ := http.NewRequest("GET", "http://ntfy.sh/mytopic/json?since=all", nil)
r.RemoteAddr = "10.0.0.1:1234"
r.Header.Set("X-Forwarded-For", " 1.2.3.4 , 5.6.7.8")
r.Header.Set("X-Client-IP", "9.10.11.12")
r.Header.Set("X-Real-IP", "13.14.15.16, 1.1.1.1")
r.Header.Set("Forwarded", "for=17.18.19.20;by=proxy.example.com, by=2.2.2.2;for=1.1.1.1")
trustedProxies := []netip.Prefix{netip.MustParsePrefix("1.1.1.1/32")}
require.Equal(t, "5.6.7.8", extractIPAddress(r, true, "X-Forwarded-For", trustedProxies).String())
require.Equal(t, "9.10.11.12", extractIPAddress(r, true, "X-Client-IP", trustedProxies).String())
require.Equal(t, "13.14.15.16", extractIPAddress(r, true, "X-Real-IP", trustedProxies).String())
require.Equal(t, "17.18.19.20", extractIPAddress(r, true, "Forwarded", trustedProxies).String())
require.Equal(t, "10.0.0.1", extractIPAddress(r, false, "X-Forwarded-For", trustedProxies).String())
}
func TestExtractIPAddress_UnixSocket(t *testing.T) {
r, _ := http.NewRequest("GET", "http://ntfy.sh/mytopic/json?since=all", nil)
r.RemoteAddr = "@"
r.Header.Set("X-Forwarded-For", "1.2.3.4, 5.6.7.8, 1.1.1.1")
r.Header.Set("Forwarded", "by=bla.example.com;for=17.18.19.20")
trustedProxies := []netip.Prefix{netip.MustParsePrefix("1.1.1.1/32")}
require.Equal(t, "5.6.7.8", extractIPAddress(r, true, "X-Forwarded-For", trustedProxies).String())
require.Equal(t, "17.18.19.20", extractIPAddress(r, true, "Forwarded", trustedProxies).String())
require.Equal(t, "0.0.0.0", extractIPAddress(r, false, "X-Forwarded-For", trustedProxies).String())
}
func TestExtractIPAddress_MixedIPv4IPv6(t *testing.T) {
r, _ := http.NewRequest("GET", "http://ntfy.sh/mytopic/json?since=all", nil)
r.RemoteAddr = "[2001:db8:abcd::1]:1234"
r.Header.Set("X-Forwarded-For", "1.2.3.4, 2001:db8:abcd::2, 5.6.7.8")
trustedProxies := []netip.Prefix{netip.MustParsePrefix("1.2.3.0/24")}
require.Equal(t, "5.6.7.8", extractIPAddress(r, true, "X-Forwarded-For", trustedProxies).String())
}
func TestExtractIPAddress_TrustedIPv6Prefix(t *testing.T) {
r, _ := http.NewRequest("GET", "http://ntfy.sh/mytopic/json?since=all", nil)
r.RemoteAddr = "[2001:db8:abcd::1]:1234"
r.Header.Set("X-Forwarded-For", "2001:db8:aaaa::1, 2001:db8:aaaa::2, 2001:db8:abcd:2::3")
trustedProxies := []netip.Prefix{netip.MustParsePrefix("2001:db8:aaaa::/48")}
require.Equal(t, "2001:db8:abcd:2::3", extractIPAddress(r, true, "X-Forwarded-For", trustedProxies).String())
}
func TestVisitorID(t *testing.T) {
confWithDefaults := &Config{
VisitorPrefixBitsIPv4: 32,
VisitorPrefixBitsIPv6: 64,
}
confWithShortenedPrefixes := &Config{
VisitorPrefixBitsIPv4: 16,
VisitorPrefixBitsIPv6: 56,
}
userWithTier := &user.User{
ID: "u_123",
Tier: &user.Tier{},
}
require.Equal(t, "ip:1.2.3.4", visitorID(netip.MustParseAddr("1.2.3.4"), nil, confWithDefaults))
require.Equal(t, "ip:2a01:599:b26:2397::", visitorID(netip.MustParseAddr("2a01:599:b26:2397:dbe7:5aa2:95ce:1e83"), nil, confWithDefaults))
require.Equal(t, "ip:2001:db8:25:86::", visitorID(netip.MustParseAddr("2001:db8:25:86:1::1"), nil, confWithDefaults))
require.Equal(t, "ip:2001:db8:25:86::", visitorID(netip.MustParseAddr("2001:db8:25:86:2::1"), nil, confWithDefaults))
require.Equal(t, "user:u_123", visitorID(netip.MustParseAddr("1.2.3.4"), userWithTier, confWithDefaults))
require.Equal(t, "user:u_123", visitorID(netip.MustParseAddr("2a01:599:b26:2397:dbe7:5aa2:95ce:1e83"), userWithTier, confWithDefaults))
require.Equal(t, "ip:1.2.0.0", visitorID(netip.MustParseAddr("1.2.3.4"), nil, confWithShortenedPrefixes))
require.Equal(t, "ip:2a01:599:b26:2300::", visitorID(netip.MustParseAddr("2a01:599:b26:2397:dbe7:5aa2:95ce:1e83"), nil, confWithShortenedPrefixes))
}

View File

@@ -2,13 +2,13 @@ package server
import (
"fmt"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/user"
"net/netip"
"sync"
"time"
"golang.org/x/time/rate"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util"
)
@@ -151,7 +151,7 @@ func (v *visitor) Context() log.Context {
func (v *visitor) contextNoLock() log.Context {
info := v.infoLightNoLock()
fields := log.Context{
"visitor_id": visitorID(v.ip, v.user),
"visitor_id": visitorID(v.ip, v.user, v.config),
"visitor_ip": v.ip.String(),
"visitor_seen": util.FormatTime(v.seen),
"visitor_messages": info.Stats.Messages,
@@ -524,9 +524,15 @@ func dailyLimitToRate(limit int64) rate.Limit {
return rate.Limit(limit) * rate.Every(oneDay)
}
func visitorID(ip netip.Addr, u *user.User) string {
// visitorID returns a unique identifier for a visitor based on user or IP, using configurable prefix bits for IPv4/IPv6
func visitorID(ip netip.Addr, u *user.User, conf *Config) string {
if u != nil && u.Tier != nil {
return fmt.Sprintf("user:%s", u.ID)
}
if ip.Is4() {
ip = netip.PrefixFrom(ip, conf.VisitorPrefixBitsIPv4).Masked().Addr()
} else if ip.Is6() {
ip = netip.PrefixFrom(ip, conf.VisitorPrefixBitsIPv6).Masked().Addr()
}
return fmt.Sprintf("ip:%s", ip.String())
}

View File

@@ -79,8 +79,9 @@ const (
deleteWebPushSubscriptionByUserIDQuery = `DELETE FROM subscription WHERE user_id = ?`
deleteWebPushSubscriptionByAgeQuery = `DELETE FROM subscription WHERE updated_at <= ?` // Full table scan!
insertWebPushSubscriptionTopicQuery = `INSERT INTO subscription_topic (subscription_id, topic) VALUES (?, ?)`
deleteWebPushSubscriptionTopicAllQuery = `DELETE FROM subscription_topic WHERE subscription_id = ?`
insertWebPushSubscriptionTopicQuery = `INSERT INTO subscription_topic (subscription_id, topic) VALUES (?, ?)`
deleteWebPushSubscriptionTopicAllQuery = `DELETE FROM subscription_topic WHERE subscription_id = ?`
deleteWebPushSubscriptionTopicWithoutSubscription = `DELETE FROM subscription_topic WHERE subscription_id NOT IN (SELECT id FROM subscription)`
)
// Schema management queries
@@ -271,6 +272,10 @@ func (c *webPushStore) RemoveSubscriptionsByUserID(userID string) error {
// RemoveExpiredSubscriptions removes all subscriptions that have not been updated for a given time period
func (c *webPushStore) RemoveExpiredSubscriptions(expireAfter time.Duration) error {
_, err := c.db.Exec(deleteWebPushSubscriptionByAgeQuery, time.Now().Add(-expireAfter).Unix())
if err != nil {
return err
}
_, err = c.db.Exec(deleteWebPushSubscriptionTopicWithoutSubscription)
return err
}

View File

@@ -28,7 +28,7 @@ const (
userHardDeleteAfterDuration = 7 * 24 * time.Hour
tokenPrefix = "tk_"
tokenLength = 32
tokenMaxCount = 20 // Only keep this many tokens in the table per user
tokenMaxCount = 60 // Only keep this many tokens in the table per user
tag = "user_manager"
)
@@ -864,13 +864,19 @@ func (a *Manager) resolvePerms(base, perm Permission) error {
}
// AddUser adds a user with the given username, password and role
func (a *Manager) AddUser(username, password string, role Role) error {
func (a *Manager) AddUser(username, password string, role Role, hashed bool) error {
if !AllowedUsername(username) || !AllowedRole(role) {
return ErrInvalidArgument
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), a.bcryptCost)
if err != nil {
return err
var hash []byte
var err error = nil
if hashed {
hash = []byte(password)
} else {
hash, err = bcrypt.GenerateFromPassword([]byte(password), a.bcryptCost)
if err != nil {
return err
}
}
userID := util.RandomStringPrefix(userIDPrefix, userIDLength)
syncTopic, now := util.RandomStringPrefix(syncTopicPrefix, syncTopicLength), time.Now().Unix()
@@ -1192,10 +1198,17 @@ func (a *Manager) ReservationOwner(topic string) (string, error) {
}
// ChangePassword changes a user's password
func (a *Manager) ChangePassword(username, password string) error {
hash, err := bcrypt.GenerateFromPassword([]byte(password), a.bcryptCost)
if err != nil {
return err
func (a *Manager) ChangePassword(username, password string, hashed bool) error {
var hash []byte
var err error
if hashed {
hash = []byte(password)
} else {
hash, err = bcrypt.GenerateFromPassword([]byte(password), a.bcryptCost)
if err != nil {
return err
}
}
if _, err := a.db.Exec(updateUserPassQuery, hash, username); err != nil {
return err

View File

@@ -14,13 +14,13 @@ import (
"time"
)
const minBcryptTimingMillis = int64(50) // Ideally should be >100ms, but this should also run on a Raspberry Pi without massive resources
const minBcryptTimingMillis = int64(40) // Ideally should be >100ms, but this should also run on a Raspberry Pi without massive resources
func TestManager_FullScenario_Default_DenyAll(t *testing.T) {
a := newTestManagerFromFile(t, filepath.Join(t.TempDir(), "user.db"), "", PermissionDenyAll, DefaultUserPasswordBcryptCost, DefaultUserStatsQueueWriterInterval)
require.Nil(t, a.AddUser("phil", "phil", RoleAdmin))
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
require.Nil(t, a.AddUser("john", "john", RoleUser))
require.Nil(t, a.AddUser("phil", "phil", RoleAdmin, false))
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
require.Nil(t, a.AddUser("john", "john", RoleUser, false))
require.Nil(t, a.AllowAccess("ben", "mytopic", PermissionReadWrite))
require.Nil(t, a.AllowAccess("ben", "readme", PermissionRead))
require.Nil(t, a.AllowAccess("ben", "writeme", PermissionWrite))
@@ -134,7 +134,7 @@ func TestManager_Access_Order_LengthWriteRead(t *testing.T) {
// and longer ACL rules are prioritized as well.
a := newTestManagerFromFile(t, filepath.Join(t.TempDir(), "user.db"), "", PermissionDenyAll, DefaultUserPasswordBcryptCost, DefaultUserStatsQueueWriterInterval)
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
require.Nil(t, a.AllowAccess("ben", "test*", PermissionReadWrite))
require.Nil(t, a.AllowAccess("ben", "*", PermissionRead))
@@ -147,20 +147,20 @@ func TestManager_Access_Order_LengthWriteRead(t *testing.T) {
func TestManager_AddUser_Invalid(t *testing.T) {
a := newTestManager(t, PermissionDenyAll)
require.Equal(t, ErrInvalidArgument, a.AddUser(" invalid ", "pass", RoleAdmin))
require.Equal(t, ErrInvalidArgument, a.AddUser("validuser", "pass", "invalid-role"))
require.Equal(t, ErrInvalidArgument, a.AddUser(" invalid ", "pass", RoleAdmin, false))
require.Equal(t, ErrInvalidArgument, a.AddUser("validuser", "pass", "invalid-role", false))
}
func TestManager_AddUser_Timing(t *testing.T) {
a := newTestManagerFromFile(t, filepath.Join(t.TempDir(), "user.db"), "", PermissionDenyAll, DefaultUserPasswordBcryptCost, DefaultUserStatsQueueWriterInterval)
start := time.Now().UnixMilli()
require.Nil(t, a.AddUser("user", "pass", RoleAdmin))
require.Nil(t, a.AddUser("user", "pass", RoleAdmin, false))
require.GreaterOrEqual(t, time.Now().UnixMilli()-start, minBcryptTimingMillis)
}
func TestManager_AddUser_And_Query(t *testing.T) {
a := newTestManagerFromFile(t, filepath.Join(t.TempDir(), "user.db"), "", PermissionDenyAll, DefaultUserPasswordBcryptCost, DefaultUserStatsQueueWriterInterval)
require.Nil(t, a.AddUser("user", "pass", RoleAdmin))
require.Nil(t, a.AddUser("user", "pass", RoleAdmin, false))
require.Nil(t, a.ChangeBilling("user", &Billing{
StripeCustomerID: "acct_123",
StripeSubscriptionID: "sub_123",
@@ -187,7 +187,7 @@ func TestManager_MarkUserRemoved_RemoveDeletedUsers(t *testing.T) {
a := newTestManager(t, PermissionDenyAll)
// Create user, add reservations and token
require.Nil(t, a.AddUser("user", "pass", RoleAdmin))
require.Nil(t, a.AddUser("user", "pass", RoleAdmin, false))
require.Nil(t, a.AddReservation("user", "mytopic", PermissionRead))
u, err := a.User("user")
@@ -237,7 +237,7 @@ func TestManager_CreateToken_Only_Lower(t *testing.T) {
a := newTestManager(t, PermissionDenyAll)
// Create user, add reservations and token
require.Nil(t, a.AddUser("user", "pass", RoleAdmin))
require.Nil(t, a.AddUser("user", "pass", RoleAdmin, false))
u, err := a.User("user")
require.Nil(t, err)
@@ -248,8 +248,8 @@ func TestManager_CreateToken_Only_Lower(t *testing.T) {
func TestManager_UserManagement(t *testing.T) {
a := newTestManager(t, PermissionDenyAll)
require.Nil(t, a.AddUser("phil", "phil", RoleAdmin))
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
require.Nil(t, a.AddUser("phil", "phil", RoleAdmin, false))
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
require.Nil(t, a.AllowAccess("ben", "mytopic", PermissionReadWrite))
require.Nil(t, a.AllowAccess("ben", "readme", PermissionRead))
require.Nil(t, a.AllowAccess("ben", "writeme", PermissionWrite))
@@ -339,21 +339,31 @@ func TestManager_UserManagement(t *testing.T) {
func TestManager_ChangePassword(t *testing.T) {
a := newTestManager(t, PermissionDenyAll)
require.Nil(t, a.AddUser("phil", "phil", RoleAdmin))
require.Nil(t, a.AddUser("phil", "phil", RoleAdmin, false))
require.Nil(t, a.AddUser("jane", "$2b$10$OyqU72muEy7VMd1SAU2Iru5IbeSMgrtCGHu/fWLmxL1MwlijQXWbG", RoleUser, true))
_, err := a.Authenticate("phil", "phil")
require.Nil(t, err)
require.Nil(t, a.ChangePassword("phil", "newpass"))
_, err = a.Authenticate("jane", "jane")
require.Nil(t, err)
require.Nil(t, a.ChangePassword("phil", "newpass", false))
_, err = a.Authenticate("phil", "phil")
require.Equal(t, ErrUnauthenticated, err)
_, err = a.Authenticate("phil", "newpass")
require.Nil(t, err)
require.Nil(t, a.ChangePassword("jane", "$2b$10$CNaCW.q1R431urlbQ5Drh.zl48TiiOeJSmZgfcswkZiPbJGQ1ApSS", true))
_, err = a.Authenticate("jane", "jane")
require.Equal(t, ErrUnauthenticated, err)
_, err = a.Authenticate("jane", "newpass")
require.Nil(t, err)
}
func TestManager_ChangeRole(t *testing.T) {
a := newTestManager(t, PermissionDenyAll)
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
require.Nil(t, a.AllowAccess("ben", "mytopic", PermissionReadWrite))
require.Nil(t, a.AllowAccess("ben", "readme", PermissionRead))
@@ -378,8 +388,8 @@ func TestManager_ChangeRole(t *testing.T) {
func TestManager_Reservations(t *testing.T) {
a := newTestManager(t, PermissionDenyAll)
require.Nil(t, a.AddUser("phil", "phil", RoleUser))
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
require.Nil(t, a.AddUser("phil", "phil", RoleUser, false))
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
require.Nil(t, a.AddReservation("ben", "ztopic_", PermissionDenyAll))
require.Nil(t, a.AddReservation("ben", "readme", PermissionRead))
require.Nil(t, a.AllowAccess("ben", "something-else", PermissionRead))
@@ -460,7 +470,7 @@ func TestManager_ChangeRoleFromTierUserToAdmin(t *testing.T) {
AttachmentTotalSizeLimit: 524288000,
AttachmentExpiryDuration: 24 * time.Hour,
}))
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
require.Nil(t, a.ChangeTier("ben", "pro"))
require.Nil(t, a.AddReservation("ben", "mytopic", PermissionDenyAll))
@@ -507,7 +517,7 @@ func TestManager_ChangeRoleFromTierUserToAdmin(t *testing.T) {
func TestManager_Token_Valid(t *testing.T) {
a := newTestManager(t, PermissionDenyAll)
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
u, err := a.User("ben")
require.Nil(t, err)
@@ -551,7 +561,7 @@ func TestManager_Token_Valid(t *testing.T) {
func TestManager_Token_Invalid(t *testing.T) {
a := newTestManager(t, PermissionDenyAll)
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
u, err := a.AuthenticateToken(strings.Repeat("x", 32)) // 32 == token length
require.Nil(t, u)
@@ -570,7 +580,7 @@ func TestManager_Token_NotFound(t *testing.T) {
func TestManager_Token_Expire(t *testing.T) {
a := newTestManager(t, PermissionDenyAll)
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
u, err := a.User("ben")
require.Nil(t, err)
@@ -618,7 +628,7 @@ func TestManager_Token_Expire(t *testing.T) {
func TestManager_Token_Extend(t *testing.T) {
a := newTestManager(t, PermissionDenyAll)
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
// Try to extend token for user without token
u, err := a.User("ben")
@@ -647,8 +657,8 @@ func TestManager_Token_MaxCount_AutoDelete(t *testing.T) {
// Tests that tokens are automatically deleted when the maximum number of tokens is reached
a := newTestManager(t, PermissionDenyAll)
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
require.Nil(t, a.AddUser("phil", "phil", RoleUser))
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
require.Nil(t, a.AddUser("phil", "phil", RoleUser, false))
ben, err := a.User("ben")
require.Nil(t, err)
@@ -668,10 +678,10 @@ func TestManager_Token_MaxCount_AutoDelete(t *testing.T) {
require.NotEmpty(t, token.Value)
philTokens = append(philTokens, token.Value)
// Create 22 tokens for ben (only 20 allowed!)
// Create 62 tokens for ben (only 60 allowed!)
baseTime := time.Now().Add(24 * time.Hour)
benTokens := make([]string, 0)
for i := 0; i < 22; i++ { //
for i := 0; i < 62; i++ { //
token, err := a.CreateToken(ben.ID, "", time.Now().Add(72*time.Hour), netip.IPv4Unspecified())
require.Nil(t, err)
require.NotEmpty(t, token.Value)
@@ -690,7 +700,7 @@ func TestManager_Token_MaxCount_AutoDelete(t *testing.T) {
require.Equal(t, ErrUnauthenticated, err)
// Ben: The other tokens should still work
for i := 2; i < 22; i++ {
for i := 2; i < 62; i++ {
userWithToken, err := a.AuthenticateToken(benTokens[i])
require.Nil(t, err, "token[%d]=%s failed", i, benTokens[i])
require.Equal(t, "ben", userWithToken.Name)
@@ -710,7 +720,7 @@ func TestManager_Token_MaxCount_AutoDelete(t *testing.T) {
require.Nil(t, err)
require.True(t, rows.Next())
require.Nil(t, rows.Scan(&benCount))
require.Equal(t, 20, benCount)
require.Equal(t, 60, benCount)
var philCount int
rows, err = a.db.Query(`SELECT COUNT(*) FROM user_token WHERE user_id=?`, phil.ID)
@@ -723,7 +733,7 @@ func TestManager_Token_MaxCount_AutoDelete(t *testing.T) {
func TestManager_EnqueueStats_ResetStats(t *testing.T) {
a, err := NewManager(filepath.Join(t.TempDir(), "db"), "", PermissionReadWrite, bcrypt.MinCost, 1500*time.Millisecond)
require.Nil(t, err)
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
// Baseline: No messages or emails
u, err := a.User("ben")
@@ -765,7 +775,7 @@ func TestManager_EnqueueStats_ResetStats(t *testing.T) {
func TestManager_EnqueueTokenUpdate(t *testing.T) {
a, err := NewManager(filepath.Join(t.TempDir(), "db"), "", PermissionReadWrite, bcrypt.MinCost, 500*time.Millisecond)
require.Nil(t, err)
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
// Create user and token
u, err := a.User("ben")
@@ -798,7 +808,7 @@ func TestManager_EnqueueTokenUpdate(t *testing.T) {
func TestManager_ChangeSettings(t *testing.T) {
a, err := NewManager(filepath.Join(t.TempDir(), "db"), "", PermissionReadWrite, bcrypt.MinCost, 1500*time.Millisecond)
require.Nil(t, err)
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
// No settings
u, err := a.User("ben")
@@ -866,7 +876,7 @@ func TestManager_Tier_Create_Update_List_Delete(t *testing.T) {
AttachmentBandwidthLimit: 21474836480,
StripeMonthlyPriceID: "price_2",
}))
require.Nil(t, a.AddUser("phil", "phil", RoleUser))
require.Nil(t, a.AddUser("phil", "phil", RoleUser, false))
require.Nil(t, a.ChangeTier("phil", "pro"))
ti, err := a.Tier("pro")
@@ -981,7 +991,7 @@ func TestManager_Tier_Change_And_Reset(t *testing.T) {
Name: "Pro",
ReservationLimit: 4,
}))
require.Nil(t, a.AddUser("phil", "phil", RoleUser))
require.Nil(t, a.AddUser("phil", "phil", RoleUser, false))
require.Nil(t, a.ChangeTier("phil", "pro"))
// Add 10 reservations (pro tier allows that)
@@ -1007,7 +1017,7 @@ func TestManager_Tier_Change_And_Reset(t *testing.T) {
func TestUser_PhoneNumberAddListRemove(t *testing.T) {
a := newTestManager(t, PermissionDenyAll)
require.Nil(t, a.AddUser("phil", "phil", RoleUser))
require.Nil(t, a.AddUser("phil", "phil", RoleUser, false))
phil, err := a.User("phil")
require.Nil(t, err)
require.Nil(t, a.AddPhoneNumber(phil.ID, "+1234567890"))
@@ -1032,8 +1042,8 @@ func TestUser_PhoneNumberAddListRemove(t *testing.T) {
func TestUser_PhoneNumberAdd_Multiple_Users_Same_Number(t *testing.T) {
a := newTestManager(t, PermissionDenyAll)
require.Nil(t, a.AddUser("phil", "phil", RoleUser))
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
require.Nil(t, a.AddUser("phil", "phil", RoleUser, false))
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
phil, err := a.User("phil")
require.Nil(t, err)
ben, err := a.User("ben")

View File

@@ -241,7 +241,7 @@ const (
)
var (
allowedUsernameRegex = regexp.MustCompile(`^[-_.@a-zA-Z0-9]+$`) // Does not include Everyone (*)
allowedUsernameRegex = regexp.MustCompile(`^[-_.+@a-zA-Z0-9]+$`) // Does not include Everyone (*)
allowedTopicRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`) // No '*'
allowedTopicPatternRegex = regexp.MustCompile(`^[-_*A-Za-z0-9]{1,64}$`) // Adds '*' for wildcards!
allowedTierRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`)

View File

@@ -61,3 +61,15 @@ func TestTierContext(t *testing.T) {
require.Equal(t, "price_456", context["stripe_yearly_price_id"])
}
func TestUsernameRegex(t *testing.T) {
username := "phil"
usernameEmail := "phil@ntfy.sh"
usernameEmailAlias := "phil+alias@ntfy.sh"
usernameInvalid := "phil\rocks"
require.True(t, AllowedUsername(username))
require.True(t, AllowedUsername(usernameEmail))
require.True(t, AllowedUsername(usernameEmailAlias))
require.False(t, AllowedUsername(usernameInvalid))
}

34
util/timeout_writer.go Normal file
View File

@@ -0,0 +1,34 @@
package util
import (
"errors"
"io"
"time"
)
// ErrWriteTimeout is returned when a write timed out
var ErrWriteTimeout = errors.New("write operation failed due to timeout since creation")
// TimeoutWriter wraps an io.Writer that will time out after the given timeout
type TimeoutWriter struct {
writer io.Writer
timeout time.Duration
start time.Time
}
// NewTimeoutWriter creates a new TimeoutWriter
func NewTimeoutWriter(w io.Writer, timeout time.Duration) *TimeoutWriter {
return &TimeoutWriter{
writer: w,
timeout: timeout,
start: time.Now(),
}
}
// Write implements the io.Writer interface, failing if called after the timeout period from creation.
func (tw *TimeoutWriter) Write(p []byte) (n int, err error) {
if time.Since(tw.start) > tw.timeout {
return 0, errors.New("write operation failed due to timeout since creation")
}
return tw.writer.Write(p)
}

View File

@@ -17,10 +17,9 @@ import (
"sync"
"time"
"golang.org/x/time/rate"
"github.com/gabriel-vasile/mimetype"
"golang.org/x/term"
"golang.org/x/time/rate"
)
const (
@@ -99,12 +98,26 @@ func SplitKV(s string, sep string) (key string, value string) {
return "", strings.TrimSpace(kv[0])
}
// LastString returns the last string in a slice, or def if s is empty
func LastString(s []string, def string) string {
if len(s) == 0 {
return def
// Map applies a function to each element of a slice and returns a new slice with the results
// Example: Map([]int{1, 2, 3}, func(i int) int { return i * 2 }) -> []int{2, 4, 6}
func Map[T any, U any](slice []T, f func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = f(v)
}
return s[len(s)-1]
return result
}
// Filter returns a new slice containing only the elements of the original slice for which the
// given function returns true.
func Filter[T any](slice []T, f func(T) bool) []T {
result := make([]T, 0)
for _, v := range slice {
if f(v) {
result = append(result, v)
}
}
return result
}
// RandomString returns a random string with a given length

View File

@@ -167,11 +167,6 @@ func TestSplitKV(t *testing.T) {
require.Equal(t, "value=with=separator", value)
}
func TestLastString(t *testing.T) {
require.Equal(t, "last", LastString([]string{"first", "second", "last"}, "default"))
require.Equal(t, "default", LastString([]string{}, "default"))
}
func TestQuoteCommand(t *testing.T) {
require.Equal(t, `ls -al "Document Folder"`, QuoteCommand([]string{"ls", "-al", "Document Folder"}))
require.Equal(t, `rsync -av /home/phil/ root@example.com:/home/phil/`, QuoteCommand([]string{"rsync", "-av", "/home/phil/", "root@example.com:/home/phil/"}))

6177
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -44,8 +44,8 @@
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"prettier": "^2.8.8",
"vite": "^4.3.9",
"vite-plugin-pwa": "^0.15.0"
"vite": "^6.3.5",
"vite-plugin-pwa": "^1.0.0"
},
"browserslist": {
"production": [

View File

@@ -330,5 +330,35 @@
"account_basics_tier_paid_until": "تم دفع مبلغ الاشتراك إلى غاية {{date}}، وسيتم تجديده تِلْقائيًا",
"account_basics_tier_canceled_subscription": "تم إلغاء اشتراكك وسيتم إعادته إلى مستوى حساب مجاني بداية مِن {{date}}.",
"account_delete_dialog_billing_warning": "إلغاء حسابك أيضاً يلغي اشتراكك في الفوترة فوراً ولن تتمكن من الوصول إلى لوح الفوترة بعد الآن.",
"nav_upgrade_banner_description": "حجز المواضيع والمزيد من الرسائل ورسائل البريد الإلكتروني والمرفقات الأكبر حجمًا"
"nav_upgrade_banner_description": "حجز المواضيع والمزيد من الرسائل ورسائل البريد الإلكتروني والمرفقات الأكبر حجمًا",
"prefs_appearance_theme_dark": "الوضع الليلي",
"prefs_appearance_theme_light": "الوضع النهاري",
"publish_dialog_checkbox_markdown": "تنسيق على هيئة ماركداون",
"alert_not_supported_context_description": "الإشعارات مسموحة فقط على بروتوكول HTTPS المأمن, هذه القيود <mdnLink>خصائص الإشعارات</mdnLink>",
"publish_dialog_call_reset": "حذف اتصال بالهاتف",
"publish_dialog_call_label": "اتصال هاتفي",
"publish_dialog_chip_call_label": "اتصال هاتفي",
"publish_dialog_delay_placeholder": "تأخير التوصيل, مثال {{unixTimestamp}}, {{relativeTime}}, او \"{{naturalLanguage}}\" (اللغة الإنجليزية فقط)",
"publish_dialog_attachment_limits_file_and_quota_reached": "تجاوز حجم {{fileSizeLimit}} الملف, {{remainingBytes}} متبقي",
"prefs_reservations_dialog_title_delete": "حذف حجز موضوع",
"publish_dialog_call_item": "اتصل برقم الهاتف {{number}}",
"publish_dialog_chip_call_no_verified_numbers_tooltip": "لا يوجد ارقام هواتف معرفة",
"action_bar_mute_notifications": "كتم الإشعارات",
"action_bar_unmute_notifications": "إلغاء كتم الإشعارات",
"alert_notification_ios_install_required_description": "اضغط على زر المشاركة ثم إضافة إلى الصفحة الرئيسية لتستقبل الإشعارات على أجهزة أبل",
"alert_notification_ios_install_required_title": "يجب تثبيت الصفحة",
"alert_notification_permission_denied_description": "الرجاء اعادة منح الصلاحيات في المتصفح",
"alert_notification_permission_denied_title": "الإشعارات مغلقة",
"notifications_actions_failed_notification": "حدث غير منفذ",
"prefs_notifications_web_push_disabled": "ملغي",
"account_basics_phone_numbers_dialog_channel_call": "اتصل",
"account_basics_phone_numbers_title": "أرقام الهواتف",
"account_basics_phone_numbers_dialog_channel_sms": "رسالة نصية قصيرة",
"account_basics_phone_numbers_dialog_check_verification_button": "رمز التأكيد",
"account_basics_phone_numbers_dialog_number_label": "رقم الهاتف",
"account_basics_phone_numbers_dialog_verify_button_call": "اتصل بي",
"account_basics_phone_numbers_dialog_code_label": "رمز التحقّق",
"account_upgrade_dialog_tier_price_per_month": "شهر",
"prefs_appearance_theme_title": "الحُلّة",
"subscribe_dialog_subscribe_use_another_background_info": "لن يتم استلام الاشعارات من الخوادم الخارجية عندما يكون تطبيق الويب مغلقاً"
}

View File

@@ -1,9 +1,9 @@
{
"action_bar_clear_notifications": "Премахване на известия",
"alert_notification_permission_required_description": "Разрешете на мрежовия четец да показва известия.",
"alert_notification_permission_required_description": "Разрешете на мрежовия четец да показва известия",
"notifications_attachment_copy_url_title": "Копиране на адреса на прикачения файл",
"notifications_example": "Пример",
"notifications_no_subscriptions_title": "Липсват абонаменти.",
"notifications_no_subscriptions_title": "Липсват абонаменти",
"nav_topics_title": "Абонаменти",
"action_bar_send_test_notification": "Пробно известие",
"action_bar_unsubscribe": "Отписване",
@@ -22,7 +22,7 @@
"publish_dialog_chip_email_label": "Препращане към ел. поща",
"publish_dialog_chip_attach_url_label": "Прикачване на файл от адрес",
"publish_dialog_chip_attach_file_label": "Прикачване местен файл",
"publish_dialog_chip_delay_label": "Забавяне на изпращането",
"publish_dialog_chip_delay_label": "Отлагане на изпращането",
"publish_dialog_chip_topic_label": "Промяна на темата",
"publish_dialog_button_cancel_sending": "Отменяне на изпращането",
"publish_dialog_button_cancel": "Отказ",
@@ -39,7 +39,7 @@
"prefs_notifications_delete_after_never": "Никога",
"prefs_users_add_button": "Добавяне",
"prefs_users_dialog_password_label": "Парола",
"alert_not_supported_description": "Мрежовият четец не поддържа известия.",
"alert_not_supported_description": "Мрежовият четец не поддържа известия",
"message_bar_type_message": "Въведете съобщение",
"message_bar_error_publishing": "Грешка при изпращане на известието",
"notifications_copied_to_clipboard": "Копирано в междинната памет",
@@ -61,10 +61,10 @@
"notifications_click_open_button": "Отваряне",
"notifications_click_copy_url_title": "Копиране на препратката в междинната памет",
"notifications_none_for_topic_title": "Темата е все още празна",
"notifications_none_for_any_title": "Липсват известия.",
"notifications_none_for_any_title": "Липсват известия",
"notifications_none_for_topic_description": "За да изпратите известия в тази тема направете заявка чрез методите PUT или POST към адреса ѝ.",
"notifications_none_for_any_description": "За да изпратите известия в тема направете заявка чрез методите PUT или POST към адреса ѝ. Ето пример с една от вашите теми.",
"notifications_no_subscriptions_description": "Щракнете върху „{{linktext}}“, за да създадете тема или да се абонирате. След това като направите заявка чрез методите PUT или POST ще ги получите тук.",
"notifications_no_subscriptions_description": "Щракнете върху „{{linktext}}“, за да създадете или да се абонирате за тема. След това като изпратите съобщение с методите PUT или POST ще го получите тук.",
"notifications_more_details": "За допълнителна информация посетете <websiteLink>страницата</websiteLink> или <docsLink>документацията</docsLink>.",
"publish_dialog_priority_min": "Най-нисък приоритет",
"publish_dialog_attachment_limits_file_reached": "надвишава ограничението от {{fileSizeLimit}} за размер на файл",
@@ -84,14 +84,14 @@
"publish_dialog_topic_label": "Име на темата",
"publish_dialog_title_label": "Заглавие",
"publish_dialog_priority_label": "Приоритет",
"publish_dialog_click_placeholder": "Адрес, който се отваря при щракване върху известието",
"publish_dialog_click_placeholder": "Адрес, който се отваря при докосване на известието",
"publish_dialog_email_placeholder": "Адрес, към който да бъдат препращани известия, напр. phil@example.com",
"publish_dialog_attach_label": "Адрес на прикачения файл",
"publish_dialog_filename_placeholder": "Име на прикачения файл",
"publish_dialog_attach_placeholder": "Прикачете файл от адрес, напр. https://f-droid.org/F-Droid.apk",
"prefs_notifications_delete_after_three_hours": "След три часа",
"publish_dialog_filename_label": "Име на файла",
"publish_dialog_delay_label": "Забавяне",
"publish_dialog_delay_label": "Отлагане",
"publish_dialog_details_examples_description": "За примери и подробно описание на всички възможности при изпращане, вижте <docsLink>документацията</docsLink>.",
"publish_dialog_button_send": "Изпращане",
"publish_dialog_checkbox_publish_another": "Изпращане на повече",
@@ -121,7 +121,7 @@
"subscribe_dialog_login_button_login": "Вход",
"subscribe_dialog_error_user_not_authorized": "Потребителят {{username}} няма достъп",
"prefs_appearance_title": "Външен вид",
"publish_dialog_delay_placeholder": "Забавяне на изпращането, {{unixTimestamp}}, {{relativeTime}} или „{{naturalLanguage}}“ (на английски)",
"publish_dialog_delay_placeholder": "Отлагане на изпращането, {{unixTimestamp}}, {{relativeTime}} или „{{naturalLanguage}}“ (на английски)",
"prefs_notifications_delete_after_one_week": "След една седмица",
"prefs_users_title": "Управление на потребители",
"prefs_users_table_base_url_header": "Адрес на услугата",
@@ -177,7 +177,7 @@
"publish_dialog_topic_reset": "Нулиране на тема",
"publish_dialog_click_reset": "Премахване на адрес",
"publish_dialog_email_reset": "Премахване на препращането към ел. поща",
"publish_dialog_delay_reset": "Премахва забавянето на изпращането",
"publish_dialog_delay_reset": "Премахва отлагането на изпращането",
"publish_dialog_attached_file_remove": "Премахване на прикачения файл",
"emoji_picker_search_clear": "Изчистване на търсенето",
"subscribe_dialog_subscribe_base_url_label": "Адрес на услугата",
@@ -212,7 +212,7 @@
"nav_upgrade_banner_label": "Надграждане до ntfy Pro",
"signup_form_confirm_password": "Парола отново",
"signup_disabled": "Регистрациите са затворени",
"signup_error_creation_limit_reached": "Достигнатео е ограничението за създаване на профили",
"signup_error_creation_limit_reached": "Достигнато е ограничението за създаване на профили",
"display_name_dialog_title": "Промяна на показваното име",
"action_bar_reservation_edit": "Промяна на резервацията",
"action_bar_sign_up": "Регистриране",
@@ -220,7 +220,7 @@
"alert_not_supported_context_description": "Известията се поддържат само през HTTPS. Това е ограничение на <mdnLink>Notifications API</mdnLink>.",
"display_name_dialog_description": "Изберете друго име за темата, което да се показва в списъка с абонаменти. Помага за по-лесното разпознаване на теми със сложни имена.",
"subscribe_dialog_error_topic_already_reserved": "Темата вече е резервирана",
"nav_upgrade_banner_description": "Резервиране на теми, повече съобщения и имейли и по-големи прикачени файлове",
"nav_upgrade_banner_description": "Резервиране на теми, повече съобщения и писма, по-големи прикачени файлове",
"display_name_dialog_placeholder": "Наименование",
"reserve_dialog_checkbox_label": "Резервиране на тема и настройки за достъп",
"subscribe_dialog_subscribe_button_generate_topic_name": "Произволно име",
@@ -380,5 +380,28 @@
"reservation_delete_dialog_action_delete_title": "Премахване на съобщения и прикачени файлове",
"reservation_delete_dialog_action_delete_description": "Съобщенията и прикачените файлове, които са във временната памет ще бъдат премахнати. Действието е необратимо.",
"prefs_reservations_description": "Тук можете да резервирате тема за собствено ползване. Резервирането ви осигурява собственост върху темата и ви дава възможност да определяте права за достъп от други потребители.",
"reservation_delete_dialog_description": "С премахването на резервирането вие се отказвате от собствеността върху темата и давате възможност друг потребител да я резервира. Можете да оставите или да премахнете съществуващите съобщения и прикачени файлове."
"reservation_delete_dialog_description": "С премахването на резервирането вие се отказвате от собствеността върху темата и давате възможност друг потребител да я резервира. Можете да оставите или да премахнете съществуващите съобщения и прикачени файлове.",
"alert_notification_permission_denied_description": "Включете ги от мрежовия четец",
"alert_notification_permission_denied_title": "Известията са изключени",
"notifications_actions_failed_notification": "Действието е неуспешно",
"publish_dialog_checkbox_markdown": "Съобщението е Markdown",
"prefs_notifications_web_push_disabled_description": "Известията ще бъдат получавани докато приложението за уеб работи (чрез WebSocket)",
"prefs_notifications_web_push_enabled": "Включено за {{server}}",
"prefs_notifications_web_push_disabled": "Изключено",
"prefs_appearance_theme_dark": "Тъмна",
"prefs_appearance_theme_light": "Светла",
"error_boundary_button_reload_ntfy": "Презареждне на ntfy",
"web_push_unknown_notification_title": "Получено е неочаквано известие",
"web_push_unknown_notification_body": "Вероятно ще трябва да обновите ntfy като отворите приложението за уеб",
"alert_notification_ios_install_required_title": "Необходимо е инсталиране за iOS",
"alert_notification_ios_install_required_description": "Докоснете бутона Споделяне и Добавяне към началния екран, за да включите известията под iOS",
"subscribe_dialog_subscribe_use_another_background_info": "Известията от други сървъри няма да бъдат получавани ако приложението за уеб не е отворено",
"action_bar_mute_notifications": "Заглушаване на известия",
"prefs_notifications_web_push_title": "Известия във фонов режим",
"prefs_notifications_web_push_enabled_description": "Известията ще бъдат получавани даже и ако приложението за уеб не работи (чрез Web Push)",
"prefs_appearance_theme_title": "Цветова тема",
"prefs_appearance_theme_system": "Системна (подразбирана)",
"web_push_subscription_expiring_title": "Известията временно ще бъдат спрени",
"web_push_subscription_expiring_body": "За да продължите да получавате известия, отворете ntfy",
"action_bar_unmute_notifications": "Включване звука на известията"
}

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,7 @@
{
"nav_button_documentation": "Documentació",
"action_bar_profile_title": "Perfil",
"action_bar_settings": "Configuració",
"action_bar_account": "Compte",
"common_add": "Afegir"
}

View File

@@ -15,7 +15,7 @@
"alert_notification_permission_required_description": "Udělte prohlížeči oprávnění k zobrazování oznámení na ploše.",
"alert_notification_permission_required_button": "Udělit nyní",
"alert_not_supported_title": "Oznámení nejsou podporována",
"alert_not_supported_description": "Oznámení nejsou ve vašem prohlížeči podporována.",
"alert_not_supported_description": "Oznámení nejsou ve vašem prohlížeči podporována",
"notifications_copied_to_clipboard": "Zkopírováno do schránky",
"notifications_tags": "Značky",
"notifications_attachment_copy_url_title": "Kopírovat URL přílohy do schránky",
@@ -380,5 +380,28 @@
"account_usage_calls_title": "Uskutečněné telefonáty",
"account_upgrade_dialog_tier_features_no_calls": "Žádné telefonní hovory",
"account_upgrade_dialog_tier_features_calls_one": "{{calls}} denní telefonní hovor",
"account_upgrade_dialog_tier_features_calls_other": "{{calls}} denních telefonních hovorů"
"account_upgrade_dialog_tier_features_calls_other": "{{calls}} denních telefonních hovorů",
"prefs_notifications_web_push_enabled": "Povoleno pro {{server}}",
"error_boundary_button_reload_ntfy": "Znovu načíst ntfy",
"web_push_subscription_expiring_body": "Otevřete ntfy a pokračujte v přijímání oznámení",
"action_bar_mute_notifications": "Ztlumit oznámení",
"action_bar_unmute_notifications": "Zrušit ztlumení oznámení",
"alert_notification_permission_denied_title": "Oznámení jsou blokována",
"alert_notification_permission_denied_description": "Prosím, znovu je povolte ve svém prohlížeči",
"alert_notification_ios_install_required_title": "Je vyžadována instalace iOS",
"alert_notification_ios_install_required_description": "Kliknutím na ikonu Sdílet a Přidat na domovskou obrazovku povolíte oznámení v systému iOS",
"notifications_actions_failed_notification": "Neúspěšná akce",
"publish_dialog_checkbox_markdown": "Formátovat jako Markdown",
"subscribe_dialog_subscribe_use_another_background_info": "Oznámení z jiných serverů nebudou přijímána, pokud není otevřena webová aplikace",
"prefs_notifications_web_push_title": "Oznámení na pozadí",
"prefs_notifications_web_push_enabled_description": "Oznámení jsou přijímána, i když webová aplikace není spuštěna (prostřednictvím Web Push)",
"prefs_notifications_web_push_disabled_description": "Oznámení jsou přijímána, když je webová aplikace spuštěna (přes WebSocket)",
"prefs_notifications_web_push_disabled": "Zakázáno",
"prefs_appearance_theme_title": "Motiv",
"prefs_appearance_theme_system": "Systém (výchozí)",
"prefs_appearance_theme_dark": "Tmavý režim",
"prefs_appearance_theme_light": "Světlý režim",
"web_push_subscription_expiring_title": "Oznámení budou pozastavena",
"web_push_unknown_notification_title": "Neznámé oznámení přijaté ze serveru",
"web_push_unknown_notification_body": "Možná bude nutné aktualizovat ntfy otevřením webové aplikace"
}

View File

@@ -31,12 +31,12 @@
"notifications_attachment_open_title": "Gehe zu {{url}}",
"notifications_none_for_any_title": "Du hast keine Benachrichtigungen empfangen.",
"action_bar_send_test_notification": "Test-Benachrichtigung senden",
"alert_notification_permission_required_description": "Dem Browser erlauben, Desktop-Benachrichtigungen anzuzeigen.",
"alert_notification_permission_required_description": "Browser erlauben, Desktop-Benachrichtigungen anzuzeigen",
"notifications_tags": "Tags",
"message_bar_type_message": "Gib hier eine Nachricht ein",
"message_bar_error_publishing": "Fehler beim Senden der Benachrichtigung",
"alert_not_supported_title": "Benachrichtigungen werden nicht unterstützt",
"alert_not_supported_description": "Benachrichtigungen werden von Deinem Browser nicht unterstützt.",
"alert_not_supported_description": "Benachrichtigungen werden von deinem Browser nicht unterstützt",
"action_bar_settings": "Einstellungen",
"action_bar_clear_notifications": "Alle Benachrichtigungen löschen",
"alert_notification_permission_required_button": "Jetzt erlauben",
@@ -208,11 +208,11 @@
"action_bar_change_display_name": "Anzeigenamen ändern",
"action_bar_reservation_add": "Thema reservieren",
"action_bar_reservation_edit": "Reservierung ändern",
"action_bar_reservation_delete": "Reservierung löschen",
"action_bar_reservation_delete": "Reservierung entfernen",
"action_bar_reservation_limit_reached": "Grenze erreicht",
"action_bar_profile_title": "Profil",
"action_bar_profile_settings": "Einstellungen",
"action_bar_profile_logout": "Abmelden",
"action_bar_profile_logout": "Ausloggen",
"action_bar_sign_in": "Anmelden",
"signup_form_password": "Kennwort",
"signup_form_toggle_password_visibility": "Kennwort-Sichtbarkeit umschalten",
@@ -380,5 +380,28 @@
"account_basics_phone_numbers_dialog_check_verification_button": "Code bestätigen",
"account_usage_calls_title": "Getätigte Anrufe",
"account_usage_calls_none": "Noch keine Anrufe mit diesem Account getätigt",
"account_upgrade_dialog_tier_features_calls_one": "{{calls}} Telefonanrufe pro Tag"
"account_upgrade_dialog_tier_features_calls_one": "{{calls}} Telefonanrufe pro Tag",
"action_bar_mute_notifications": "Benachrichtigungen stummschalten",
"action_bar_unmute_notifications": "Stummschaltung von Benachrichtigungen aufheben",
"alert_notification_permission_denied_title": "Benachrichtigungen sind blockiert",
"alert_notification_permission_denied_description": "Bitte reaktiviere diese in deinem Browser",
"notifications_actions_failed_notification": "Aktion nicht erfolgreich",
"alert_notification_ios_install_required_title": "iOS Installation erforderlich",
"alert_notification_ios_install_required_description": "Klicke auf das Teilen-Symbol und “Zum Home-Bildschirm” um auf iOS Benachrichtigungen zu aktivieren",
"subscribe_dialog_subscribe_use_another_background_info": "Benachrichtigungen von anderen Servern werden nicht empfangen, wenn die Web App nicht geöffnet ist",
"publish_dialog_checkbox_markdown": "Als Markdown formatieren",
"prefs_notifications_web_push_title": "Hintergrundbenachrichtigungen",
"prefs_notifications_web_push_disabled_description": "Benachrichtigungen werden empfangen, wenn die Web App geöffnet ist (via WebSocket)",
"prefs_notifications_web_push_enabled": "Aktiviert für {{server}}",
"prefs_notifications_web_push_disabled": "Deaktiviert",
"prefs_appearance_theme_title": "Thema",
"prefs_appearance_theme_system": "System (Standard)",
"prefs_appearance_theme_dark": "Nachtmodus",
"prefs_appearance_theme_light": "Tagmodus",
"error_boundary_button_reload_ntfy": "ntfy neu laden",
"web_push_subscription_expiring_title": "Benachrichtigungen werden pausiert",
"web_push_subscription_expiring_body": "Öffne ntfy um weiterhin Benachrichtigungen zu erhalten",
"web_push_unknown_notification_title": "Unbekannte Benachrichtigung vom Server empfangen",
"web_push_unknown_notification_body": "Du musst möglicherweise ntfy aktualisieren, indem du die Web App öffnest",
"prefs_notifications_web_push_enabled_description": "Benachrichtigungen werden empfangen, auch wenn die Web App nicht geöffnet ist (via Web Push)"
}

View File

@@ -8,7 +8,7 @@
"message_bar_type_message": "Escriba un mensaje aquí",
"message_bar_error_publishing": "Error al publicar la notificación",
"alert_notification_permission_required_title": "Las notificaciones están deshabilitadas",
"alert_notification_permission_required_description": "Concede a tu navegador permiso para mostrar notificaciones en el escritorio.",
"alert_notification_permission_required_description": "Concede a tu navegador permiso para mostrar notificaciones de escritorio",
"nav_button_all_notifications": "Todas las notificaciones",
"nav_button_settings": "Ajustes",
"nav_button_subscribe": "Suscribirse al tópico",
@@ -16,13 +16,13 @@
"nav_button_publish_message": "Publicar notificación",
"notifications_copied_to_clipboard": "Copiado al portapapeles",
"alert_not_supported_title": "Notificaciones no soportadas",
"alert_not_supported_description": "Las notificaciones no están soportadas por tu navegador.",
"alert_not_supported_description": "Su navegador no admite notificaciones",
"notifications_tags": "Etiquetas",
"notifications_attachment_copy_url_title": "Copiar la URL del archivo adjunto en el portapapeles",
"notifications_attachment_copy_url_button": "Copiar URL",
"notifications_attachment_open_title": "Ir a {{url}}",
"notifications_attachment_open_button": "Abrir archivo adjunto",
"notifications_attachment_link_expires": "el enlace expira el día {{fecha}}",
"notifications_attachment_link_expires": "el enlace expira el día {{date}}",
"notifications_attachment_link_expired": "el enlace de descarga ha expirado",
"notifications_click_copy_url_title": "Copiar la URL del enlace en el portapapeles",
"notifications_click_copy_url_button": "Copiar enlace",
@@ -226,7 +226,7 @@
"account_basics_password_dialog_current_password_incorrect": "Contraseña incorrecta",
"account_usage_unlimited": "Ilimitado",
"account_usage_title": "Uso",
"account_usage_of_limit": "de {{límite}}",
"account_usage_of_limit": "de {{limit}}",
"account_usage_limits_reset_daily": "Los límites de uso se restablecen diariamente a la medianoche (UTC)",
"account_basics_tier_description": "Nivel de poder de tu cuenta",
"account_basics_tier_admin": "Administrador",
@@ -247,7 +247,7 @@
"account_basics_tier_free": "Gratis",
"account_basics_tier_upgrade_button": "Actualizar a Pro",
"account_basics_tier_change_button": "Cambiar",
"account_basics_tier_paid_until": "Suscripción pagada hasta {{fecha}}, y se renovará automáticamente",
"account_basics_tier_paid_until": "Suscripción pagada hasta {{date}}, y se renovará automáticamente",
"account_basics_tier_manage_billing_button": "Administrar la facturación",
"account_basics_tier_title": "Tipo de cuenta",
"account_tokens_description": "Utilice tokens de acceso al publicar y suscribirse a través de la API de ntfy para no tener que enviar las credenciales de su cuenta. Consulte la <Link>documentación</Link> para obtener más información.",
@@ -371,8 +371,8 @@
"account_basics_phone_numbers_dialog_channel_call": "Llamar",
"account_usage_calls_title": "Llamadas telefónicas realizadas",
"account_usage_calls_none": "No se pueden hacer llamadas telefónicas con esta cuenta",
"account_upgrade_dialog_tier_features_calls_one": "{{llamadas}} llamadas telefónicas diarias",
"account_upgrade_dialog_tier_features_calls_other": "{{llamadas}} llamadas telefónicas diarias",
"account_upgrade_dialog_tier_features_calls_one": "{{calls}} llamadas telefónicas diarias",
"account_upgrade_dialog_tier_features_calls_other": "{{calls}} llamadas telefónicas diarias",
"account_upgrade_dialog_tier_features_no_calls": "No hay llamadas telefónicas",
"publish_dialog_call_reset": "Eliminar llamada telefónica",
"account_basics_phone_numbers_dialog_description": "Para utilizar la función de notificación de llamadas, tiene que añadir y verificar al menos un número de teléfono. La verificación puede realizarse mediante un SMS o una llamada telefónica.",
@@ -381,5 +381,28 @@
"account_basics_phone_numbers_dialog_title": "Agregar número de teléfono",
"account_basics_phone_numbers_dialog_code_placeholder": "p.ej. 123456",
"publish_dialog_call_item": "Llamar al número de teléfono {{number}}",
"publish_dialog_chip_call_no_verified_numbers_tooltip": "No hay números de teléfono verificados"
"publish_dialog_chip_call_no_verified_numbers_tooltip": "No hay números de teléfono verificados",
"action_bar_mute_notifications": "Silenciar Notificaciones",
"action_bar_unmute_notifications": "Reactivar notificaciones",
"alert_notification_permission_denied_title": "Notificaciones bloqueadas",
"alert_notification_permission_denied_description": "Porfavor, reactivelas en su navegador",
"alert_notification_ios_install_required_title": "Requiere instalacion de iOS",
"alert_notification_ios_install_required_description": "Haz click en el icono de compartir y Añadir a pantalla de inicio para activar las notificaciones de iOS",
"notifications_actions_failed_notification": "Acción fallida",
"publish_dialog_checkbox_markdown": "Formatear como Markdown",
"subscribe_dialog_subscribe_use_another_background_info": "Las notificaciones de otros servidores no se recibirán cuando la aplicación web no esté abierta",
"prefs_notifications_web_push_title": "Notificaciones en segundo plano",
"prefs_notifications_web_push_enabled_description": "Las notificaciones se reciben incluso cuando la aplicación web no se está ejecutando (a través de Web Push)",
"prefs_notifications_web_push_disabled": "Desactivado",
"prefs_appearance_theme_title": "Tema",
"prefs_appearance_theme_system": "Sistema (por defecto)",
"error_boundary_button_reload_ntfy": "Volver a cargar ntfy",
"web_push_subscription_expiring_title": "Las notificaciones se pausarán",
"prefs_notifications_web_push_disabled_description": "Las notificaciones se reciben cuando la aplicación web se está ejecutando (a través de WebSocket)",
"prefs_notifications_web_push_enabled": "Activado para {{server}}",
"prefs_appearance_theme_light": "Claro",
"prefs_appearance_theme_dark": "Oscuro",
"web_push_subscription_expiring_body": "Abrir ntfy para seguir recibiendo notificaciones",
"web_push_unknown_notification_title": "Notificación desconocida recibida del servidor",
"web_push_unknown_notification_body": "Puede que necesites actualizar ntfy abriendo la aplicación web"
}

View File

@@ -0,0 +1,274 @@
{
"signup_title": "Loo ntfy kasutajakonto",
"signup_form_username": "Kasutajanimi",
"signup_form_password": "Salasõna",
"signup_form_confirm_password": "Kinnita salasõna õigsust",
"signup_already_have_account": "Sul juba on kasutajakonto olemas? Siis logi sisse!",
"signup_disabled": "Kasutajakonto loomine pole hetkel lubatud",
"signup_error_username_taken": "Kasutajanimi {{username}} on juba olemas",
"signup_error_creation_limit_reached": "Kasutajakontode loomise ülempiir on käes",
"login_title": "Logi sisse oma ntfy kasutajakontole",
"login_form_button_submit": "Logi sisse",
"login_link_signup": "Liitu",
"login_disabled": "Sisselogimine pole hetkel kasutusel",
"action_bar_show_menu": "Näita menüüd",
"action_bar_logo_alt": "ntfy logo",
"action_bar_settings": "Seadistused",
"action_bar_change_display_name": "Muuda kuvatavat nime",
"common_cancel": "Katkesta",
"common_save": "Salvesta",
"common_back": "Tagasi",
"common_copy_to_clipboard": "Kopeeri lõikelauale",
"common_add": "Lisa",
"signup_form_button_submit": "Liitu",
"signup_form_toggle_password_visibility": "Vaheta salasõna nähtavust",
"action_bar_account": "Kasutajakonto",
"action_bar_sign_in": "Logi sisse",
"nav_button_documentation": "Dokumentatsioon",
"action_bar_profile_title": "Profiil",
"action_bar_profile_settings": "Seadistused",
"action_bar_sign_up": "Liitu",
"message_bar_type_message": "Sisesta oma sõnum siia",
"message_bar_error_publishing": "Viga teavituse avaldamisel",
"message_bar_show_dialog": "Näita avaldamisvaadet",
"message_bar_publish": "Avalda sõnum",
"nav_topics_title": "Tellitud teemad",
"nav_button_all_notifications": "Kõik teavitused",
"nav_button_account": "Kasutajakonto",
"nav_button_settings": "Seadistused",
"nav_button_publish_message": "Avalda teavitus",
"nav_button_subscribe": "Telli teema",
"nav_button_muted": "Teavitused on summutatud",
"nav_button_connecting": "loome ühendust",
"nav_upgrade_banner_label": "Uuenda ntfy Pro teenuseks",
"action_bar_profile_logout": "Logi välja",
"notifications_list_item": "Teavitus",
"account_tokens_table_expires_header": "Aegub",
"notifications_attachment_file_document": "muu dokument",
"notifications_list": "Teavituste loend",
"notifications_delete": "Kustuta",
"notifications_copied_to_clipboard": "Kopeeritud lõikelauale",
"alert_notification_permission_denied_description": "Palun luba nad veebibrauseris uuesti",
"account_tokens_table_last_access_header": "Viimase kasutamise aeg",
"account_tokens_table_token_header": "Tunnusluba",
"account_tokens_table_last_origin_tooltip": "IP-aadressilt {{ip}}, klõpsi täpsema teabe nägemiseks",
"action_bar_reservation_add": "Reserveeri teema",
"action_bar_reservation_edit": "Muuda reserveeringut",
"action_bar_reservation_delete": "Eemalda reserveering",
"action_bar_reservation_limit_reached": "Ülempiir on käes",
"action_bar_send_test_notification": "Saata testteavitus",
"action_bar_clear_notifications": "Kustuta kõik teavitused",
"action_bar_mute_notifications": "Summuta teavitused",
"nav_upgrade_banner_description": "Reserveeri teemasid, rohkem sõnumeid ja e-kirju ning suuremad manused",
"action_bar_unmute_notifications": "Lõpeta teavituste summutamine",
"action_bar_unsubscribe": "Lõpeta tellimus",
"action_bar_toggle_mute": "Lülita teavituste summutamine sisse/välja",
"action_bar_toggle_action_menu": "Ava/sulge tegevuste menüü",
"notifications_mark_read": "Märgi loetuks",
"notifications_tags": "Sildid",
"notifications_priority_x": "{{priority}}. prioriteet",
"notifications_new_indicator": "Uus teavitus",
"notifications_attachment_image": "Pilt manusena",
"notifications_attachment_copy_url_title": "Kopeeri manuse võrguaadress lõikelauale",
"notifications_attachment_copy_url_button": "Kopeeri võrguaadress",
"notifications_attachment_open_title": "Ava {{url}} aadress",
"notifications_attachment_open_button": "Ava manus",
"notifications_attachment_link_expires": "link aegub {{date}}",
"notifications_attachment_link_expired": "allalaadimise link on aegunud",
"notifications_attachment_file_image": "pildifail",
"notifications_attachment_file_video": "videofail",
"notifications_attachment_file_audio": "helifail",
"notifications_attachment_file_app": "Androidi rakenduse fail",
"notifications_click_copy_url_title": "Kopeeri lingi võrguaadress lõikelauale",
"notifications_click_copy_url_button": "Kopeeri link",
"notifications_click_open_button": "Ava link",
"notifications_actions_open_url_title": "Ava {{url}} aadress",
"notifications_actions_not_supported": "Toiming pole veebirakenduses toetatud",
"alert_notification_permission_required_title": "Teavitused pole kasutusel",
"alert_notification_permission_required_description": "Anna oma brauserile õigused näidata töölauateavitusi",
"alert_notification_permission_required_button": "Luba nüüd",
"alert_notification_permission_denied_title": "Teavitused on blokeeritud",
"alert_notification_ios_install_required_title": "Vajalik on iOS-i paigaldamine",
"alert_not_supported_title": "Teavitused pole toetatud",
"alert_not_supported_description": "Teavitused pole sinu veebibrauseris toetatud",
"account_tokens_table_label_header": "Silt",
"account_tokens_table_never_expires": "Ei aegu iialgi",
"account_tokens_table_current_session": "Praegune brauserisessioon",
"account_tokens_table_copied_to_clipboard": "Ligipääsu tunnusluba on kopeeritud",
"account_tokens_table_cannot_delete_or_edit": "Praeguse sessiooni tunnusluba ei saa muuta ega kustutada",
"account_tokens_table_create_token_button": "Loo ligipääsuks vajalik tunnusluba",
"account_tokens_dialog_title_create": "Loo ligipääsuks vajalik tunnusluba",
"account_tokens_dialog_title_edit": "Muuda ligipääsuks vajalikku tunnusluba",
"account_tokens_dialog_title_delete": "Kustuta ligipääsuks vajalik tunnusluba",
"subscribe_dialog_login_password_label": "Salasõna",
"publish_dialog_filename_label": "Failinimi",
"prefs_reservations_table_access_header": "Ligipääs",
"publish_dialog_chip_click_label": "Klõpsi võrguaadressi",
"subscribe_dialog_subscribe_button_cancel": "Katkesta",
"publish_dialog_delay_label": "Viivitus",
"account_basics_password_title": "Salasõna",
"account_upgrade_dialog_button_cancel": "Katkesta",
"notifications_example": "Näide",
"account_usage_title": "Kasutus",
"account_basics_title": "Kasutajakonto",
"prefs_reservations_table_topic_header": "Teema",
"account_delete_dialog_button_cancel": "Katkesta",
"account_delete_dialog_label": "Salasõna",
"publish_dialog_message_label": "Sõnum",
"account_basics_phone_numbers_dialog_channel_call": "Kõne",
"prefs_users_dialog_password_label": "Salasõna",
"subscribe_dialog_subscribe_button_subscribe": "Telli",
"publish_dialog_priority_label": "Prioriteet",
"subscribe_dialog_login_button_login": "Logi sisse",
"subscribe_dialog_error_user_anonymous": "anonüümne",
"prefs_appearance_theme_title": "Kujundus",
"publish_dialog_button_cancel": "Katkesta",
"account_usage_unlimited": "Piiramatu",
"prefs_notifications_delete_after_never": "Mitte kunagi",
"account_upgrade_dialog_interval_monthly": "Iga kuu",
"account_upgrade_dialog_tier_price_per_month": "kuu",
"prefs_notifications_web_push_disabled": "Pole kasutusel",
"prefs_appearance_title": "Välimus",
"prefs_appearance_language_title": "Keel",
"prefs_reservations_dialog_topic_label": "Teema",
"publish_dialog_priority_min": "Väikseim tähtsus",
"notifications_actions_failed_notification": "Ebaõnnestunud toiming",
"publish_dialog_title_label": "Pealkiri",
"publish_dialog_tags_label": "Sildid",
"publish_dialog_email_label": "E-post",
"display_name_dialog_placeholder": "Kuvatav nimi",
"publish_dialog_title_no_topic": "Avalda teavitus",
"publish_dialog_progress_uploading": "Laadin üles…",
"publish_dialog_message_published": "Teavitus on saadetud",
"publish_dialog_emoji_picker_show": "Vali emoji",
"publish_dialog_priority_low": "Vähetähtis",
"publish_dialog_priority_default": "Vaikimisi tähtsus",
"publish_dialog_priority_high": "Oluline",
"publish_dialog_priority_max": "Väga oluline",
"publish_dialog_base_url_label": "Teenuse võrguaadress",
"publish_dialog_topic_label": "Teema nimi",
"publish_dialog_topic_reset": "Lähtesta teema",
"publish_dialog_click_label": "Klõpsi võrguaadressi",
"publish_dialog_call_label": "Telefonikõne",
"publish_dialog_button_send": "Saada",
"publish_dialog_attach_label": "Manuse võrguaadress",
"publish_dialog_filename_placeholder": "Manuse failinimi",
"publish_dialog_other_features": "Lisavõimalused:",
"publish_dialog_chip_call_label": "Telefonikõne",
"publish_dialog_chip_delay_label": "Viivita saatmisega",
"publish_dialog_chip_topic_label": "Muuda teemat",
"publish_dialog_button_cancel_sending": "Katkesta saatmine",
"account_basics_username_title": "Kasutajanimi",
"account_basics_phone_numbers_dialog_channel_sms": "Tekstisõnum",
"account_basics_tier_admin": "Peakasutaja",
"account_basics_tier_basic": "Baasteenus",
"account_basics_tier_free": "Tasuta",
"account_basics_tier_interval_monthly": "kord kuus",
"account_basics_tier_interval_yearly": "kord aastas",
"account_basics_tier_change_button": "Muuda",
"account_upgrade_dialog_interval_yearly": "Kord aastas",
"account_upgrade_dialog_tier_selected_label": "Valitud",
"account_upgrade_dialog_tier_current_label": "Praegune",
"account_tokens_dialog_button_cancel": "Katkesta",
"prefs_notifications_title": "Teavitused",
"prefs_users_table_user_header": "Kasutaja",
"prefs_reservations_dialog_access_label": "Ligipääs",
"priority_min": "min",
"priority_low": "madal",
"priority_default": "vaikimisi",
"priority_high": "kõrge",
"priority_max": "kõrgeim",
"alert_notification_ios_install_required_description": "Teavituste lubamiseks iOS-is klõpsi „Jaga“ ikooni ja vali „Lisa avaekraanile“",
"notifications_none_for_topic_title": "Sul pole selles teemas veel ühtegi teavitust.",
"notifications_none_for_topic_description": "Selles teemas teavituste saatmiseks tee PUT või POST meetodiga päring teema võrguaadressile.",
"publish_dialog_base_url_placeholder": "Teenuse võrguaadress, nt. https://toresait.com",
"notifications_loading": "Laadin teavitusi…",
"publish_dialog_title_topic": "Avalda teemas {{topic}}",
"publish_dialog_progress_uploading_detail": "Üleslaadimisel {{loaded}}/{{total}} ({{percent}}%) …",
"publish_dialog_topic_placeholder": "Teema nimi, nt. kati_teavitused",
"publish_dialog_title_placeholder": "Teavituse pealkiri, nt. Andmeruumi teavitus",
"publish_dialog_message_placeholder": "Siia sisesta sõnum",
"notifications_none_for_any_title": "Sa pole veel saanud ühtegi teavitust.",
"publish_dialog_chip_attach_file_label": "Lisa kohalik fail",
"publish_dialog_chip_attach_url_label": "Lisa fail võrguaadressilt",
"publish_dialog_chip_call_no_verified_numbers_tooltip": "Kinnitatud telefoninumbreid ei leidu",
"publish_dialog_chip_email_label": "Edasta e-posti aadressile",
"subscribe_dialog_subscribe_base_url_label": "Teenuse võrguaadress",
"subscribe_dialog_subscribe_button_generate_topic_name": "Loo nimi",
"publish_dialog_checkbox_markdown": "Kasuta Markdown-vormingut",
"subscribe_dialog_login_title": "Vajalik on sisselogimine",
"subscribe_dialog_login_username_label": "Kasutajanimi, nt. kadri",
"account_basics_phone_numbers_dialog_verify_button_sms": "Saada SMS",
"account_basics_username_description": "Hei, see oled sina ❤",
"account_basics_username_admin_tooltip": "Sina oled peakasutaja",
"account_basics_phone_numbers_dialog_verify_button_call": "Helista mulle",
"account_basics_phone_numbers_dialog_code_label": "Kinnituskood",
"account_basics_phone_numbers_dialog_code_placeholder": "nt. 123456",
"account_basics_phone_numbers_dialog_check_verification_button": "Korda koodi",
"account_upgrade_dialog_tier_features_messages_one": "{{messages}} sõnum päevas",
"account_upgrade_dialog_tier_features_messages_other": "{{messages}} sõnumit päevas",
"account_upgrade_dialog_button_redirect_signup": "Liitu kohe",
"notifications_actions_http_request_title": "Tee päring HTTP {{method}}-meetodiga võrguaadressile {{url}}",
"account_upgrade_dialog_tier_features_emails_other": "{{emails}} e-kirja päevas",
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} e-kiri päevas",
"alert_not_supported_context_description": "Teavitused võivad kasutada vaid HTTPS-ühendust. See on <mdnLink>Teavituste API</mdnLink> piirang.",
"publish_dialog_tags_placeholder": "Komadega eraldatud siltide loend, nt. hoiatus, srv1-varundus",
"display_name_dialog_title": "Muuda kuvatavat nime",
"display_name_dialog_description": "Lisa teemale alternatiivne nimi, mida kuvatakse tellimuste loendis. See on näiteks abiks keerukate nimedega teemade tuvastamiseks.",
"reserve_dialog_checkbox_label": "Reserveeri teema ja seadista ligipääs",
"publish_dialog_attachment_limits_file_reached": "ületab failisuuruse piiri: {{fileSizeLimit}}",
"publish_dialog_attachment_limits_quota_reached": "ületab kvooti, jäänud on {{remainingBytes}}",
"publish_dialog_attachment_limits_file_and_quota_reached": "ületab failisuuruse ülempiiri ({{fileSizeLimit}}) ja kvooti, jäänud on {{remainingBytes}}",
"publish_dialog_click_placeholder": "Teavituse klõpsimisel avatav võrguaadress",
"publish_dialog_click_reset": "Eemalda klikatav võrguaadress",
"publish_dialog_email_placeholder": "Aadress, kuhu teavitus edastatakse, nt. kadri@torefirma.com",
"publish_dialog_email_reset": "Eemalda edastamiseks kasutatav e-posti aadress",
"publish_dialog_call_item": "Helista telefoninumbrile {{number}}",
"publish_dialog_call_reset": "Eemalda helistamine",
"publish_dialog_attach_placeholder": "Lisa fail võrguaadressilt, nt. https://f-droid.org/F-Droid.apk",
"publish_dialog_attach_reset": "Eemalda manuse lisamisel kasutatav võrguaadress",
"publish_dialog_delay_reset": "Eemalda viivitus teavituse edastamisel",
"account_basics_password_description": "Muuda oma kasutajakonto salasõna",
"account_basics_password_dialog_title": "Salasõna muutmine",
"account_basics_password_dialog_current_password_label": "Senine salasõna",
"account_basics_password_dialog_button_submit": "Muuda salasõna",
"account_basics_password_dialog_current_password_incorrect": "Salasõna pole korrektne",
"account_basics_phone_numbers_title": "Telefoninumbrid",
"account_basics_phone_numbers_description": "Kõneteavituste jaoks",
"account_basics_tier_title": "Kasutajakonto tüüp",
"account_basics_tier_description": "Sinu kasutajakonto õigused",
"account_delete_dialog_button_submit": "Kustuta kasutajakonto jäädavalt",
"prefs_appearance_theme_system": "Süsteemi kujundus",
"prefs_appearance_theme_dark": "Tume kujundus",
"prefs_appearance_theme_light": "Hele kujundus",
"prefs_reservations_title": "Reserveeritud teemad",
"prefs_users_table": "Kasutajate loend",
"prefs_users_add_button": "Lisa kasutaja",
"prefs_users_edit_button": "Muuda kasutajat",
"prefs_users_delete_button": "Kustuta kasutaja",
"prefs_users_table_cannot_delete_or_edit": "Sisselogitud kasutajat ei saa kustutada ega muuta",
"prefs_users_table_base_url_header": "Teenuse võrguaadress",
"prefs_users_dialog_title_add": "Lisa kasutaja",
"prefs_users_dialog_title_edit": "Muuda kasutajat",
"prefs_users_dialog_base_url_label": "Teenuse võrguaadress, nt. https://ntfy.sh",
"prefs_users_dialog_username_label": "Kasutajanimi, nt. kadri",
"prefs_notifications_delete_after_three_hours": "Kolme tunni möödumisel",
"prefs_notifications_delete_after_three_hours_description": "Teavitused kustutatakse automaatselt kolme tunni möödumisel",
"prefs_notifications_delete_after_one_day_description": "Teavitused kustutatakse automaatselt ühe päeva möödumisel",
"prefs_notifications_delete_after_one_week_description": "Teavitused kustutatakse automaatselt ühe nädala möödumisel",
"prefs_notifications_delete_after_one_month_description": "Teavitused kustutatakse automaatselt ühe kuu möödumisel",
"prefs_notifications_delete_after_never_description": "Mitte kunagi ei kustutata teavitusi automaatselt",
"prefs_notifications_delete_after_title": "Kustuta teavitused",
"publish_dialog_delay_placeholder": "Viivitus teavituse edastamisel, nt. {{unixTimestamp}}, {{relativeTime}} või „{{naturalLanguage}}“ (vaid inglise keeles)",
"account_basics_password_dialog_new_password_label": "Uus salasõna",
"account_basics_password_dialog_confirm_password_label": "Korda salasõna",
"account_basics_phone_numbers_dialog_description": "Kõneteavituse kasutamiseks pead lisama ja kinnitama vähemalt ühe telefoninumbri. Kinnitamist saad teha SMS-i või kõne abil.",
"account_basics_phone_numbers_dialog_number_placeholder": "nt. +37256123456",
"account_basics_phone_numbers_no_phone_numbers_yet": "Telefoninumbreid veel pole",
"account_basics_phone_numbers_copied_to_clipboard": "Telefoninumber on kopeeritud lõikelauale",
"account_basics_phone_numbers_dialog_title": "Lisa telefoninumber",
"account_basics_phone_numbers_dialog_number_label": "Telefoninumber",
"prefs_notifications_delete_after_one_week": "Ühe nädala möödumisel",
"prefs_notifications_delete_after_one_day": "Ühe päeva möödumisel",
"prefs_notifications_delete_after_one_month": "Ühe kuu möödumisel"
}

View File

@@ -0,0 +1,58 @@
{
"signup_title": "ایجاد اکانت ntfy",
"signup_form_button_submit": "ثبت نام",
"signup_already_have_account": "قبلا اکانت دارید؟ وارد بشود",
"signup_disabled": "ثبت نام غیرفعال است",
"login_title": "ورود به اکانت ntfy",
"login_link_signup": "ثبت نام",
"login_disabled": "ورود غیرفعال است",
"action_bar_show_menu": "نمایش منو",
"action_bar_account": "اکانت",
"action_bar_reservation_limit_reached": "دسترسی محدود",
"action_bar_send_test_notification": "ارسال تستی اعلان",
"action_bar_unmute_notifications": "لغو ساکت کردن اعلان ها",
"action_bar_unsubscribe": "لغو اشتراک",
"action_bar_toggle_mute": "بی صدا/لغو اعلان ها",
"common_cancel": "لغو",
"common_save": "ذخیره",
"common_add": "اضافه کردن",
"common_back": "عقب",
"common_copy_to_clipboard": "کپی به کلیپ بورد",
"signup_form_username": "نام کاربری",
"signup_form_password": "کلمه عبور",
"signup_form_confirm_password": "تایید پسورد",
"signup_form_toggle_password_visibility": "تغییر وضعیت نمایش کلمه عبور",
"signup_error_username_taken": "نام کاربری {{username}} قبلا استفاده شده است",
"signup_error_creation_limit_reached": "به حد مجاز ایجاد حساب رسیده است",
"login_form_button_submit": "ورود",
"action_bar_logo_alt": "لوگوی ntfy",
"action_bar_settings": "تنظیمات",
"action_bar_change_display_name": "تغییر نام نمایشی",
"action_bar_reservation_add": "رزرو موضوع",
"action_bar_reservation_edit": "تغییر رزرو",
"action_bar_reservation_delete": "حذف رزرو",
"action_bar_mute_notifications": "ساکت کردن اعلان ها",
"action_bar_clear_notifications": "پاک کردن تمام اعلان ها",
"action_bar_toggle_action_menu": "گشودن يا بستن فهرست کنش",
"action_bar_profile_title": "نمايه",
"action_bar_profile_settings": "تنظیمات",
"action_bar_profile_logout": "خروج",
"action_bar_sign_in": "ورود",
"action_bar_sign_up": "ثبت نام",
"message_bar_type_message": "یک پیام بنویسید",
"message_bar_error_publishing": "خطا در انتظار اعلان",
"message_bar_publish": "انتشار پیام",
"nav_button_all_notifications": "همه اعلان‌ها",
"nav_button_account": "حساب کاربری",
"nav_button_settings": "تنظیمات",
"nav_button_documentation": "مستندات",
"nav_button_publish_message": "انتشار اعلان",
"nav_button_muted": "اعلان بی‌صدا شد",
"nav_button_connecting": "در حال اتصال",
"nav_upgrade_banner_label": "ارتقا با ntfy پیشرفته",
"alert_notification_permission_required_title": "اعلان‌ها غیرفعال هستند",
"alert_notification_permission_required_description": "به مرورگر خود اجازه دهید تا اعلان‌های دسکتاپ را نمایش دهد",
"alert_notification_permission_denied_title": "اعلان‌ها مسدود هستند",
"alert_notification_ios_install_required_title": "لازم به نصب نسخه iOS است",
"alert_notification_ios_install_required_description": "برای فعال کردن اعلان‌ها در iOS، روی نماد اشتراک‌گذاری و افزودن به صفحه اصلی کلیک کنید"
}

View File

@@ -1,7 +1,7 @@
{
"publish_dialog_message_placeholder": "Kirjoita viesti tähän",
"account_upgrade_dialog_tier_features_no_calls": "Ei puheluita",
"account_upgrade_dialog_billing_contact_email": "Laskutukseen liittyvissä kysymyksissä <Link>contact us</Link> suoraan.",
"account_upgrade_dialog_billing_contact_email": "Laskutukseen liittyvissä kysymyksissä <Link>ole yhteydessä</Link> .",
"account_tokens_dialog_title_create": "Luo käyttöoikeustunnus",
"prefs_reservations_dialog_title_edit": "Muokkaa varattua topikkia",
"account_basics_tier_interval_monthly": "Kuukausittain",
@@ -12,7 +12,7 @@
"prefs_notifications_min_priority_title": "Vähimmäisprioriteetti",
"account_upgrade_dialog_tier_features_calls_one": "{{calls}} päivittäisiä puheluja",
"account_upgrade_dialog_tier_current_label": "Nykyinen",
"action_bar_account": "Kirjautuminen",
"action_bar_account": "Tili",
"publish_dialog_filename_placeholder": "Liitetiedoston nimi",
"account_basics_password_dialog_current_password_incorrect": "Salasana virheellinen",
"account_tokens_table_token_header": "Token",
@@ -33,7 +33,7 @@
"account_upgrade_dialog_proration_info": "<strong>Osuussuhde</strong>: Kun päivität maksullisten pakettien välillä, hintaero <strong>veloitetaan välittömästi</strong>. Kun siirryt alemmalle tasolle, saldoa käytetään tulevien laskutuskausien maksamiseen.",
"prefs_reservations_dialog_access_label": "Oikeudet",
"account_usage_attachment_storage_title": "Liiteiden säilytys",
"prefs_users_dialog_username_label": "Username, esim pena",
"prefs_users_dialog_username_label": "Käyttäjätunnus, esim. pentti",
"message_bar_error_publishing": "Virhe ilmoituksen julkaisemisessa",
"publish_dialog_chip_delay_label": "Viivästytä toimitusta",
"account_usage_messages_title": "Julkaistut viestit",
@@ -86,10 +86,10 @@
"prefs_reservations_dialog_title_delete": "Poista topikin varaus",
"prefs_users_table": "Käyttäjätaulukko",
"prefs_reservations_table_topic_header": "Topikki",
"action_bar_toggle_mute": "Hiljennä/poista hiljennys",
"action_bar_toggle_mute": "Mykistä/palauta ilmoitukset",
"reservation_delete_dialog_submit_button": "Poista varaus",
"account_basics_title": "Tili",
"nav_button_documentation": "Dokumentointi",
"nav_button_documentation": "Dokumentaatio",
"prefs_reservations_limit_reached": "Olet saavuttanut varattujen topikkien rajan.",
"account_upgrade_dialog_interval_monthly": "Kuukausittain",
"prefs_users_add_button": "Lisää käyttäjä",
@@ -116,8 +116,8 @@
"account_tokens_table_label_header": "Merkki",
"notifications_attachment_file_document": "muu asiakirja",
"publish_dialog_button_cancel": "Peruuta",
"account_upgrade_dialog_billing_contact_website": "Laskutukseen liittyvissä kysymyksissä käy sivulla <Link>website</Link>.",
"signup_form_button_submit": "Kirjaudu linkki",
"account_upgrade_dialog_billing_contact_website": "Laskutukseen liittyvissä kysymyksissä käy <Link>verkkosivustolla</Link>.",
"signup_form_button_submit": "Rekisteröidy",
"account_basics_username_admin_tooltip": "Olet pääkäyttäjä",
"prefs_notifications_delete_after_never_description": "Ilmoituksia ei koskaan poisteta automaattisesti",
"account_delete_dialog_description": "Tämä poistaa pysyvästi tilisi, mukaan lukien kaikki palvelimelle tallennetut tiedot. Poistamisen jälkeen käyttäjätunnuksesi on poissa käytöstä 7 päivään. Jos todella haluat jatkaa, vahvista salasanasi alla olevaan kenttään.",
@@ -143,13 +143,13 @@
"prefs_users_description_no_sync": "Käyttäjiä ja salasanoja ei ole synkronoitu tiliisi.",
"account_tokens_dialog_title_edit": "Muokkaa käyttöoikeustunnusta",
"nav_button_publish_message": "Julkaise ilmoitus",
"prefs_users_table_base_url_header": "Palvelin-URL",
"prefs_users_table_base_url_header": "Palvelun URL",
"notifications_click_copy_url_title": "Kopioi linkin URL-osoite leikepöydälle",
"publish_dialog_attach_reset": "Poista liitteen URL-osoite",
"account_upgrade_dialog_tier_features_messages_one": "{{messages}} päivittäisiä viestejä",
"account_upgrade_dialog_reservations_warning_one": "Valittu taso sallii vähemmän varattuja topikeita kuin nykyinen tasosi. Ennen kuin muutat tasosi, <strong>poista vähintään yksi varaus</strong>. Voit poistaa varauksia <Link>Asetuksista</Link>.",
"common_copy_to_clipboard": "Kopioi leikkelepöydälle",
"alert_not_supported_description": "Selaimesi ei tue ilmoituksia.",
"common_copy_to_clipboard": "Kopioi leikepöydälle",
"alert_not_supported_description": "Selaimesi ei tue ilmoituksia",
"subscribe_dialog_error_topic_already_reserved": "Topikki on jo varattu",
"message_bar_publish": "Julkaise viesti",
"alert_grant_description": "Myönnä selaimelle lupa näyttää työpöytäilmoituksia.",
@@ -170,7 +170,7 @@
"account_basics_tier_description": "Tilisi taso",
"account_basics_phone_numbers_description": "Puheluilmoituksia varten",
"prefs_reservations_dialog_title_add": "Varaa topikki",
"account_basics_tier_free": "Vapaa",
"account_basics_tier_free": "Maksuton",
"account_upgrade_dialog_cancel_warning": "Tämä <strong>peruuttaa tilauksesi</strong> ja alentaa tilisi {{date}}. Tuona päivänä topikit sekä palvelimen välimuistissa olevat viestit <strong>poistetaan</strong>.",
"notifications_click_copy_url_button": "Kopioi linkki",
"account_basics_tier_admin": "Admin",
@@ -179,7 +179,7 @@
"prefs_notifications_sound_title": "Ilmoitusääni",
"prefs_notifications_min_priority_default_and_higher": "Oletusprioriteetti ja korkeammat",
"prefs_reservations_table_access_header": "Oikeudet",
"action_bar_show_menu": "Näytä menu",
"action_bar_show_menu": "Näytä valikko",
"action_bar_settings": "Asetukset",
"notifications_copied_to_clipboard": "Kopioitu leikepöydälle",
"account_delete_dialog_button_cancel": "Peruuta",
@@ -196,15 +196,15 @@
"publish_dialog_call_label": "Puhelu",
"account_usage_calls_title": "Soitetut puhelut",
"error_boundary_description": "Näin ei selvästikään pitäisi tapahtua. Pahoittelut tästä.<br/>Jos sinulla on hetki aikaa, <githubLink>ilmoita tästä GitHubissa</githubLink> tai ilmoita meille <discordLink>Discordin</discordLink> tai <matrixLink>Matrix</matrixLink> kautta.",
"signup_form_toggle_password_visibility": "Vaihda salasanan näkyvyys",
"login_link_signup": "Kirjautumislinkki",
"signup_form_toggle_password_visibility": "Näytä/piilota salasana",
"login_link_signup": "Rekisteröidy",
"publish_dialog_message_label": "Viesti",
"publish_dialog_attached_file_title": "Liitetiedosto:",
"priority_min": "min",
"action_bar_sign_in": "Kirjaudu sisään",
"action_bar_unsubscribe": "Peruuta tilaus",
"account_basics_tier_basic": "Perus",
"signup_title": "Lisää ntfy tili",
"signup_title": "Luo ntfy-tili",
"prefs_notifications_min_priority_description_any": "Näytetään kaikki ilmoitukset tärkeydestä riippumatta",
"error_boundary_gathering_info": "Kerää lisätietoja…",
"publish_dialog_priority_max": "Max. prioriteetti",
@@ -266,7 +266,7 @@
"alert_not_supported_title": "Ilmoituksia ei tueta",
"account_tokens_dialog_button_cancel": "Peruuta",
"subscribe_dialog_error_user_anonymous": "Anonyymi",
"account_upgrade_dialog_tier_price_billed_yearly": "{{price}} laskutetaan vuosittain. Tallenna {{save}}.",
"account_upgrade_dialog_tier_price_billed_yearly": "{{price}} laskutetaan vuosittain. Säästä {{save}}.",
"prefs_notifications_min_priority_high_and_higher": "Korkea prioriteetti ja korkeammat",
"account_usage_basis_ip_description": "Tämän tilin käyttötilastot ja rajoitukset perustuvat IP-osoitteeseesi, joten ne voidaan jakaa muiden käyttäjien kanssa. Yllä esitetyt rajat ovat likimääräisiä perustuen olemassa oleviin rajoituksiin.",
"publish_dialog_priority_high": "Korkea prioriteetti",
@@ -285,7 +285,7 @@
"account_basics_phone_numbers_title": "Puhelinnumerot",
"prefs_notifications_delete_after_title": "Poista ilmoitukset",
"account_upgrade_dialog_interval_yearly_discount_save": "säästä {{discount}}%",
"signup_disabled": "Kirjautuminen estetty",
"signup_disabled": "Rekisteröityminen estetty",
"publish_dialog_drop_file_here": "Pudota tiedosto tähän",
"prefs_users_dialog_title_edit": "Muokkaa käyttäjää",
"account_basics_password_dialog_current_password_label": "Nykyinen salasana",
@@ -295,7 +295,7 @@
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} lopullinen tiedostokoko",
"publish_dialog_title_label": "Otsikko",
"prefs_reservations_table_everyone_write_only": "Minä voin julkaista ja tilata, kaikki voivat julkaista",
"prefs_appearance_title": "Näkymä",
"prefs_appearance_title": "Ulkoasu",
"publish_dialog_topic_reset": "Resetoi topikki",
"account_tokens_table_cannot_delete_or_edit": "Nykyistä istuntotunnusta ei voi muokata tai poistaa",
"notifications_tags": "Tagit",
@@ -338,7 +338,7 @@
"login_title": "Kirjaudu sisään ntfy-tilillesi",
"notifications_list": "Ilmoitusluettelo",
"common_save": "Tallenna",
"prefs_users_dialog_base_url_label": "Palvelin URL, esim. https://ntfy.sh",
"prefs_users_dialog_base_url_label": "Palvelun URL, esim. https://ntfy.sh",
"account_usage_emails_title": "Sähköpostit lähetetty",
"account_basics_phone_numbers_dialog_channel_sms": "SMS",
"action_bar_reservation_add": "Varalla oleva aihe",
@@ -365,20 +365,46 @@
"notifications_no_subscriptions_title": "Näyttää siltä, että sinulla ei ole vielä tilauksia.",
"notifications_none_for_topic_title": "Et ole vielä saanut ilmoituksia tästä aiheesta.",
"notifications_actions_http_request_title": "Lähetä HTTP {{method}} osoitteeseen {{url}}",
"reserve_dialog_checkbox_label": "Käänteinen aihe ja aseta pääsy",
"reserve_dialog_checkbox_label": "Varaa aihe ja aseta pääsy",
"publish_dialog_progress_uploading": "Lähetetään …",
"publish_dialog_title_no_topic": "Julkaise ilmoitus",
"notifications_example": "Esimerkki",
"notifications_loading": "Ladataan ilmoituksia …",
"notifications_loading": "Ladataan ilmoituksia…",
"notifications_no_subscriptions_description": "Klikkaa \"{{linktext}}\" linkkiä luodaksesi tai tilataksesi aihe. Sen jälkeen voit lähettää viestejä PUT tai POST metodeilla ja saat ilmoituksesi täällä.",
"display_name_dialog_description": "Aseta vaihtoehtoinen nimi aiheelle, joka on näytetty tilaus-listassa. Tämä auttaa tunnistamaan aiheet helpommin, joilla on hankalat nimet.",
"publish_dialog_message_published": "Ilmoitus julkaistu",
"notifications_more_details": "Saadaksesi lisää tietoa, katso <websiteLink>nettisivu</websiteLink> tai <docsLink>documentointi</docsLink>.",
"notifications_more_details": "Saadaksesi lisää tietoa, katso <websiteLink>verkkosivusto</websiteLink> tai <docsLink>dokumentaatio</docsLink>.",
"publish_dialog_attachment_limits_quota_reached": "ylittää kiintiön, {{remainingBytes}} jäljellä",
"publish_dialog_title_topic": "Julkaise aiheeseen {{topic}}",
"display_name_dialog_placeholder": "Näyttönimi",
"publish_dialog_attachment_limits_file_and_quota_reached": "ylittää {{fileSizeLimit}} tiedostokoon rajan ja määrän, {{remainingBytes}} jäljellä",
"publish_dialog_attachment_limits_file_reached": "ylittää {{fileSizeLimit}} tiedostokoon rajan",
"publish_dialog_progress_uploading_detail": "Lähetetään {{loaded}}/{{total}} ({{percent}}%) …",
"display_name_dialog_title": "Vaihda näyttönimi"
"display_name_dialog_title": "Vaihda näyttönimi",
"action_bar_mute_notifications": "Mykistä ilmoitukset",
"action_bar_unmute_notifications": "Poista ilmoitusten mykistys",
"alert_notification_permission_required_title": "Ilmoitukset eivät ole käytössä",
"alert_notification_permission_required_description": "Anna selaimelle lupa näyttää työpöytäilmoituksia",
"alert_notification_permission_required_button": "Myönnä lupa nyt",
"alert_notification_permission_denied_title": "Ilmoitukset on estetty",
"alert_notification_ios_install_required_title": "iOS-asennus vaaditaan",
"publish_dialog_checkbox_markdown": "Muotoile Markdownina",
"prefs_notifications_web_push_title": "Taustailmoitukset",
"prefs_appearance_theme_system": "Järjestelmä (oletus)",
"alert_notification_permission_denied_description": "Ota ilmoitukset uudelleen käyttöön selaimessa",
"prefs_appearance_theme_title": "Teema",
"prefs_appearance_theme_light": "Vaalea tila",
"prefs_notifications_web_push_enabled": "Käytössä palvelimelle {{server}}",
"prefs_notifications_web_push_disabled": "Pois käytöstä",
"prefs_appearance_theme_dark": "Tumma tila",
"error_boundary_button_reload_ntfy": "Lataa ntfy uudelleen",
"web_push_subscription_expiring_title": "Ilmoitukset keskeytetään",
"web_push_subscription_expiring_body": "Avaa ntfy jatkaaksesi ilmoitusten vastaanottamista",
"web_push_unknown_notification_title": "Tuntematon ilmoitus vastaanotettu palvelimelta",
"alert_notification_ios_install_required_description": "Napauta Jaa-kuvaketta ja Lisää aloitusnäyttöön ottaaksesi ilmoitukset käyttöön iOS:ssä",
"prefs_notifications_web_push_disabled_description": "Ilmoituksia vastaanotetaan, kun verkkosovellus on käynnissä (WebSocket:in kautta)",
"web_push_unknown_notification_body": "Voit joutua päivittämään ntfy:n avaamalla verkkosovelluksen",
"notifications_actions_failed_notification": "Epäonnistunut toiminto",
"subscribe_dialog_subscribe_use_another_background_info": "Ilmoituksia muilta palvelimilta ei vastaanoteta, mikäli verkkosovellus ei ole avoinna",
"prefs_notifications_web_push_enabled_description": "Ilmoituksia vastaanotetaan siitä huolimatta, että verkkosovellus ei ole käynnissä (Web Push:n kautta)"
}

View File

@@ -11,7 +11,7 @@
"nav_button_all_notifications": "Toutes les notifications",
"nav_button_settings": "Paramètres",
"nav_button_documentation": "Documentation",
"alert_not_supported_description": "Les notifications ne sont pas prises en charge par votre navigateur.",
"alert_not_supported_description": "Les notifications ne sont pas prises en charge par votre navigateur",
"notifications_attachment_copy_url_title": "Copier l'URL de la pièce jointe dans le presse-papiers",
"notifications_attachment_open_title": "Aller à {{url}}",
"notifications_attachment_link_expired": "lien de téléchargement expiré",
@@ -51,7 +51,7 @@
"nav_button_subscribe": "S'abonner au sujet",
"notifications_no_subscriptions_description": "Cliquez sur le lien « {{linktext}} » pour créer ou vous abonner à un sujet. Après cela, vous pouvez envoyer des messages via PUT ou POST et vous recevrez des notifications ici.",
"alert_notification_permission_required_title": "Les notifications sont désactivées",
"alert_notification_permission_required_description": "Autorisez votre navigateur à afficher les notifications du bureau.",
"alert_notification_permission_required_description": "Autorisez votre navigateur à afficher les notifications du bureau",
"alert_notification_permission_required_button": "Accorder maintenant",
"notifications_none_for_any_title": "Vous n'avez reçu aucune notification.",
"publish_dialog_title_topic": "Publier vers {{topic}}",
@@ -98,7 +98,7 @@
"subscribe_dialog_subscribe_button_subscribe": "S'abonner",
"subscribe_dialog_login_description": "Ce sujet est protégé par un mot de passe. Veuillez entrer le nom d'utilisateur et le mot de passe pour vous abonner.",
"subscribe_dialog_login_username_label": "Nom d'utilisateur, par ex. phil",
"subscribe_dialog_login_button_login": "Connexion",
"subscribe_dialog_login_button_login": "Se connecter",
"prefs_notifications_sound_title": "Son de notification",
"prefs_notifications_delete_after_never": "Jamais",
"prefs_users_table_base_url_header": "URL de service",
@@ -194,13 +194,13 @@
"signup_error_username_taken": "L'identifiant {{username}} est déjà utilisé",
"signup_error_creation_limit_reached": "Limite de création de comptes atteinte",
"login_title": "Se connecter à son compte Ntfy",
"login_form_button_submit": "Connexion",
"login_form_button_submit": "Se connecter",
"login_link_signup": "S'inscrire",
"login_disabled": "La connection est désactivée",
"action_bar_account": "Compte",
"action_bar_profile_title": "Profil",
"action_bar_profile_settings": "Paramètres",
"action_bar_sign_in": "Connexion",
"action_bar_sign_in": "Se connecter",
"action_bar_sign_up": "Inscription",
"nav_button_account": "Compte",
"signup_title": "Créer un compte Ntfy",
@@ -380,5 +380,28 @@
"publish_dialog_chip_call_no_verified_numbers_tooltip": "Aucun numéro de téléphone vérifié",
"account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} sujet réservé",
"account_upgrade_dialog_tier_features_calls_one": "{{calls}} appels journaliers",
"account_usage_calls_title": "Appels téléphoniques passés"
"account_usage_calls_title": "Appels téléphoniques passés",
"action_bar_mute_notifications": "Désactiver les notifications",
"action_bar_unmute_notifications": "Réactiver les notifications",
"alert_notification_permission_denied_title": "Les notifications sont bloquées",
"alert_notification_permission_denied_description": "Veuillez les réactiver dans votre navigateur",
"alert_notification_ios_install_required_description": "Cliquez sur l'icône Partager, puis Sur l'écran d'accueil pour activer les notifications sur iOS",
"alert_notification_ios_install_required_title": "Installation iOS nécessaire",
"notifications_actions_failed_notification": "Échec de l'action",
"publish_dialog_checkbox_markdown": "Formater en Markdown",
"subscribe_dialog_subscribe_use_another_background_info": "Les notifications provenant d'autres serveurs ne seront pas reçues tant que l'application web n'est pas ouverte",
"prefs_notifications_web_push_title": "Notifications en arrière-plan",
"prefs_notifications_web_push_enabled_description": "Les notifications sont reçues même quand l'application web n'est pas en cours d'exécution (via Web Push)",
"prefs_notifications_web_push_disabled_description": "Les notifications sont reçues quand l'application web est en cours d'exécution (via WebSocket)",
"prefs_notifications_web_push_enabled": "Activé pour {{server}}",
"prefs_notifications_web_push_disabled": "Désactivé",
"prefs_appearance_theme_title": "Thème",
"prefs_appearance_theme_system": "Système (défaut)",
"prefs_appearance_theme_dark": "Mode sombre",
"prefs_appearance_theme_light": "Mode clair",
"error_boundary_button_reload_ntfy": "Recharger ntfy",
"web_push_subscription_expiring_title": "Les notifications seront suspendues",
"web_push_subscription_expiring_body": "Ouvrez ntfy pour continuer à recevoir les notifications",
"web_push_unknown_notification_title": "Notification inconnue reçue du serveur",
"web_push_unknown_notification_body": "Il est possible que vous deviez mettre à jour ntfy en ouvrant l'application web"
}

View File

@@ -51,7 +51,7 @@
"nav_button_muted": "Notificacións acaladas",
"nav_button_connecting": "conectando",
"nav_upgrade_banner_label": "Mellorar a ntfy Pro",
"alert_not_supported_description": "O teu navegador non ten soporte para notificacións.",
"alert_not_supported_description": "O teu navegador non ten soporte para notificacións",
"notifications_priority_x": "Prioridade {{priority}}",
"notifications_attachment_link_expires": "a ligazón caduca o {{date}}",
"notifications_attachment_link_expired": "a ligazón de descarga caducou",
@@ -62,7 +62,7 @@
"notifications_none_for_topic_title": "Aínda non recibiches ningunha notificación para este tema.",
"reserve_dialog_checkbox_label": "Reservar tema e configurar acceso",
"notifications_loading": "Cargando notificacións…",
"publish_dialog_base_url_placeholder": "URL de servizo, ex. https://exemplo.com",
"publish_dialog_base_url_placeholder": "URL do servizo, ex. https://exemplo.com",
"publish_dialog_topic_label": "Nome do tema",
"publish_dialog_topic_placeholder": "Nome do tema, ex. alertas_equipo",
"publish_dialog_topic_reset": "Restablecer tema",
@@ -172,7 +172,7 @@
"account_tokens_table_token_header": "Token",
"prefs_notifications_delete_after_never": "Nunca",
"prefs_users_description": "Engadir/eliminar usuarias dos temas protexidos. Ten en conta que as credenciais gárdanse na almacenaxe local do navegador.",
"subscribe_dialog_subscribe_description": "Os temas poderían non estar proxetidos con contrasinal, así que elixe un nome complicado de adiviñar. Unha vez subscrita, podes PUT/POST notificacións.",
"subscribe_dialog_subscribe_description": "Os temas poden non estar protexidos con contrasinal, asi que escolle un nome que non sexa fácil de pesquisar. Unha vez suscrito, podes notificar con PUT/POST.",
"account_upgrade_dialog_interval_yearly_discount_save_up_to": "aforro ata un {{discount}}%",
"account_tokens_dialog_label": "Etiqueta, ex. notificación de Radarr",
"account_tokens_table_expires_header": "Caducidade",
@@ -315,17 +315,17 @@
"account_basics_password_dialog_current_password_incorrect": "Contrasinal incorrecto",
"account_basics_phone_numbers_dialog_number_label": "Número de teléfono",
"account_basics_password_dialog_button_submit": "Modificar contrasinal",
"account_basics_username_title": "Usuario",
"account_basics_username_title": "Identificador",
"account_basics_phone_numbers_dialog_check_verification_button": "Código de confirmación",
"account_usage_messages_title": "Mesaxes publicados",
"account_basics_phone_numbers_dialog_verify_button_sms": "Enviar SMS",
"account_basics_tier_change_button": "Cambiar",
"account_basics_phone_numbers_dialog_description": "Para usar a característica de chamadas de teléfono, vostede debe engadir e verificar ao menos un número de teléfono. A verificación pode ser realizada vía SMS ou a través de chamada.",
"account_delete_title": "Borrar conta",
"account_delete_title": "Eliminar a conta",
"account_delete_dialog_label": "Contrasinal",
"account_basics_tier_admin_suffix_with_tier": "(con tier {{tier}})",
"subscribe_dialog_login_username_label": "Nome de usuario, ex. phil",
"subscribe_dialog_error_user_not_authorized": "Usuario {{username}} non autorizado",
"subscribe_dialog_login_username_label": "Identificador, ex. xoana",
"subscribe_dialog_error_user_not_authorized": "Identificador {{username}} non autorizado",
"account_basics_title": "Conta",
"account_basics_phone_numbers_no_phone_numbers_yet": "Aínda non hay números de teléfono",
"subscribe_dialog_subscribe_button_generate_topic_name": "Xerar nome",
@@ -333,9 +333,9 @@
"subscribe_dialog_subscribe_button_subscribe": "Subscribirse",
"account_basics_phone_numbers_dialog_title": "Engadir número de teléfono",
"account_basics_username_admin_tooltip": "É vostede Admin",
"account_delete_dialog_description": "Isto borrará permanentemente a túa conta, incluido todos os datos almacenados no servidor. Despois do borrado, o teu nome de usuario non estará dispoñible durante 7 días. Se realmente queres proceder, por favor confirme co seu contrasinal na caixa inferior.",
"account_delete_dialog_description": "Isto borrará permanentemente a conta, incluido todos os datos almacenados no servidor. Despois do borrado, o teu identificador non estará dispoñible durante 7 días. Se realmente queres proceder, por favor confirma co contrasinal na caixa inferior.",
"account_usage_reservations_none": "Non hai temas reservados para esta conta",
"subscribe_dialog_subscribe_topic_placeholder": "Nome do tema, ex. phil_alertas",
"subscribe_dialog_subscribe_topic_placeholder": "Nome do tema, ex. alertas_xoana",
"account_usage_title": "Uso",
"account_basics_tier_upgrade_button": "Mexorar a Pro",
"subscribe_dialog_error_topic_already_reserved": "Tema xa reservado",
@@ -351,11 +351,11 @@
"account_basics_phone_numbers_copied_to_clipboard": "Número de teléfono copiado no portapapeis",
"account_basics_tier_title": "Tipo de conta",
"account_usage_cannot_create_portal_session": "Non foi posible abrir o portal de pagos",
"account_delete_description": "Borrar permanentemente a túa conta",
"account_delete_description": "Eliminar a conta de xeito definitivo",
"account_basics_phone_numbers_dialog_number_placeholder": "ex. +1222333444",
"account_basics_phone_numbers_dialog_code_placeholder": "ex. 123456",
"account_basics_tier_manage_billing_button": "Xestionar pagos",
"account_basics_username_description": "Ei, ese eres ti ❤",
"account_basics_username_description": "Ei, es ti ❤",
"account_basics_password_dialog_confirm_password_label": "Confirmar contrasinal",
"account_basics_tier_interval_yearly": "anual",
"account_delete_dialog_button_submit": "Borrar permanentemente a conta",
@@ -364,7 +364,7 @@
"account_basics_password_dialog_new_password_label": "Novo contrasinal",
"account_usage_of_limit": "de {{limit}}",
"subscribe_dialog_error_user_anonymous": "anónimo",
"account_usage_basis_ip_description": "Estadísticas de uso e límites para esta conta están basados na sua IP, polo que poden estar compartidos con outros usuarios. Os limites mostrados son aproximados, basados nos ratios de limite existentes.",
"account_usage_basis_ip_description": "As estatísticas de uso e límites para esta conta están basados na IP, polo que poden estar compartidas con outras usuarias. Os limites mostrados son aproximados, baseados nos límites das taxas existentes.",
"account_basics_password_dialog_title": "Modificar contrasinal",
"account_usage_limits_reset_daily": "Límite de uso é reiniciado diariamente a medianoite (UTC(",
"account_usage_unlimited": "Sen límites",
@@ -380,5 +380,31 @@
"account_basics_phone_numbers_dialog_verify_button_call": "Chámame",
"account_usage_emails_title": "Emails enviados",
"account_basics_phone_numbers_dialog_channel_sms": "SMS",
"subscribe_dialog_login_description": "Este tema está protexido por contrasinal. Por favor, introduza o usuario e contrasinal para subscribirse."
"subscribe_dialog_login_description": "Este tema está protexido por contrasinal. Por favor, escribe as credenciais para subscribirte.",
"action_bar_mute_notifications": "Acalar notificacións",
"action_bar_unmute_notifications": "Reactivar notificacións",
"alert_notification_permission_required_title": "Notificacións desactivadas",
"alert_notification_permission_required_description": "Concederlle permisos ao navegador para mostrar notificacións de escritorio",
"alert_notification_permission_required_button": "Conceder",
"alert_notification_permission_denied_title": "Notificacións bloqueadas",
"alert_notification_permission_denied_description": "Por favor reactívaas no navegador",
"alert_notification_ios_install_required_title": "Require instalación iOS",
"alert_notification_ios_install_required_description": "Preme na icona Compartir e Engadir a Pantalla de Inicio para activar as notificacións en iOS",
"notifications_actions_failed_notification": "Non se puido realizar a acción",
"publish_dialog_checkbox_markdown": "Dar formato Markdow",
"prefs_notifications_web_push_title": "Notificacións en segundo plano",
"prefs_notifications_web_push_enabled_description": "Recíbense notificacións incluso se a app web non está en execución (vía Web Push)",
"prefs_notifications_web_push_disabled_description": "Recíbense as notificacións cando a app web está en execución (vía WebSocket)",
"prefs_notifications_web_push_enabled": "Activadas para {{server}}",
"prefs_notifications_web_push_disabled": "Desactivadas",
"prefs_appearance_theme_title": "Decorado",
"prefs_appearance_theme_system": "Sistema (por defecto)",
"prefs_appearance_theme_dark": "Modo escuro",
"prefs_appearance_theme_light": "Modo claro",
"error_boundary_button_reload_ntfy": "Recargar ntfy",
"web_push_subscription_expiring_title": "Vanse pausar as notificacións",
"web_push_subscription_expiring_body": "Abrir ntfy para seguir recibindo notificacións",
"web_push_unknown_notification_title": "Recibida unha notificación descoñecida desde o servidor",
"web_push_unknown_notification_body": "Poderías ter que actualizar ntfy abrindo a app web",
"subscribe_dialog_subscribe_use_another_background_info": "As notificacións procedentes doutros servidores non se van recibir cando a app web estea pechada"
}

View File

@@ -1,7 +1,7 @@
{
"action_bar_send_test_notification": "Teszt értesítés küldése",
"action_bar_clear_notifications": "Összes értesítés törlése",
"alert_not_supported_description": "A böngésző nem támogatja az értesítések fogadását.",
"alert_not_supported_description": "A böngésződ nem támogatja az értesítések fogadását",
"action_bar_settings": "Beállítások",
"action_bar_unsubscribe": "Leiratkozás",
"message_bar_type_message": "Írd ide az üzenetet",
@@ -9,19 +9,19 @@
"nav_button_all_notifications": "Összes értesítés",
"nav_topics_title": "Feliratkozott témák",
"alert_notification_permission_required_title": "Az értesítések le vannak tiltva",
"alert_notification_permission_required_description": "Engedélyezd a böngészőnek, hogy asztali értesítéseket jeleníttessen meg.",
"alert_notification_permission_required_description": "Engedélyezd a böngésződnek, hogy asztali értesítéseket jelenítsen meg",
"nav_button_settings": "Beállítások",
"nav_button_documentation": "Dokumentáció",
"nav_button_publish_message": "Értesítés küldése",
"alert_notification_permission_required_button": "Engedélyezés",
"alert_not_supported_title": "Nem támogatott funkció",
"notifications_copied_to_clipboard": "Másolva a vágólapra",
"alert_not_supported_title": "Az értesítések nincsenek támogatva",
"notifications_copied_to_clipboard": "Vágólapra másolva",
"notifications_tags": "Címkék",
"notifications_attachment_copy_url_title": "Másolja vágólapra a csatolmány URL-ét",
"notifications_attachment_copy_url_button": "URL másolása",
"notifications_attachment_open_title": "Menjen a(z) {{url}} címre",
"notifications_attachment_open_button": "Csatolmány megnyitása",
"notifications_attachment_link_expired": "A letöltési hivatkozás lejárt",
"notifications_attachment_link_expired": "A letöltési link lejárt",
"notifications_attachment_link_expires": "A hivatkozás {{date}}-kor jár le",
"nav_button_subscribe": "Feliratkozás témára",
"notifications_click_copy_url_title": "Másolja vágólapra a hivatkozás URL-ét",
@@ -187,5 +187,33 @@
"prefs_users_edit_button": "Felhasználó szerkesztése",
"prefs_users_delete_button": "Felhasználó törlése",
"error_boundary_unsupported_indexeddb_title": "Privát böngészés nem támogatott",
"subscribe_dialog_subscribe_base_url_label": "Szolgáltató URL"
"subscribe_dialog_subscribe_base_url_label": "Szolgáltató URL",
"signup_form_username": "Felhasználónév",
"signup_form_password": "Jelszó",
"signup_form_button_submit": "Regisztráció",
"login_form_button_submit": "Bejelentkezés",
"login_link_signup": "Regisztráció",
"login_disabled": "Bejelentkezés kikapcsolva",
"action_bar_change_display_name": "Megjelenített név módosítása",
"action_bar_profile_logout": "Kijelentkezés",
"action_bar_sign_in": "Bejelentkezés",
"action_bar_sign_up": "Regisztráció",
"action_bar_profile_title": "Profil",
"nav_button_account": "Fiók",
"common_copy_to_clipboard": "Másolás vágólapra",
"action_bar_reservation_limit_reached": "Limit elérve",
"login_title": "Jelentkezz be a ntfy felhasználódba",
"signup_title": "Hozz létre egy ntfy felhasználói fiókot",
"signup_form_confirm_password": "Jelszó megerősítése",
"signup_already_have_account": "Már van felhasználód? Jelentkezz be!",
"action_bar_account": "Fiók",
"action_bar_profile_settings": "Beállítások",
"signup_error_username_taken": "A felhasználónév {{username}} már foglalt",
"signup_error_creation_limit_reached": "Felhasználói regisztráció limit elérve",
"action_bar_mute_notifications": "Értesítések némítása",
"action_bar_unmute_notifications": "Értesítések némításának feloldása",
"alert_notification_permission_denied_title": "Az értesítések blokkolva vannak",
"alert_notification_permission_denied_description": "Kérjük kapcsold őket vissza a böngésződben",
"alert_notification_ios_install_required_title": "iOS telepítés szükséges",
"alert_not_supported_context_description": "Az értesítések kizárólag HTTPS-en keresztül támogatottak. Ez a <mdnLink>Notifications API</mdnLink> korlátozása."
}

View File

@@ -24,7 +24,7 @@
"nav_button_subscribe": "Berlangganan ke topik",
"alert_notification_permission_required_title": "Notifikasi dinonaktifkan",
"alert_notification_permission_required_description": "Berikan izin ke peramban untuk menampilkan notifikasi desktop.",
"alert_not_supported_description": "Notifikasi tidak didukung dalam peramban Anda.",
"alert_not_supported_description": "Notifikasi tidak didukung dalam peramban Anda",
"notifications_attachment_open_title": "Pergi ke {{url}}",
"notifications_attachment_open_button": "Buka lampiran",
"notifications_attachment_link_expires": "tautan kadaluwarsa {{date}}",
@@ -381,5 +381,28 @@
"account_upgrade_dialog_tier_features_no_calls": "Tidak ada panggilan telepon",
"account_basics_phone_numbers_dialog_code_label": "Kode verifikasi",
"publish_dialog_call_item": "Panggil nomor telepon {{number}}",
"publish_dialog_chip_call_no_verified_numbers_tooltip": "Tidak ada nomor telepon terverifikasi"
"publish_dialog_chip_call_no_verified_numbers_tooltip": "Tidak ada nomor telepon terverifikasi",
"action_bar_unmute_notifications": "Nyalakan notifikasi",
"alert_notification_permission_denied_title": "Notifikasi sedang diblokir",
"alert_notification_permission_denied_description": "Silakan aktifkan lagi dalam peramban Anda",
"alert_notification_ios_install_required_title": "Pemasangan iOS diperlukan",
"alert_notification_ios_install_required_description": "Klik ikon Bagikan dan Tambahkan ke Layar Beranda untuk mengaktifkan notifikasi di iOS",
"notifications_actions_failed_notification": "Tindakan tidak berhasil",
"publish_dialog_checkbox_markdown": "Format sebagai Markdown",
"prefs_notifications_web_push_title": "Notifikasi latar belakang",
"prefs_notifications_web_push_enabled_description": "Notifikasi diterima bahkan ketika aplikasi web tidak berjalan (melalui Web Push)",
"prefs_notifications_web_push_disabled_description": "Notifikasi diterima ketika aplikasi web berjalan (melalui WebSocket)",
"prefs_appearance_theme_title": "Tema",
"error_boundary_button_reload_ntfy": "Muat ulang ntfy",
"action_bar_mute_notifications": "Matikan notifikasi",
"subscribe_dialog_subscribe_use_another_background_info": "Notifikasi dari server lain tidak akan diterima ketika aplikasi web tidak buka",
"prefs_notifications_web_push_enabled": "Diaktifkan untuk {{server}}",
"prefs_notifications_web_push_disabled": "Dinonaktifkan",
"prefs_appearance_theme_dark": "Mode gelap",
"prefs_appearance_theme_system": "Sistem (bawaan)",
"prefs_appearance_theme_light": "Mode terang",
"web_push_subscription_expiring_title": "Notifikasi akan dijeda",
"web_push_subscription_expiring_body": "Buka ntfy untuk terus menerima notifikasi",
"web_push_unknown_notification_title": "Notifikasi yang tidak diketahui diterima dari server",
"web_push_unknown_notification_body": "Anda mungkin harus memperbarui ntfy dengan membuka aplikasi web"
}

View File

@@ -18,7 +18,7 @@
"alert_notification_permission_required_title": "Le notifiche sono disabilitate",
"alert_notification_permission_required_button": "Concedi ora",
"notifications_list": "Elenco notifiche",
"notifications_list_item": "Notifiche",
"notifications_list_item": "Notifica",
"notifications_mark_read": "Segna come letto",
"notifications_delete": "Elimina",
"notifications_copied_to_clipboard": "Copiato negli appunti",
@@ -152,10 +152,10 @@
"error_boundary_unsupported_indexeddb_title": "Navigazione privata non supportata",
"action_bar_show_menu": "Mostra menu",
"action_bar_send_test_notification": "Inviare una notifica di prova",
"alert_not_supported_description": "Le notifiche non sono supportate nel tuo browser.",
"alert_not_supported_description": "Le notifiche non sono supportate nel tuo browser",
"nav_button_documentation": "Documentazione",
"notifications_actions_http_request_title": "Invia HTTP {{method}} a {{url}}",
"alert_notification_permission_required_description": "Concedi al tuo browser l'autorizzazione a visualizzare le notifiche sul desktop.",
"alert_notification_permission_required_description": "Concedi al tuo browser l'autorizzazione a visualizzare le notifiche sul desktop",
"alert_not_supported_title": "Notifiche non supportate",
"notifications_attachment_file_app": "file app Android",
"notifications_no_subscriptions_description": "Fai clic sul link \"{{linktext}}\" per creare o iscriverti a un topic. Successivamente, puoi inviare messaggi tramite PUT o POST e riceverai le notifiche qui.",
@@ -307,5 +307,101 @@
"account_delete_dialog_label": "Password",
"account_upgrade_dialog_tier_features_no_reservations": "Nessun argomento riservato",
"account_upgrade_dialog_tier_features_messages_one": "{{messages}} messaggi giornalieri",
"account_upgrade_dialog_reservations_warning_one": "Il livello selezionato consente meno argomenti riservati rispetto al livello corrente. Prima di cambiare il livello, <strong> si prega di eliminare almeno una prenotazione</strong>. È possibile rimuovere le prenotazioni nel <Link>Impostazioni</Link>."
"account_upgrade_dialog_reservations_warning_one": "Il livello selezionato consente meno argomenti riservati rispetto al livello corrente. Prima di cambiare il livello, <strong> si prega di eliminare almeno una prenotazione</strong>. È possibile rimuovere le prenotazioni nel <Link>Impostazioni</Link>.",
"alert_notification_permission_denied_title": "Le notifiche sono bloccate",
"alert_notification_permission_denied_description": "Per favore riabilitale nel tuo browser",
"subscribe_dialog_subscribe_use_another_background_info": "Le notifiche dagli altri server non saranno ricevute quando la web app non è in esecuzione",
"error_boundary_button_reload_ntfy": "Ricarica ntfy",
"action_bar_mute_notifications": "Silenzia notifiche",
"action_bar_unmute_notifications": "Riattiva audio notifiche",
"alert_notification_ios_install_required_title": "E' richiesta l'installazione di iOS",
"alert_notification_ios_install_required_description": "Fare clic sull'icona Condividi e Aggiungi alla schermata home per abilitare le notifiche su iOS",
"publish_dialog_checkbox_markdown": "Formatta come markdown",
"account_upgrade_dialog_interval_yearly": "Annualmente",
"account_tokens_table_token_header": "Token",
"account_tokens_table_label_header": "Etichetta",
"account_tokens_table_cannot_delete_or_edit": "Impossibile modificare o eliminare il token della sessione corrente",
"account_tokens_dialog_label": "Etichetta, ad esempio Notifiche Radarr",
"account_tokens_dialog_title_delete": "Elimina token di accesso",
"account_tokens_dialog_title_edit": "Modifica token di accesso",
"account_tokens_dialog_button_create": "Crea token",
"account_tokens_dialog_button_update": "Aggiorna token",
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} e-mails giornaliere",
"account_upgrade_dialog_tier_features_messages_other": "{{messages}} messaggi giornalieri",
"account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} per file",
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} spazio di archiviazione totale",
"notifications_actions_failed_notification": "Azione non riuscita",
"account_usage_attachment_storage_description": "{{filesize}} per file, eliminato dopo {{expiry}}",
"account_upgrade_dialog_title": "Cambia livello account",
"account_upgrade_dialog_interval_monthly": "Mensilmente",
"account_upgrade_dialog_cancel_warning": "Questa azione <strong>annullerà il tuo abbonamento</strong> e declasserà il tuo account il {{date}}. In quella data, le prenotazioni degli argomenti e i messaggi memorizzati nella cache del server <strong>verranno eliminati</strong>.",
"account_upgrade_dialog_reservations_warning_other": "Il livello selezionato consente meno argomenti riservati rispetto al livello attuale. Prima di cambiare il livello, <strong>elimina almeno {{count}} prenotazioni</strong>. Puoi rimuovere le prenotazioni nelle <Link>Impostazioni</Link>.",
"account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} argomenti riservati",
"account_upgrade_dialog_tier_features_emails_other": "{{emails}} e-mail giornaliere",
"account_upgrade_dialog_tier_features_calls_one": "{{calls}} telefonate giornaliere",
"account_upgrade_dialog_tier_features_calls_other": "{{calls}} telefonate giornaliere",
"account_upgrade_dialog_tier_features_no_calls": "Nessuna telefonata",
"account_tokens_description": "Utilizza i token di accesso quando pubblichi e ti iscrivi tramite l'API ntfy, così non dovrai inviare le credenziali del tuo account. Consulta la <Link>documentazione</Link> per saperne di più.",
"account_tokens_table_copied_to_clipboard": "Token di accesso copiato",
"account_tokens_table_create_token_button": "Crea token di accesso",
"account_tokens_table_last_origin_tooltip": "Dall'indirizzo IP {{ip}}, clicca per cercare",
"account_tokens_dialog_title_create": "Crea token di accesso",
"account_tokens_dialog_button_cancel": "Annulla",
"web_push_unknown_notification_body": "Potrebbe essere necessario aggiornare ntfy aprendo l'app web",
"account_upgrade_dialog_proration_info": "<strong>Prorata</strong>: quando si esegue l'upgrade tra piani a pagamento, la differenza di prezzo verrà <strong>addebitata immediatamente</strong>. Quando si esegue il downgrade a un livello inferiore, il saldo verrà utilizzato per pagare i periodi di fatturazione futuri.",
"account_tokens_table_last_access_header": "Ultimo accesso",
"account_tokens_table_expires_header": "Scade",
"account_tokens_table_never_expires": "Non scade mai",
"account_tokens_table_current_session": "Sessione corrente del browser",
"account_upgrade_dialog_interval_yearly_discount_save_up_to": "Risparmia fino al {{discount}}%",
"account_upgrade_dialog_interval_yearly_discount_save": "conserva {{discount}}%",
"prefs_users_description_no_sync": "Gli utenti e le password non vengono sincronizzati con il tuo account.",
"prefs_reservations_title": "Argomenti riservati",
"prefs_reservations_table_access_header": "Accesso",
"reservation_delete_dialog_action_delete_title": "Elimina i messaggi e gli allegati memorizzati nella cache",
"reservation_delete_dialog_submit_button": "Elimina prenotazione",
"account_tokens_dialog_expires_label": "Il token di accesso scade tra",
"account_tokens_dialog_expires_unchanged": "Lascia la data di scadenza invariata",
"account_tokens_delete_dialog_submit_button": "Elimina definitivamente il token",
"prefs_reservations_description": "Qui puoi riservare i nomi degli argomenti per uso personale. Riservare un argomento ti dà la proprietà dell'argomento e ti consente di definire i permessi di accesso per altri utenti sull'argomento.",
"prefs_reservations_add_button": "Aggiungi argomento riservato",
"prefs_reservations_edit_button": "Modifica accesso argomento",
"prefs_reservations_delete_button": "Reimposta accesso argomento",
"prefs_reservations_table_everyone_read_only": "Posso pubblicare e iscrivermi, tutti possono iscriversi",
"prefs_reservations_table_not_subscribed": "Non iscritto",
"prefs_reservations_table_everyone_write_only": "Posso pubblicare ed iscrivermi, tutti possono pubblicare",
"prefs_reservations_table_everyone_read_write": "Tutti possono pubblicare e iscriversi",
"prefs_reservations_dialog_title_delete": "Elimina prenotazione argomento",
"prefs_reservations_dialog_description": "Prenotando un argomento ne diventi proprietario e puoi definire le autorizzazioni di accesso per altri utenti.",
"reservation_delete_dialog_action_keep_description": "I messaggi e gli allegati memorizzati nella cache del server diventeranno visibili al pubblico per le persone a conoscenza del nome dell'argomento.",
"reservation_delete_dialog_action_delete_description": "I messaggi e gli allegati memorizzati nella cache verranno eliminati definitivamente. Questa azione non può essere annullata.",
"prefs_reservations_limit_reached": "Hai raggiunto il limite di argomenti riservati.",
"prefs_reservations_table_click_to_subscribe": "Clicca per iscriverti",
"prefs_reservations_dialog_title_add": "Prenota argomento",
"prefs_reservations_dialog_title_edit": "Modifica argomento riservato",
"account_tokens_dialog_expires_x_days": "Il token scade tra {{days}} giorni",
"account_tokens_dialog_expires_never": "Il token non scade mai",
"account_tokens_delete_dialog_title": "Elimina token di accesso",
"account_tokens_delete_dialog_description": "Prima di eliminare un token di accesso, assicurati che nessuna applicazione o script lo stia utilizzando attivamente. <strong>Questa azione non può essere annullata</strong>.",
"prefs_notifications_web_push_title": "Notifiche in background",
"prefs_notifications_web_push_enabled_description": "Le notifiche vengono ricevute anche quando l'app Web non è in esecuzione (tramite Web Push)",
"prefs_notifications_web_push_disabled_description": "Le notifiche vengono ricevute quando l'app Web è in esecuzione (tramite WebSocket)",
"prefs_notifications_web_push_enabled": "Abilitato per {{server}}",
"prefs_notifications_web_push_disabled": "Disabilitato",
"prefs_users_table_cannot_delete_or_edit": "Impossibile eliminare o modificare l'utente registrato",
"prefs_appearance_theme_title": "Tema",
"prefs_appearance_theme_system": "Sistema (predefinito)",
"prefs_appearance_theme_dark": "Modalità scura",
"prefs_appearance_theme_light": "Modalità chiara",
"prefs_reservations_table_topic_header": "Argomento",
"prefs_reservations_dialog_access_label": "Accesso",
"reservation_delete_dialog_description": "La rimozione di una prenotazione comporta la rinuncia alla proprietà dell'argomento e consente ad altri di riservarlo. Puoi mantenere o eliminare i messaggi e gli allegati esistenti.",
"prefs_reservations_table_everyone_deny_all": "Solo io posso pubblicare e iscrivermi",
"prefs_reservations_dialog_topic_label": "Argomento",
"reservation_delete_dialog_action_keep_title": "Mantieni i messaggi e gli allegati memorizzati nella cache",
"web_push_subscription_expiring_title": "Le notifiche verranno sospese",
"web_push_subscription_expiring_body": "Apri ntfy per continuare a ricevere notifiche",
"web_push_unknown_notification_title": "Notifica sconosciuta ricevuta dal server",
"account_tokens_dialog_expires_x_hours": "Il token scade tra {{hours}} ore",
"prefs_reservations_table": "Tabella argomenti riservati"
}

View File

@@ -7,7 +7,7 @@
"action_bar_clear_notifications": "全ての通知を消去",
"action_bar_unsubscribe": "購読解除",
"nav_button_documentation": "ドキュメント",
"alert_not_supported_description": "通知機能はこのブラウザではサポートされていません",
"alert_not_supported_description": "通知機能はこのブラウザではサポートされていません",
"notifications_copied_to_clipboard": "クリップボードにコピーしました",
"notifications_example": "例",
"publish_dialog_title_topic": "{{topic}}に送信",
@@ -28,7 +28,7 @@
"message_bar_type_message": "メッセージを入力してください",
"nav_topics_title": "購読しているトピック",
"nav_button_subscribe": "トピックを購読",
"alert_notification_permission_required_description": "ブラウザのデスクトップ通知を許可してください",
"alert_notification_permission_required_description": "ブラウザのデスクトップ通知を許可してください",
"alert_notification_permission_required_button": "許可する",
"notifications_attachment_link_expires": "リンクは {{date}} に失効します",
"notifications_click_copy_url_button": "リンクをコピー",
@@ -191,7 +191,7 @@
"signup_form_username": "ユーザー名",
"signup_form_password": "パスワード",
"signup_form_confirm_password": "パスワードを確認",
"signup_already_have_account": "アカウントをお持ちならサインイン",
"signup_already_have_account": "アカウントをお持ちならサインイン",
"signup_disabled": "サインアップは無効化されています",
"signup_error_creation_limit_reached": "アカウント作成制限に達しました",
"login_title": "あなたのntfyアカウントにサインイン",
@@ -380,5 +380,28 @@
"account_upgrade_dialog_tier_features_calls_other": "電話 1日 {{calls}} 回",
"publish_dialog_chip_call_no_verified_numbers_tooltip": "認証済み電話番号がありません",
"account_basics_phone_numbers_dialog_channel_sms": "SMS",
"account_basics_phone_numbers_dialog_channel_call": "電話する"
"account_basics_phone_numbers_dialog_channel_call": "電話する",
"error_boundary_button_reload_ntfy": "ntfyをリロード",
"prefs_appearance_theme_light": "ライトモード",
"web_push_subscription_expiring_title": "通知は一時停止されます",
"web_push_subscription_expiring_body": "ntfyを開いて通知の受信を継続させてください",
"alert_notification_ios_install_required_description": "Shareアイコンをクリック・ホーム画面に追加してiOSでの通知を有効化して下さい",
"action_bar_mute_notifications": "通知をミュート",
"action_bar_unmute_notifications": "通知ミュートを解除",
"alert_notification_permission_denied_title": "通知はブロックされています",
"alert_notification_permission_denied_description": "ブラウザで通知を再度有効化してください",
"notifications_actions_failed_notification": "アクション失敗",
"alert_notification_ios_install_required_title": "iOS用インストールが必要です",
"publish_dialog_checkbox_markdown": "Markdownとして表示",
"subscribe_dialog_subscribe_use_another_background_info": "ウェブアプリが開かれていない場合は他のサーバーからの通知は受信されません",
"prefs_notifications_web_push_title": "バックグラウンド通知",
"prefs_notifications_web_push_enabled_description": "ウェブアプリが開かれていなくても通知を受信します (Web Push経由)",
"prefs_notifications_web_push_disabled_description": "ウェブアプリが開かれていなくても通知を受信します (WebSocket経由)",
"prefs_notifications_web_push_enabled": "{{server}}で有効",
"prefs_notifications_web_push_disabled": "無効",
"prefs_appearance_theme_title": "テーマ",
"prefs_appearance_theme_system": "システム (既定)",
"prefs_appearance_theme_dark": "ダークモード",
"web_push_unknown_notification_title": "不明な通知を受信しました",
"web_push_unknown_notification_body": "ウェブアプリを開いてntfyをアップデートする必要があります"
}

View File

@@ -0,0 +1,190 @@
{
"signup_disabled": "Pendaftaran dilumpuhkan",
"signup_error_username_taken": "Nama pengguna {{username}} telah digunakan",
"signup_error_creation_limit_reached": "Pendaftaran sudah melebihi had",
"login_form_button_submit": "Log masuk",
"login_disabled": "Log masuk dilumpuhkan",
"action_bar_show_menu": "Tunjuk menu",
"action_bar_logo_alt": "logo ntfy",
"action_bar_settings": "Tetapan",
"action_bar_account": "Akaun",
"action_bar_change_display_name": "Tukar nama paparan",
"action_bar_reservation_add": "Tempah topik",
"action_bar_reservation_edit": "Tukar tempahan",
"action_bar_reservation_delete": "Batalkan tempahan",
"action_bar_reservation_limit_reached": "Melebihi had",
"action_bar_mute_notifications": "Senyapkan notifikasi",
"action_bar_toggle_action_menu": "Buka/tutup menu aksi",
"action_bar_profile_settings": "Tetapan",
"action_bar_profile_logout": "Log keluar",
"action_bar_sign_in": "Log masuk",
"action_bar_sign_up": "Daftar",
"message_bar_type_message": "Tulis mesej disini",
"message_bar_show_dialog": "Tunjuk dialog terterbit",
"message_bar_publish": "Hantar mesej",
"nav_topics_title": "Topik terlanggan",
"nav_button_all_notifications": "Semua notifikasi",
"nav_button_account": "Akaun",
"nav_button_settings": "Tetapan",
"nav_button_documentation": "Dokumentasi",
"nav_button_publish_message": "Terbitkan notifikasi",
"nav_button_subscribe": "Melanggan topik",
"nav_button_muted": "Notifikasi disenyapkan",
"nav_button_connecting": "menyambung",
"nav_upgrade_banner_label": "Naik taraf kepada ntfy Pro",
"nav_upgrade_banner_description": "Tempah topik, lebih banyak mesej & e-mel dan lampiran yang lebih besar",
"alert_notification_permission_required_button": "Benarkan",
"alert_notification_permission_denied_description": "Sila benarkan semula di pelayar anda",
"alert_notification_ios_install_required_title": "Perlukan muatan iOS",
"notifications_tags": "Tag",
"notifications_priority_x": "Keutamaan {{priority}}",
"notifications_new_indicator": "Notifikasi baharu",
"notifications_attachment_copy_url_button": "Salin URL",
"notifications_attachment_link_expires": "pautan tamat tempoh pada {{date}}",
"notifications_attachment_link_expired": "link muat turun telah tamat tempoh",
"notifications_attachment_file_image": "fail imej",
"notifications_attachment_file_video": "fail video",
"notifications_attachment_file_audio": "fail audio",
"notifications_attachment_file_app": "Fail aplikasi Android",
"notifications_attachment_file_document": "lain-lain dokumen",
"notifications_click_copy_url_title": "Salin pautan URL ke papan klip",
"notifications_click_copy_url_button": "Salin pautan",
"notifications_click_open_button": "Buka pautan",
"notifications_actions_open_url_title": "Pergi ke {{url}}",
"notifications_actions_not_supported": "Tindakan tidak disokong di aplikasi web",
"notifications_actions_http_request_title": "Hantar {{method}} HTTP ke {{url}}",
"notifications_actions_failed_notification": "Tindakan tidak berjaya",
"notifications_none_for_topic_title": "Anda belum menerima sebarang pemberitahuan untuk topik ini lagi.",
"notifications_example": "Contoh",
"display_name_dialog_title": "Tukar nama paparan",
"display_name_dialog_placeholder": "Nama paparan",
"common_cancel": "Batal",
"common_back": "Kembali",
"common_save": "Simpan",
"common_add": "Tambah",
"signup_form_toggle_password_visibility": "Tunjuk/sembunyikan kata laluan",
"action_bar_send_test_notification": "Hantar notifikasi percubaan",
"action_bar_toggle_mute": "Senyap/nyahsenyapkan notifikasi",
"common_copy_to_clipboard": "Salin ke papan klip",
"signup_title": "Cipta akaun baru ntfy",
"login_link_signup": "Daftar",
"signup_form_username": "Nama pengguna",
"signup_form_confirm_password": "Pengesahan kata laluan",
"signup_already_have_account": "Sudah daftar? Log masuk disini!",
"action_bar_clear_notifications": "Padam semua notifikasi",
"signup_form_password": "Kata laluan",
"signup_form_button_submit": "Daftar",
"login_title": "Log masuk ke akaun ntfy",
"action_bar_unmute_notifications": "Nyahsenyapkan notifikasi",
"action_bar_unsubscribe": "Nyahlanggan",
"action_bar_profile_title": "Profil",
"message_bar_error_publishing": "Ralat menerbitkan notifikasi",
"alert_notification_permission_required_title": "Notifikasi telah dinyahkan",
"notifications_mark_read": "Tanda sebagai telah dibaca",
"alert_notification_permission_required_description": "Berikan kebenaran pelayar anda untuk memaparkan pemberitahuan desktop",
"alert_notification_permission_denied_title": "Notifikasi disekat",
"notifications_delete": "Padam",
"notifications_copied_to_clipboard": "Salin ke papan klip",
"notifications_attachment_image": "Imej lampiran",
"alert_notification_ios_install_required_description": "Klik pada ikon Kongsi dan Tambah ke Skrin Utama untuk membenarkan pemberitahuan pada iOS",
"alert_not_supported_title": "Notifikasi tidak disokong",
"alert_not_supported_description": "Notifikasi tidak disokong di pelayar anda",
"notifications_list": "Senarai notifikasi",
"notifications_list_item": "Notifikasi",
"notifications_attachment_copy_url_title": "Salin URL lampiran ke papan klip",
"notifications_attachment_open_title": "Pergi ke {{url}}",
"notifications_attachment_open_button": "Buka lampiran",
"notifications_none_for_topic_description": "Untuk menghantar pemberitahuan kepada topik ini, hanya PUT atau POST ke URL topik.",
"notifications_no_subscriptions_title": "Nampaknya anda belum mempunyai sebarang langganan lagi.",
"notifications_none_for_any_title": "Anda tidak menerima sebarang notifikasi.",
"notifications_none_for_any_description": "Untuk menghantar pemberitahuan kepada topik, hanya PUT atau POST ke URL topik. Berikut ialah contoh menggunakan salah satu topik anda.",
"account_basics_phone_numbers_copied_to_clipboard": "Nombor telefon disalin ke papan klip",
"account_basics_phone_numbers_dialog_verify_button_sms": "Hantar SMS",
"account_basics_phone_numbers_dialog_verify_button_call": "Telefon diri sendiri",
"account_basics_phone_numbers_dialog_code_label": "Kod verifikasi",
"account_basics_phone_numbers_dialog_channel_call": "Panggil",
"account_usage_title": "Penggunaan",
"account_usage_of_limit": ": {{limit}}",
"account_usage_unlimited": "Tanpa had",
"account_usage_limits_reset_daily": "Had penggunaan akan di set semula pada tengah malam (UTC)",
"account_basics_tier_title": "Jenis akaun",
"account_basics_tier_description": "Tahap kekuatan akaun anda",
"account_basics_tier_admin_suffix_with_tier": "(dengan peringkat {{tier}})",
"account_basics_tier_admin_suffix_no_tier": "(tiada peringkat)",
"account_basics_tier_basic": "Asas",
"account_basics_tier_free": "Percuma",
"account_basics_tier_change_button": "Ubah",
"account_delete_title": "Padam akaun",
"account_upgrade_dialog_tier_features_calls_other": "{{calls}} paggilan harian",
"account_upgrade_dialog_tier_features_no_calls": "Tiada panggilan",
"account_upgrade_dialog_tier_price_per_month": "bulan",
"account_tokens_table_token_header": "Token",
"account_tokens_table_label_header": "Tahap",
"account_tokens_table_never_expires": "Tidak pernah luput",
"account_tokens_table_expires_header": "Luput",
"account_tokens_table_current_session": "Sesi pelayar semasa",
"account_tokens_table_copied_to_clipboard": "Token akses telah disalin",
"account_tokens_table_cannot_delete_or_edit": "TIdak boleh ubah atau padam token sesi semasa",
"account_basics_phone_numbers_dialog_title": "Tambah nombor telefon",
"account_basics_phone_numbers_dialog_number_label": "Nombor telefon",
"account_basics_phone_numbers_dialog_number_placeholder": "cth: +1222333444",
"account_basics_phone_numbers_dialog_code_placeholder": "cth: 123456",
"account_basics_tier_admin": "Pengurus",
"account_basics_phone_numbers_dialog_check_verification_button": "Kod pengesahan",
"account_basics_phone_numbers_dialog_channel_sms": "SMS",
"account_basics_tier_interval_monthly": "bulanan",
"account_basics_tier_interval_yearly": "tahunan",
"account_basics_tier_upgrade_button": "Naik taraf ke Pro",
"account_basics_tier_manage_billing_button": "Urus cara pembayaran",
"account_basics_tier_paid_until": "Langganan telah dibayar sehingga {{date}}, dan akan diperbaharui secara automatik",
"account_basics_tier_payment_overdue": "Bayaran anda tertunggak. Sila kemas kini kaedah pembayaran anda atau akaun anda akan diturunkan tidak lama lagi.",
"account_basics_tier_canceled_subscription": "Langganan anda telah dibatalkan dan akan diturunkan taraf kepada akaun percuma pada {{date}}.",
"account_usage_attachment_storage_description": "{{filesize}} setiap fail, akan dipadam selepas {{expiry}}",
"account_upgrade_dialog_interval_monthly": "Bulanan",
"account_usage_messages_title": "Mesej yang telah diterbitkan",
"account_usage_emails_title": "Email telah dihantar",
"account_usage_calls_title": "Panggilan telefon",
"account_usage_calls_none": "Tiada panggilan telefon boleh dibuat dengan akaun ini",
"account_usage_reservations_none": "Tiada topik simpanan untuk akaun ini",
"account_usage_reservations_title": "Topik simpanan",
"account_usage_attachment_storage_title": "Simpanan lampiran",
"account_delete_dialog_button_cancel": "Batal",
"account_usage_cannot_create_portal_session": "Tidak dapat membuka portal pengebilan",
"account_delete_description": "Padam akaun selamanya",
"account_delete_dialog_label": "Kata laluan",
"account_delete_dialog_button_submit": "Padamkan akaun secara kekal",
"account_upgrade_dialog_title": "Tukar peringkat akaun",
"account_delete_dialog_description": "Ini akan memadamkan akaun anda secara kekal, termasuk semua data yang disimpan pada pelayan. Selepas pemadaman, nama pengguna anda tidak akan tersedia selama 7 hari. Jika anda benar-benar mahu meneruskan, sila sahkan dengan kata laluan anda dalam kotak di bawah.",
"account_upgrade_dialog_interval_yearly": "Tahunan",
"account_delete_dialog_billing_warning": "Memadamkan akaun anda turut membatalkan langganan pengebilan anda serta-merta. Anda tidak akan mempunyai akses kepada papan pemuka pengebilan lagi.",
"account_upgrade_dialog_interval_yearly_discount_save": "jimat {{discount}}%",
"account_upgrade_dialog_interval_yearly_discount_save_up_to": "jimat sehingga {{discount}}%",
"account_upgrade_dialog_cancel_warning": "Ini akan <strong>membatalkan langganan anda</strong> dan menurunkan taraf akaun anda pada {{date}}. Pada tarikh tersebut, tempahan topik serta mesej yang dicache pada pelayan <strong>akan dipadamkan</strong>.",
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} email harian",
"account_upgrade_dialog_proration_info": "<strong>Tambahan</strong>: Apabila menaik taraf antara pelan berbayar, perbezaan harga akan <strong>caj serta-merta</strong>. Apabila menurunkan taraf kepada peringkat yang lebih rendah, baki akan digunakan untuk membayar bagi tempoh pengebilan akan datang.",
"account_tokens_table_create_token_button": "Cipta token akses",
"account_tokens_table_last_origin_tooltip": "Daripada alamat IP {{ip}}, klik untuk mencari",
"account_upgrade_dialog_tier_features_reservations_one": "{{reservation}} topik tersimpan",
"account_upgrade_dialog_tier_features_messages_one": "{{messages}} mesej harian",
"account_upgrade_dialog_tier_features_reservations_other": "{{reservation}} topik tersimpan",
"account_upgrade_dialog_tier_features_messages_other": "{{messages}} mesej harian",
"account_upgrade_dialog_tier_features_no_reservations": "Tiada topik tersimpan",
"account_upgrade_dialog_tier_features_emails_other": "{{emails}} email harian",
"account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} setiap fail",
"account_upgrade_dialog_tier_features_calls_one": "{{calls}} paggilan harian",
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} jumlah simpanan",
"account_upgrade_dialog_tier_price_billed_monthly": "{{price}} setahun. Dibilkan setiap bulan.",
"account_upgrade_dialog_tier_selected_label": "Pilih",
"account_upgrade_dialog_tier_price_billed_yearly": "{{price}} dibilkan setiap tahun. Jimat {{simpan}}.",
"account_upgrade_dialog_tier_current_label": "Semasa",
"account_upgrade_dialog_billing_contact_website": "Untuk pertanyaan pengebilan, sila rujuk <Link>laman web</Link> kami.",
"account_upgrade_dialog_button_cancel": "Batal",
"account_upgrade_dialog_billing_contact_email": "Untuk pertanyaan pengebilan, sila <Link>hubungi kami</Link> secara terus.",
"account_upgrade_dialog_button_redirect_signup": "Daftar sekarang",
"account_upgrade_dialog_button_cancel_subscription": "Batalkan langganan",
"account_upgrade_dialog_button_update_subscription": "Kemas kini langganan",
"account_upgrade_dialog_button_pay_now": "Bayar sekarang dan langgan",
"account_tokens_title": "Token akses",
"account_tokens_description": "Gunakan token akses semasa menerbitkan dan melanggan melalui API ntfy, jadi anda tidak perlu menghantar bukti kelayakan akaun anda. Lihat <Link>dokumentasi</Link> untuk mengetahui lebih lanjut.",
"account_tokens_table_last_access_header": "Akses terakhir"
}

View File

@@ -3,7 +3,7 @@
"action_bar_settings": "Innstillinger",
"action_bar_send_test_notification": "Send testmerknad",
"action_bar_clear_notifications": "Tøm alle merknader",
"action_bar_unsubscribe": "Opphev abonnement",
"action_bar_unsubscribe": "Meld av",
"message_bar_type_message": "Skriv en melding her",
"nav_button_all_notifications": "Alle merknader",
"nav_button_settings": "Innstillinger",
@@ -133,8 +133,8 @@
"publish_dialog_chip_delay_label": "Forsink leveringen",
"publish_dialog_details_examples_description": "For eksempler og en detaljert beskrivelse av alle sendefunksjoner, se <docsLink>dokumentasjonen</docsLink>.",
"publish_dialog_base_url_placeholder": "Tjeneste-URL, f.eks. https://example.com",
"alert_notification_permission_required_description": "Gi nettleseren din tillatelse til å vise skrivebordsvarsler.",
"alert_not_supported_description": "Varsler støttes ikke i nettleseren din.",
"alert_notification_permission_required_description": "Gi nettleseren din tillatelse til å vise skrivebordsvarsler",
"alert_not_supported_description": "Varsler støttes ikke i nettleseren din",
"notifications_attachment_file_app": "Android-app-fil",
"notifications_no_subscriptions_description": "Klikk på \"{{linktext}}\"-koblingen for å opprette eller abonnere på et emne. Etter det kan du sende meldinger via PUT eller POST, og du vil motta varsler her.",
"notifications_actions_http_request_title": "Send HTTP {{metode}} til {{url}}",
@@ -195,5 +195,213 @@
"signup_form_username": "Brukernavn",
"signup_form_password": "Passord",
"signup_form_button_submit": "Meld deg på",
"signup_form_confirm_password": "Bekreft passord"
"signup_form_confirm_password": "Bekreft passord",
"signup_disabled": "Registrering er deaktivert",
"common_copy_to_clipboard": "Kopier til utklippstavle",
"signup_form_toggle_password_visibility": "Slå av/på passordvisning",
"signup_already_have_account": "Har du allerede en konto? Logg inn!",
"signup_error_username_taken": "Brukernavnet {{username}} er allerede opptatt",
"signup_error_creation_limit_reached": "Grense for nye kontoer nådd",
"login_title": "Logg inn på ntfy-kontoen din",
"login_form_button_submit": "Logg inn",
"login_link_signup": "Registrer deg",
"login_disabled": "Innlogging deaktivert",
"action_bar_change_display_name": "Endre visningsnavn",
"account_basics_tier_interval_yearly": "årlig",
"account_basics_tier_change_button": "Endre",
"account_usage_reservations_title": "Reserverte emner",
"account_usage_cannot_create_portal_session": "Kunne ikke åpne betalingsportalen",
"account_delete_dialog_label": "Passord",
"account_tokens_table_copied_to_clipboard": "Tilgangstoken kopiert",
"account_tokens_table_last_origin_tooltip": "Fra IP-adresse {{ip}}, klikk for å gjøre oppslag",
"account_tokens_dialog_title_create": "Opprett tilgangstoken",
"account_tokens_delete_dialog_title": "Slett tilgangstoken",
"prefs_users_table_cannot_delete_or_edit": "Kan ikke slette eller redigere innlogget bruker",
"prefs_reservations_table_everyone_deny_all": "Bare jeg kan publisere og abonnere",
"prefs_reservations_dialog_access_label": "Tilgang",
"reservation_delete_dialog_action_keep_title": "Behold mellomlagrede meldinger og vedlegg",
"action_bar_reservation_add": "Reserver emne",
"action_bar_reservation_edit": "Endre reservasjon",
"action_bar_reservation_delete": "Fjern reservasjon",
"action_bar_reservation_limit_reached": "Grense nådd",
"account_basics_phone_numbers_dialog_description": "For å bruke ringevarslingsfunksjonen må du legge til og verifisere minst ett telefonnummer. Verifisering kan gjøres vis SMS eller oppringing.",
"account_basics_tier_interval_monthly": "månedlig",
"account_basics_tier_upgrade_button": "Oppgrader til Pro",
"account_usage_emails_title": "E-poster sendt",
"account_delete_description": "Slett kontoen din permanent",
"account_usage_calls_title": "Telefonsamtaler",
"account_upgrade_dialog_interval_monthly": "Månedlig",
"account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} reserverte emner",
"account_upgrade_dialog_tier_features_messages_other": "{{messages}} daglige meldinger",
"account_upgrade_dialog_tier_features_emails_other": "{{emails}} daglige e-poster",
"account_upgrade_dialog_tier_features_calls_one": "{{calls}} daglige telefonsamtaler",
"account_upgrade_dialog_tier_selected_label": "Valgt",
"account_upgrade_dialog_tier_current_label": "Nåværende",
"account_upgrade_dialog_button_cancel": "Avbryt",
"account_upgrade_dialog_billing_contact_email": "For faktureringsspørsmål, vennligst <Link>kontakt oss</Link> direkte.",
"account_tokens_table_token_header": "Token",
"account_tokens_table_label_header": "Etikett",
"account_tokens_table_cannot_delete_or_edit": "Kan ikke redigere eller slette nåværende økt-token",
"account_tokens_table_create_token_button": "Opprett tilgangstoken",
"account_tokens_dialog_expires_unchanged": "La utløpsdato være uendret",
"account_tokens_dialog_expires_x_hours": "Token utløper om {{hours}} timer",
"account_tokens_delete_dialog_description": "Før du sletter et tilgangstoken, sørg for at ingen applikasjoner eller script bruker det. <strong>Denne handlingen kan ikke angres</strong>.",
"account_tokens_delete_dialog_submit_button": "Slett token permanent",
"prefs_users_description_no_sync": "Brukere og passord synkroniseres ikke til kontoen din.",
"prefs_reservations_dialog_title_delete": "Slett emnereservasjon",
"prefs_reservations_dialog_topic_label": "Emne",
"display_name_dialog_title": "Endre visningsnavn",
"reserve_dialog_checkbox_label": "Rserver emne og sett opp tilgang",
"publish_dialog_chip_call_label": "Telefonsamtale",
"account_basics_tier_free": "Gratis",
"account_basics_tier_basic": "Grunnleggende",
"account_basics_tier_canceled_subscription": "Abonnementet ditt ble avsluttet og blir degradert til en gratiskonto den {{date}}.",
"account_delete_dialog_description": "Dette vil slette kontoen din permanent, inkludert alle data som er lagret på serveren. Etter sletting vil brukernavnet ditt være utilgjengelig i 7 dager. Hvis du virkelig vil fortsette, vennligst bekreft ved å skrive passordet ditt i boksen under.",
"account_upgrade_dialog_proration_info": "<strong>Pro-rate</strong>: Når du oppgraderer mellom betalte kontotyper, vil prisforskjellen <strong>bli fakturert umiddelbart</strong>. Når du nedgraderer til en billigere kontotype, vil det allerede innbetalte beløpet brukes til å betale for fremtidige regningsperioder.",
"account_upgrade_dialog_reservations_warning_other": "Det valgte nivået tillater færre reserverte emner enn ditt nåvære nivå. Før du endrer nivå, <strong>vennligst slett minst {{count}} reservasjoner</strong>. Du kan slette reservasjoner i <Link>Innstillingene</Link>.",
"account_upgrade_dialog_tier_features_messages_one": "{{messages}} daglig melding",
"account_upgrade_dialog_tier_price_billed_monthly": "{{price}} pr. år. Fakturert månedlig.",
"account_upgrade_dialog_button_redirect_signup": "Registrer deg nå",
"account_upgrade_dialog_button_pay_now": "Betal nå og abonner",
"account_upgrade_dialog_button_cancel_subscription": "Avslutt abonnement",
"account_tokens_description": "Bruk tilgangstokener når du publiserer og abonnerer via ntfy-APIet, slik at du ikke trenger å sende innloggingsinformasjon for kontoen din. Se <Link>dokumentasjonen</Link> for å lære mer.",
"account_tokens_table_current_session": "Nåværende nettleserøkt",
"prefs_appearance_theme_system": "System (standard)",
"prefs_notifications_web_push_disabled_description": "Varslinger mottas når web-appen kjører (via WebSocket)",
"prefs_appearance_theme_title": "Tema",
"prefs_appearance_theme_dark": "Mørk modus",
"prefs_appearance_theme_light": "Lys modus",
"prefs_reservations_title": "Reserverte emner",
"prefs_reservations_table_click_to_subscribe": "Klikk for å abonnere",
"prefs_reservations_table_everyone_read_write": "Alle kan publisere og abonnere",
"prefs_reservations_table_not_subscribed": "Ikke abonnent",
"prefs_reservations_table_everyone_write_only": "Jeg kan publisere og abonnere, alle andre kan publisere",
"prefs_reservations_dialog_title_add": "Reserver emne",
"prefs_reservations_dialog_title_edit": "Rediger reservert emne",
"account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} reservert emne",
"reservation_delete_dialog_action_delete_title": "Slett mellomlagrede meldinger og vedlegg",
"nav_upgrade_banner_label": "Oppgrader til ntfy Pro",
"nav_upgrade_banner_description": "Reserver emner, flere meldinger & e-poster, og større vedlegg",
"account_delete_dialog_button_submit": "Slett konto permanent",
"account_basics_username_description": "Hei, det er deg ❤",
"account_basics_username_admin_tooltip": "Du er administrator",
"account_basics_password_title": "Passord",
"account_basics_password_description": "Endre passordet ditt",
"account_usage_title": "Forbruk",
"account_delete_dialog_button_cancel": "Avbryt",
"account_tokens_dialog_title_delete": "Slett tilgangstoken",
"account_tokens_dialog_label": "Etikett, f.eks. Radarr-varslinger",
"prefs_reservations_table": "Tabell over reserverte emner",
"prefs_reservations_edit_button": "Rediger tilgang til emne",
"prefs_reservations_delete_button": "Nullstill tilgang til emne",
"prefs_reservations_table_topic_header": "Emne",
"account_basics_title": "Konto",
"account_basics_phone_numbers_dialog_code_label": "Verifiseringskode",
"alert_notification_permission_denied_title": "Varslinger blokkert",
"alert_notification_permission_denied_description": "Vennligst reaktiver dem i nettleseren din",
"alert_notification_ios_install_required_title": "iOS-installasjon kreves",
"alert_notification_ios_install_required_description": "Klikk på Del-ikonet og Legg til hjemmeskjerm for å aktivere varslinger på iOS",
"action_bar_mute_notifications": "Demp varslinger",
"action_bar_unmute_notifications": "Avdemp varslinger",
"action_bar_profile_title": "Profil",
"action_bar_profile_logout": "Logg ut",
"action_bar_sign_in": "Logg inn",
"action_bar_sign_up": "Registrer deg",
"alert_not_supported_context_description": "Varslinger er kun støttet over HTTPS. Dette er en begrensning i <mdnLink>Varslings-APIet</mdnLink>.",
"notifications_actions_failed_notification": "Handling feilet",
"display_name_dialog_description": "Angi et alternativt navn for et emne som vises i abonneringslisten. Dette hjelper til med å enklere identifisere emner med kompliserte navn.",
"display_name_dialog_placeholder": "Visningnavn",
"publish_dialog_call_label": "Telefonsamtale",
"publish_dialog_call_item": "Ring telefonnummer {{number}}",
"publish_dialog_call_reset": "Fjern telefonsamtale",
"publish_dialog_chip_call_no_verified_numbers_tooltip": "Ingen verfiserte telefonnumre",
"publish_dialog_checkbox_markdown": "Formatter som Markdown",
"subscribe_dialog_subscribe_use_another_background_info": "Varslinger fra andre servere vil ikke bli tatt imot når webappen ikke er åpen",
"subscribe_dialog_subscribe_button_generate_topic_name": "Generer navn",
"subscribe_dialog_error_topic_already_reserved": "Emne allerede reservert",
"account_basics_username_title": "Brukernavn",
"account_basics_password_dialog_title": "Endre passord",
"account_basics_password_dialog_current_password_label": "Nåværende passord",
"account_basics_password_dialog_new_password_label": "Nytt passord",
"account_basics_password_dialog_confirm_password_label": "Bekreft passord",
"account_basics_password_dialog_button_submit": "Endre passord",
"account_basics_password_dialog_current_password_incorrect": "Passordet er feil",
"account_basics_phone_numbers_title": "Telefonnumre",
"account_basics_phone_numbers_description": "For telefonvarsling",
"account_basics_phone_numbers_no_phone_numbers_yet": "Ingen telefonnumre enda",
"account_basics_phone_numbers_copied_to_clipboard": "Telefonnummer kopiert til utklippstavle",
"account_basics_phone_numbers_dialog_title": "Legg til telefonnummer",
"account_basics_phone_numbers_dialog_number_label": "Telefonnummer",
"account_basics_phone_numbers_dialog_number_placeholder": "f.eks. +1222333444",
"account_basics_phone_numbers_dialog_verify_button_sms": "Send SMS",
"account_basics_phone_numbers_dialog_verify_button_call": "Ring meg",
"account_basics_phone_numbers_dialog_code_placeholder": "f.eks. 123456",
"account_basics_phone_numbers_dialog_check_verification_button": "Bekreft kode",
"account_basics_phone_numbers_dialog_channel_sms": "SMS",
"account_basics_phone_numbers_dialog_channel_call": "Ring",
"account_usage_of_limit": "av {{limit}}",
"account_usage_unlimited": "Ubegrenset",
"account_usage_limits_reset_daily": "Forbruksgrenser nullstilles hver dag ved midnatt (UTC)",
"account_basics_tier_title": "Kontotype",
"account_basics_tier_description": "Din kontos styrke",
"account_basics_tier_admin": "Administrator",
"account_basics_tier_admin_suffix_with_tier": "(med {{tier}} nivå)",
"account_basics_tier_admin_suffix_no_tier": "(ingen nivå)",
"account_basics_tier_paid_until": "Abonnement betalt til {{date}}, og vil bli fornyet automatisk",
"account_basics_tier_payment_overdue": "Betalingen din har forfalt. Vennligst oppdater betalingsmetoden din, hvis ikke blir kontoen din snart degradert.",
"account_basics_tier_manage_billing_button": "Behandle betalinger",
"account_usage_messages_title": "Publiserte meldinger",
"account_usage_calls_none": "Ingen telefonsamtaler kan foretas med denne kontoen",
"account_usage_reservations_none": "Ingen reserverte emner for denne kontoen",
"account_usage_attachment_storage_title": "Vedleggslagring",
"account_usage_basis_ip_description": "Forbruksstatistikk og -grenser for denne kontoen er basert på IP-adressen din, så det kan være de er delt med andre brukere. Forbruksgrenser vist over er omtrentlige, basert på eksisterende begrensninger.",
"account_delete_title": "Slett konto",
"account_delete_dialog_billing_warning": "Sletting av kontoen din avslutter også abonnementet og betalingene dine umiddelbart. Du vil ikke ha tilgang til betalingsportalen lenger.",
"account_upgrade_dialog_title": "Endre kontonivå",
"account_upgrade_dialog_interval_yearly": "Årlig",
"account_upgrade_dialog_interval_yearly_discount_save": "spar {{discount}}%",
"account_upgrade_dialog_interval_yearly_discount_save_up_to": "spar inntil {{discount}}%",
"account_upgrade_dialog_cancel_warning": "Dette vil <strong>avslutte abonnementet ditt</strong>, og nedgradere kontoen din den {{date}}. På den datoen vil alle emnereservasjoner såvel som meldinger lagret på serveren <strong>bli slettet</strong>.",
"account_upgrade_dialog_reservations_warning_one": "Det valgte nivået tillater færre reserverte emner enn ditt nåvære nivå. Før du endrer nivå, <strong>vennligst slett minst én reservasjon</strong>. Du kan slette reservasjoner i <Link>Innstillingene</Link>.",
"account_upgrade_dialog_tier_features_no_reservations": "Ingen reserverte emner",
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} daglig e-post",
"account_upgrade_dialog_tier_features_calls_other": "{{calls}} daglige telefonsamtaler",
"account_upgrade_dialog_tier_features_no_calls": "Ingen telefonsamtaler",
"account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} pr. fil",
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} total lagringsplass",
"account_upgrade_dialog_tier_price_per_month": "måned",
"account_upgrade_dialog_tier_price_billed_yearly": "{{price}} fakturert årlig. Spar {{save}}.",
"account_upgrade_dialog_billing_contact_website": "For faktureringsspørsmål, vennligst se vår <Link>nettside</Link>.",
"account_upgrade_dialog_button_update_subscription": "Oppdater abonnement",
"account_tokens_title": "Tilgangstokener",
"account_tokens_table_last_access_header": "Sist aksessert",
"account_tokens_table_expires_header": "Utløper",
"account_tokens_table_never_expires": "Utløper aldri",
"account_tokens_dialog_title_edit": "Rediger tilgangstoken",
"account_tokens_dialog_button_create": "Opprett token",
"account_tokens_dialog_button_update": "Oppdater token",
"account_tokens_dialog_button_cancel": "Avbryt",
"account_tokens_dialog_expires_label": "Tilgangstoken utløper om",
"account_tokens_dialog_expires_x_days": "Token utløper om {{days}} dager",
"account_tokens_dialog_expires_never": "Token utløper aldri",
"prefs_notifications_web_push_title": "Bakgrunnsvarslinger",
"prefs_notifications_web_push_enabled_description": "Varslinger mottas send om web-appen ikke kjører (via Web Push)",
"prefs_notifications_web_push_enabled": "Aktivert for {{server}}",
"prefs_notifications_web_push_disabled": "Deaktivert",
"prefs_reservations_description": "Du kan reservere emnenavn for personlig bruk her. Reservasjon av et emne gir deg eierskap over emnet og lar deg definere tilgangsrettigheter for andre brukere av dette emnet.",
"prefs_reservations_limit_reached": "Du har nådd grensen for antall reserverte emner du kan ha.",
"prefs_reservations_add_button": "Legg til reservert emne",
"prefs_reservations_table_access_header": "Tilgang",
"prefs_reservations_table_everyone_read_only": "Jeg kan publisere og abonnere, alle andre kan abonnere",
"prefs_reservations_dialog_description": "Reservering av et emne gir deg eierskap over emnet, og lar deg definere tilgangsrettigheter for andre brukere av emnet.",
"reservation_delete_dialog_description": "Ved å fjerne en reservasjon gir du fra deg eierskapet over emnet, og gir dermed andre muligheten til å reservere det. Du kan beholde eller slette eksisterende meldinger og vedlegg.",
"reservation_delete_dialog_action_keep_description": "Meldinger og vedlegg som er mellomlagret på serveren vil bli synlige for alle som kjenner til emnenavnet.",
"reservation_delete_dialog_action_delete_description": "Mellomlagrede meldinger og vedlegg vil bli permanent slettet. Denne handlingen kan ikke angres.",
"reservation_delete_dialog_submit_button": "Slett reservasjon",
"error_boundary_button_reload_ntfy": "Last inn ntfy på nytt",
"web_push_subscription_expiring_title": "Varslinger vil bli satt på pause",
"web_push_subscription_expiring_body": "Åpne ntfy for å fortsette å motta varslinger",
"web_push_unknown_notification_title": "Ukjent varsel mottatt fra server",
"web_push_unknown_notification_body": "Du må muligens oppdatere ntfy ved å åpne web-appen",
"account_usage_attachment_storage_description": "{{filesize}} pr. fil, slettet etter {{expiry}}"
}

View File

@@ -5,9 +5,9 @@
"message_bar_type_message": "Typ hier een bericht",
"action_bar_unsubscribe": "Afmelden",
"message_bar_error_publishing": "Fout bij publiceren notificatie",
"nav_topics_title": "Geabonneerde onderwerpen",
"nav_topics_title": "Geabonneerde topics",
"nav_button_settings": "Instellingen",
"alert_not_supported_description": "Notificaties worden niet ondersteund door je browser.",
"alert_not_supported_description": "Notificaties worden niet ondersteund door je browser",
"notifications_none_for_any_title": "Je hebt nog geen notificaties ontvangen.",
"publish_dialog_tags_label": "Tags",
"publish_dialog_chip_attach_file_label": "Lokaal bestand bijvoegen",
@@ -36,7 +36,7 @@
"nav_button_muted": "Notificaties gedempt",
"nav_button_connecting": "verbinden",
"alert_notification_permission_required_title": "Notificaties zijn uitgeschakeld",
"alert_notification_permission_required_description": "Verleen je browser toestemming voor het weergeven van notificaties.",
"alert_notification_permission_required_description": "Verleen je browser toestemming voor het weergeven van notificaties op desktop",
"alert_notification_permission_required_button": "Nu toestaan",
"alert_not_supported_title": "Notificaties zijn niet ondersteund",
"notifications_list": "Notificatielijst",
@@ -195,14 +195,14 @@
"signup_disabled": "Registreren is uitgeschakeld",
"signup_error_username_taken": "Gebruikersnaam {{username}} is al bezet",
"signup_error_creation_limit_reached": "Limiet voor aanmaken account bereikt",
"login_title": "Aanmelden bij uw ntfy account",
"login_title": "Inloggen met uw ntfy account",
"login_form_button_submit": "Inloggen",
"login_link_signup": "Registreer",
"login_link_signup": "Registreren",
"login_disabled": "Inloggen is uitgeschakeld",
"action_bar_account": "Account",
"action_bar_reservation_add": "Onderwerp reserveren",
"action_bar_reservation_edit": "Reservatie wijzigen",
"action_bar_reservation_delete": "Verwijder reservatie",
"action_bar_reservation_add": "Topic reserveren",
"action_bar_reservation_edit": "Reservering wijzigen",
"action_bar_reservation_delete": "Verwijder reservering",
"action_bar_reservation_limit_reached": "Limiet bereikt",
"action_bar_profile_title": "Profiel",
"nav_upgrade_banner_label": "Upgrade naar ntfy Pro",
@@ -380,5 +380,28 @@
"account_basics_phone_numbers_dialog_verify_button_sms": "Stuur SMS",
"account_basics_phone_numbers_dialog_code_label": "Verificatiecode",
"account_usage_calls_title": "Aantal telefoontjes",
"account_usage_calls_none": "Met dit account kan niet worden gebeld"
"account_usage_calls_none": "Met dit account kan niet worden gebeld",
"action_bar_mute_notifications": "Notificaties dempen",
"prefs_notifications_web_push_disabled_description": "Notificaties worden ontvangen als de webapplicatie geopend is (via WebSocket)",
"web_push_unknown_notification_body": "Het is mogelijk dat je ntfy moet updaten door de webapplicatie opnieuw te openen",
"action_bar_unmute_notifications": "Dempen notificaties opheffen",
"alert_notification_permission_denied_title": "Notificaties zijn geblokkeerd",
"alert_notification_permission_denied_description": "Activeer ze in de browser",
"alert_notification_ios_install_required_title": "iOS installatie vereist",
"alert_notification_ios_install_required_description": "Klik op het Deel icoon, daarna op \"Add to Home Screen\" om notificaties op iOS toe te staan",
"notifications_actions_failed_notification": "Actie onsuccesvol",
"publish_dialog_checkbox_markdown": "Opmaken met Markdown",
"subscribe_dialog_subscribe_use_another_background_info": "Notificaties van andere servers worden niet ontvangen als de webapplicatie niet geopend is",
"prefs_notifications_web_push_title": "Achtergrond notificaties",
"prefs_notifications_web_push_enabled": "Aan voor {{server}}",
"prefs_notifications_web_push_disabled": "Uitgezet",
"prefs_notifications_web_push_enabled_description": "Notificaties worden ontvangen, ook als de webapplicatie niet geopend is (via Web Push)",
"prefs_appearance_theme_title": "Thema",
"prefs_appearance_theme_system": "Systeem (standaard)",
"prefs_appearance_theme_dark": "Donkere modus",
"prefs_appearance_theme_light": "Lichte modus",
"error_boundary_button_reload_ntfy": "Herlaad ntfy",
"web_push_subscription_expiring_title": "Notificaties worden gepauzeerd",
"web_push_subscription_expiring_body": "Open ntfy om weer notificaties te ontvangen",
"web_push_unknown_notification_title": "Onbekende notificatie ontvangen van de server"
}

View File

@@ -10,10 +10,10 @@
"nav_button_documentation": "Dokumentacja",
"nav_button_muted": "Powiadomienia wyciszone",
"alert_notification_permission_required_title": "Powiadomienia są wyłączone",
"alert_notification_permission_required_description": "Udziel przeglądarce pozwolenia na wyświetlanie powiadomień na pulpicie.",
"alert_notification_permission_required_description": "Udziel przeglądarce pozwolenia na wyświetlanie powiadomień na pulpicie",
"alert_notification_permission_required_button": "Pozwól teraz",
"alert_not_supported_title": "Powiadomienia nie są obsługiwane",
"alert_not_supported_description": "Powiadomienia nie są obsługiwane przez Twoją przeglądarkę.",
"alert_not_supported_description": "Powiadomienia nie są obsługiwane przez Twoją przeglądarkę",
"notifications_list": "Lista powiadomień",
"notifications_list_item": "Powiadomienie",
"notifications_mark_read": "Oznacz jako przeczytane",
@@ -355,5 +355,59 @@
"publish_dialog_call_item": "Zadzwoń pod numer {{number}}",
"account_basics_phone_numbers_dialog_channel_sms": "SMS",
"account_upgrade_dialog_tier_selected_label": "Wybrane",
"account_upgrade_dialog_reservations_warning_other": "Wybrany plan zezwala na mniejszą liczbę zarezerwowanych tematów niż obecny. Przed zmianą planu, <strong>usuń co najmniej tyle rezerwacji: {{count}}</strong>. Rezerwacje możesz usunąć w <Link>Ustawieniach</Link>."
"account_upgrade_dialog_reservations_warning_other": "Wybrany plan zezwala na mniejszą liczbę zarezerwowanych tematów niż obecny. Przed zmianą planu, <strong>usuń co najmniej tyle rezerwacji: {{count}}</strong>. Rezerwacje możesz usunąć w <Link>Ustawieniach</Link>.",
"prefs_reservations_title": "Zarezerwowane tematy",
"prefs_reservations_table_everyone_read_only": "Ja mogę publikować i subskrybować, każdy może subskrybować",
"prefs_reservations_table_not_subscribed": "Nie jesteś zasubskrybowany",
"prefs_reservations_dialog_title_delete": "Usuń rezerwacje tematu",
"prefs_reservations_dialog_topic_label": "Temat",
"reservation_delete_dialog_action_delete_title": "Usuń wiadomości i załączniki zapisane w pamięci cache",
"prefs_reservations_description": "Możesz tutaj zarezerwować nazwy tematów do własnego użytku. Rezerwacja tematu daje ci go na własność i pozwala definiować permisje dla innych użytkowników.",
"prefs_reservations_limit_reached": "Zużyłeś swój limit zarezerwowanych tematów.",
"prefs_reservations_add_button": "Dodaj zarezerwowany temat",
"account_tokens_delete_dialog_description": "Przed usuwaniem tokenu dostępu upewnij się, że nie jest on aktywnie używany przez inną aplikację lub skrypt. <strong>Ta akcja nie może być wycofana</strong>.",
"prefs_reservations_table_everyone_read_write": "Każdy może publikować i subskrybować",
"prefs_reservations_table_click_to_subscribe": "Kliknij aby subskrybować",
"prefs_reservations_dialog_title_edit": "Modyfikuj zarezerwowany temat",
"prefs_reservations_table_everyone_write_only": "Ja mogę publikować i subskrybować, każdy może publikować",
"action_bar_mute_notifications": "Wycisz powiadomienia",
"alert_notification_permission_denied_title": "Powiadomienia są blokowane",
"alert_notification_ios_install_required_description": "Wciśnij ikonę Udostępniania i dodaj do Strony Głównej aby zezwolić na otrzymywanie powiadomień na IOS",
"notifications_actions_failed_notification": "Akcja zakończona niepowodzeniem",
"prefs_notifications_web_push_disabled_description": "Powiadomienia są dostarczane kiedy aplikacja jest aktywna (poprzez WebSocket)",
"prefs_notifications_web_push_enabled": "Włączone dla {{server}}",
"prefs_notifications_web_push_disabled": "Wyłączone",
"prefs_appearance_theme_system": "Systemowy (domyślny)",
"prefs_appearance_theme_dark": "Tryb ciemny",
"prefs_appearance_theme_light": "Tryb jasny",
"prefs_reservations_edit_button": "Modyfikuj ustawienia dostępu dla tematu",
"prefs_reservations_table": "Tabela zarezerwowanych tematów",
"prefs_reservations_table_topic_header": "Temat",
"prefs_reservations_table_access_header": "Dostęp",
"prefs_reservations_table_everyone_deny_all": "Tylko ja mogę publikować i subskrybować",
"prefs_reservations_dialog_access_label": "Dostęp",
"reservation_delete_dialog_action_delete_description": "Wiadomości i załączniki zapisane w pamięci cache zostaną pernamentie usunięte. <strong>Ta akcja nie może być wycofana</strong>.",
"reservation_delete_dialog_submit_button": "Usuń rezerwację",
"error_boundary_button_reload_ntfy": "Przeładuj ntfy",
"web_push_subscription_expiring_title": "Powiadomienia będą wstrzymane",
"web_push_subscription_expiring_body": "Otwórz ntfy aby nadal dostawać powiadomienia",
"alert_notification_permission_denied_description": "Prosze ponownie pozwolić na otrzymywanie powiadomień w twojej przeglądarce",
"subscribe_dialog_subscribe_use_another_background_info": "Powiadomienia z innych serwerów nie zostaną odebrane jeśli aplikacja nie jest otwarta",
"alert_notification_ios_install_required_title": "Instalacja IOS wymagana",
"publish_dialog_checkbox_markdown": "Formatuj jako Markdown",
"account_tokens_delete_dialog_submit_button": "Pernamentnie usuń token dostępu",
"prefs_notifications_web_push_title": "Powiadomienia w tle",
"prefs_notifications_web_push_enabled_description": "Powiadomienia są dostarczane nawet kiedy aplikacja nie jest aktywna (poprzez Web Push)",
"prefs_users_description_no_sync": "Nazwy użytkownika i hasła nie są synchronizowane z kontem.",
"prefs_users_table_cannot_delete_or_edit": "Nie można usunąć lub modyfikować zalogowanego użytkownika",
"prefs_reservations_delete_button": "Zresetuj ustawienia dostępu dla tematu",
"prefs_reservations_dialog_title_add": "Zarezerwuj temat",
"reservation_delete_dialog_action_keep_title": "Zachowaj wiadomości i załącznik w pamięci cache",
"reservation_delete_dialog_action_keep_description": "Wiadomości i załączniki które są zapisane w pamięci cache będą dostępne publicznie dla każdego znającego nazwę powiązanego z nimi tematu.",
"web_push_unknown_notification_title": "Nieznane powiadomienie otrzymane od serwera",
"action_bar_unmute_notifications": "Włącz ponownie powiadomienia",
"prefs_appearance_theme_title": "Wygląd",
"prefs_reservations_dialog_description": "Zastrzeżenie tematu daje użytkownikowi prawo własności do tego tematu i umożliwia zdefiniowanie uprawnień dostępu do tego tematu dla innych użytkowników.",
"reservation_delete_dialog_description": "Usunięcie rezerwacji powoduje rezygnację z prawa własności do tematu i umożliwia innym jego zarezerwowanie. Istniejące wiadomości i załączniki można zachować lub usunąć.",
"web_push_unknown_notification_body": "Konieczne może być zaktualizowanie ntfy poprzez otwarcie aplikacji internetowej"
}

View File

@@ -16,10 +16,10 @@
"nav_button_muted": "Notificações desativadas",
"nav_button_connecting": "A ligar",
"alert_notification_permission_required_title": "As notificações estão desativadas",
"alert_notification_permission_required_description": "Conceder permissão ao seu navegador para mostrar notificações.",
"alert_notification_permission_required_description": "Conceder permissão ao seu navegador para mostrar notificações",
"alert_not_supported_title": "Notificações não suportadas",
"notifications_list": "Lista de notificações",
"alert_not_supported_description": "As notificações não são suportadas pelo seu navegador.",
"alert_not_supported_description": "As notificações não são suportadas pelo seu navegador",
"notifications_list_item": "Notificação",
"notifications_mark_read": "Marcar como lido",
"notifications_delete": "Apagar",
@@ -215,16 +215,99 @@
"action_bar_reservation_add": "Reservar tópico",
"action_bar_sign_up": "Registar",
"nav_button_account": "Conta",
"common_copy_to_clipboard": "Copiar",
"nav_upgrade_banner_label": "Atualizar para ntfy Pro",
"alert_not_supported_context_description": "Notificações são suportadas apenas sobre HTTPS. Essa é uma limitação da <mdnLink>API de Notificações</mdnLink>.",
"display_name_dialog_title": "Alterar nome mostrado",
"display_name_dialog_description": "Configura um nome alternativo ao tópico que é mostrado na lista de assinaturas. Isto ajuda a identificar tópicos com nomes complicados mais facilmente.",
"display_name_dialog_placeholder": "Nome exibido",
"common_copy_to_clipboard": "Copiar à área de transferência",
"nav_upgrade_banner_label": "Upgrade para ntfy Pro",
"alert_not_supported_context_description": "As notificações são apenas suportadas através de HTTPS. Isto é uma limitação da <mdnLink>Notifications API</mdnLink>.",
"display_name_dialog_title": "Alterar o nome público",
"display_name_dialog_description": "Configurar um nome alternativo para um tópico que é mostrado na lista de subscrições. Isto ajuda a identificar tópicos com nomes complicados mais facilmente.",
"display_name_dialog_placeholder": "Nome público",
"reserve_dialog_checkbox_label": "Reservar tópico e configurar acesso",
"publish_dialog_call_label": "Chamada telefônica",
"publish_dialog_call_label": "Chamada telefónica",
"publish_dialog_call_placeholder": "Número de telefone para ligar com a mensagem, ex: +12223334444, ou 'Sim'",
"publish_dialog_call_reset": "Remover chamada telefônica",
"publish_dialog_chip_call_label": "Chamada telefônica",
"subscribe_dialog_subscribe_button_generate_topic_name": "Gerar nome"
"subscribe_dialog_subscribe_button_generate_topic_name": "Gerar nome",
"action_bar_unmute_notifications": "Restaurar notificações",
"alert_notification_ios_install_required_description": "Clique no ícone Compartilhar e Adicionar à Tela Inicial para ativar as notificações no iOS",
"publish_dialog_checkbox_markdown": "Formatar como Markdown",
"publish_dialog_chip_call_no_verified_numbers_tooltip": "Números de telefone não verificados",
"subscribe_dialog_error_topic_already_reserved": "Tópico já reservado",
"action_bar_mute_notifications": "Silenciar notificações",
"alert_notification_permission_denied_title": "Notificações estão bloqueadas",
"alert_notification_permission_denied_description": "Por favor reative-as em seu navegador",
"alert_notification_ios_install_required_title": "Requer instalação em iOS",
"notifications_actions_failed_notification": "Houve uma falha na ação",
"publish_dialog_call_item": "Ligar para o número de telefone {{number}}",
"subscribe_dialog_subscribe_use_another_background_info": "Notificações de outros servidores não serão recebidas enquanto o aplicativo web não estiver aberto",
"account_basics_username_description": "Olá, és tu ❤",
"account_basics_password_dialog_new_password_label": "Nova palavra-passe",
"account_basics_password_dialog_current_password_incorrect": "Palavra-passe inválida",
"account_basics_phone_numbers_title": "Números de telefone",
"account_basics_phone_numbers_dialog_description": "Para utilizar o recurso de notificação por ligação, você precisa adicionar e verificar pelo menos um número de telefone. A verificação poderá ser feita via SMS ou ligação telefônica.",
"account_basics_phone_numbers_dialog_title": "Adicionar número de telefone",
"account_basics_phone_numbers_dialog_verify_button_call": "Ligue me",
"account_basics_phone_numbers_dialog_number_label": "Número de telefone",
"account_basics_phone_numbers_dialog_number_placeholder": "ex.: +1222333444",
"account_basics_phone_numbers_dialog_verify_button_sms": "Enviar SMS",
"account_basics_phone_numbers_dialog_code_placeholder": "ex.: 123456",
"account_basics_phone_numbers_dialog_code_label": "Código de verificação",
"account_basics_phone_numbers_dialog_check_verification_button": "Código de confirmação",
"account_basics_phone_numbers_dialog_channel_call": "Ligação",
"account_basics_tier_canceled_subscription": "Sua assinatura foi cancelada e será rebaixada para uma conta gratuita em {[data}}.",
"account_basics_tier_manage_billing_button": "Gerenciar cobrança",
"account_usage_reservations_none": "Esta conta não possui tópicos reservados",
"account_usage_attachment_storage_title": "Armazenamento de anexos",
"account_usage_emails_title": "E-mails enviados",
"account_basics_password_description": "Mudar a palavra-passe da conta",
"account_basics_password_dialog_title": "Mudar a palavra-passe",
"account_basics_phone_numbers_description": "Para notificações por ligação",
"account_basics_tier_paid_until": "Assinatura paga até {{date}}, e será renovada automaticamente",
"account_basics_password_dialog_confirm_password_label": "Confirmar palavra-passe",
"account_basics_password_dialog_button_submit": "Mudar palavra-passe",
"account_basics_title": "Conta",
"account_basics_username_admin_tooltip": "És Admin",
"account_basics_password_title": "Palavra-passe",
"account_basics_password_dialog_current_password_label": "Palavra-passe atual",
"account_basics_phone_numbers_no_phone_numbers_yet": "Nenhum número de telefone",
"account_basics_phone_numbers_copied_to_clipboard": "Telefones copiados para área de transferência",
"account_basics_phone_numbers_dialog_channel_sms": "SMS",
"account_usage_title": "Utilização",
"account_usage_of_limit": "de {{limit}}",
"account_usage_unlimited": "Ilimitado",
"account_usage_limits_reset_daily": "Limites de uso são resetados diariamente à meia noite (UTC)",
"account_basics_tier_title": "Tipo de conta",
"account_basics_tier_description": "Nível da sua conta",
"account_basics_tier_admin": "Administrador",
"account_basics_tier_admin_suffix_with_tier": "(com {{tier}} classe)",
"account_basics_tier_admin_suffix_no_tier": "(sem classe)",
"account_basics_tier_basic": "Básico",
"account_basics_tier_free": "Grátis",
"account_basics_tier_interval_monthly": "Mensalmente",
"account_basics_tier_interval_yearly": "anualmente",
"account_basics_tier_upgrade_button": "Atualizar para o Pro",
"account_basics_tier_change_button": "Alterar",
"account_basics_tier_payment_overdue": "Seu pagamento está em atraso. Por favor atualize seu método de pagamento, ou sua conta será rebaixada em breve.",
"account_usage_messages_title": "Mensagens publicadas",
"account_usage_calls_title": "Ligações realizadas",
"account_usage_calls_none": "Esta conta não pode realizar ligações",
"account_usage_reservations_title": "Tópicos reservados",
"account_basics_username_title": "Usuário",
"account_delete_dialog_description": "Isto irá eliminar definitivamente a sua conta, incluindo dados que estejam armazenados no servidor. Apos ser eliminado, o nome de utilizador ficará indisponível durante 7 dias. Se deseja mesmo proceder, por favor confirme com a sua palavra-passe na caixa abaixo.",
"account_delete_dialog_button_submit": "Eliminar conta definitivamente",
"account_delete_dialog_billing_warning": "Eliminar a sua conta também cancela a sua subscrição de faturação imediatamente. Não terá acesso ao portal de faturação de futuro.",
"account_upgrade_dialog_title": "Alterar o nível da sua conta",
"account_upgrade_dialog_interval_monthly": "Mensalmente",
"account_upgrade_dialog_interval_yearly": "Anualmente",
"account_upgrade_dialog_interval_yearly_discount_save": "poupe {{discount}}%",
"account_upgrade_dialog_interval_yearly_discount_save_up_to": "poupe até {{discount}}%",
"account_delete_dialog_label": "Palavra-passe",
"account_usage_cannot_create_portal_session": "Impossível abrir o portal de faturação",
"account_usage_basis_ip_description": "Estatísticas de utilização e limites para esta conta são baseadas no seu endereço IP, pelo que podem ser partilhados com outros utilizadores. Os limites mostrados acima são aproximados com base nos limites existentes.",
"account_usage_attachment_storage_description": "{{filesize}} por ficheiro, eliminado após {{expiry}}",
"account_delete_title": "Eliminar conta",
"account_delete_description": "Eliminar definitivamente a sua conta",
"account_delete_dialog_button_cancel": "Cancelar",
"account_upgrade_dialog_cancel_warning": "Isto irá <strong>cancelar a sua assinatura</strong>, e fazer downgrade da sua conta em {{date}}. Nessa data, tópicos reservados bem como mensagens guardadas no servidor <strong>serão eliminados</strong>.",
"account_upgrade_dialog_proration_info": "<strong>Proporção</strong>: Quando atualizar entre planos pagos, a diferença de preço será <strong>debitada imediatamente</strong>. Quando efetuar um downgrade para um escalão inferior, o saldo disponível será usado para futuros períodos de faturação.",
"prefs_users_description_no_sync": "Utilizadores e palavras-passe não estão sincronizados com a sua conta."
}

View File

@@ -8,10 +8,10 @@
"nav_button_settings": "Configurações",
"nav_button_subscribe": "Inscrever no tópico",
"alert_notification_permission_required_title": "Notificações estão desativadas",
"alert_notification_permission_required_description": "Conceder ao navegador permissão para mostrar notificações.",
"alert_notification_permission_required_description": "Conceder permissão ao seu navegador para mostrar notificações",
"alert_notification_permission_required_button": "Conceder agora",
"alert_not_supported_title": "Notificações não são suportadas",
"alert_not_supported_description": "Notificações não são suportadas pelo seu navagador.",
"alert_not_supported_description": "Notificações não são suportadas pelo seu navegador",
"notifications_copied_to_clipboard": "Copiado para a área de transferência",
"notifications_tags": "Etiquetas",
"notifications_attachment_copy_url_title": "Copiar URL do anexo para a área de transferência",
@@ -189,15 +189,15 @@
"prefs_users_delete_button": "Excluir usuário",
"error_boundary_unsupported_indexeddb_title": "Navegação anônima não suportada",
"error_boundary_unsupported_indexeddb_description": "O ntfy web app precisa do IndexedDB para funcionar, e seu navegador não suporta IndexedDB no modo de navegação privada.<br/><br/>Embora isso seja lamentável, também não faz muito sentido usar o ntfy web app no modo de navegação privada de qualquer maneira, porque tudo é armazenado no armazenamento do navegador. Você pode ler mais sobre isso <githubLink>nesta edição do GitHub</githubLink>, ou falar conosco em <discordLink>Discord</discordLink> ou <matrixLink>Matrix</matrixLink>.",
"action_bar_reservation_add": "Reserve topic",
"action_bar_reservation_edit": "Change reservation",
"signup_disabled": "Registrar está desativado",
"signup_error_username_taken": "Usuário {{username}} já existe",
"action_bar_reservation_add": "Reservar tópico",
"action_bar_reservation_edit": "Mudar reserva",
"signup_disabled": "O registro está desativado",
"signup_error_username_taken": "O nome de usuário {{username}} já está em uso",
"signup_error_creation_limit_reached": "Limite de criação de contas atingido",
"action_bar_reservation_delete": "Remover reserva",
"action_bar_account": "Conta",
"action_bar_change_display_name": "Change display name",
"common_copy_to_clipboard": "Copiar para área de transferência",
"action_bar_change_display_name": "Mudar nome de exibição",
"common_copy_to_clipboard": "Copiar para a Área de Transferência",
"login_link_signup": "Registrar",
"login_title": "Entrar na sua conta ntfy",
"login_form_button_submit": "Entrar",
@@ -210,13 +210,13 @@
"action_bar_sign_up": "Registrar",
"nav_button_account": "Conta",
"signup_title": "Criar uma conta ntfy",
"signup_form_username": "Usuário",
"signup_form_username": "Nome de usuário",
"signup_form_password": "Senha",
"signup_form_confirm_password": "Confirmar senha",
"signup_form_button_submit": "Registrar",
"signup_form_button_submit": "Criar conta",
"account_basics_phone_numbers_title": "Telefones",
"signup_form_toggle_password_visibility": "Ativar visibilidade de senha",
"signup_already_have_account": "Já possui uma conta? Entrar!",
"signup_form_toggle_password_visibility": "Alterar visibilidade da senha",
"signup_already_have_account": "Já tem uma conta? Entre!",
"nav_upgrade_banner_label": "Atualizar para ntfy Pro",
"account_basics_phone_numbers_dialog_description": "Para usar o recurso de notificação de chamada, é necessários adicionar e verificar pelo menos um número de telefone. A verificação pode ser feita por SMS ou chamada telefônica.",
"account_basics_phone_numbers_description": "Para notificações de chamada telefônica",
@@ -224,7 +224,7 @@
"account_basics_tier_canceled_subscription": "Sua assinatura foi cancelada e será rebaixada para uma conta gratuita em {{date}}.",
"account_basics_password_dialog_current_password_incorrect": "Senha incorreta",
"account_basics_phone_numbers_dialog_number_label": "Número de telefone",
"account_basics_password_dialog_button_submit": "Alterar senha",
"account_basics_password_dialog_button_submit": "Mudar senha",
"reserve_dialog_checkbox_label": "Guardar tópico e configurar acesso",
"account_basics_username_title": "Nome de usuário",
"account_basics_phone_numbers_dialog_check_verification_button": "Confirmar código",
@@ -250,11 +250,11 @@
"account_basics_tier_free": "Grátis",
"account_basics_tier_admin": "Administrador",
"publish_dialog_chip_call_no_verified_numbers_tooltip": "Nenhum número de telefone verificado",
"account_basics_password_description": "Alterar a senha da sua conta",
"account_basics_password_description": "Mudar a senha da sua conta",
"publish_dialog_call_label": "Chamada telefônica",
"account_usage_calls_title": "Chamadas de telefone feitas",
"account_basics_tier_basic": "Básico",
"alert_not_supported_context_description": "Notificações são suportadas apenas através de HTTPS. Esta é uma limitação da <mdnLink>API de Notificações</mdnLink>.",
"alert_not_supported_context_description": "Notificações são suportadas somente por HTTPS. Essa é uma limitação da <mdnLink>Notifications API</mdnLink>.",
"account_basics_phone_numbers_copied_to_clipboard": "Número de telefone copiado para a área de transferência",
"account_basics_tier_title": "Tipo de conta",
"account_basics_phone_numbers_dialog_number_placeholder": "ex. +1222333444",
@@ -268,14 +268,14 @@
"account_basics_password_dialog_new_password_label": "Nova senha",
"display_name_dialog_placeholder": "Nome de exibição",
"account_usage_of_limit": "de {{limit}}",
"account_basics_password_dialog_title": "Alterar senha",
"account_basics_password_dialog_title": "Mudar senha",
"account_usage_limits_reset_daily": "Os limites de uso são redefinidos diariamente à meia-noite (UTC)",
"account_usage_unlimited": "Ilimitado",
"account_basics_password_dialog_current_password_label": "Senha atual",
"account_usage_reservations_title": "Tópicos reservados",
"account_usage_calls_none": "Nenhum telefonema pode ser feito com esta conta",
"display_name_dialog_title": "Alterar o nome de exibição",
"nav_upgrade_banner_description": "Guarde tópicos, mais mensagens & emails e anexos grandes",
"display_name_dialog_title": "Alterar nome de exibição",
"nav_upgrade_banner_description": "Reserve tópicos, mais mensagens e e-mails, e anexos maiores",
"publish_dialog_call_reset": "Remover chamada telefônica",
"account_basics_phone_numbers_dialog_code_label": "Código de verificação",
"account_basics_tier_paid_until": "Assinatura paga até {{date}}, será renovada automaticamente",
@@ -283,5 +283,126 @@
"account_basics_phone_numbers_dialog_verify_button_call": "Ligar pra mim",
"publish_dialog_call_item": "Ligue para o número de telefone {{number}}",
"account_usage_emails_title": "Emails enviados",
"account_basics_phone_numbers_dialog_channel_sms": "SMS"
"account_basics_phone_numbers_dialog_channel_sms": "SMS",
"account_delete_title": "Deletar conta",
"account_delete_dialog_label": "Senha",
"account_upgrade_dialog_interval_yearly": "Anual",
"account_upgrade_dialog_title": "Alterar nível da conta",
"alert_notification_ios_install_required_description": "Clique no ícone Compartilhar e adicione a tela inicial para ativar notificações no iOS",
"account_delete_dialog_billing_warning": "Excluir sua conta também cancela imediatamente sua assinatura de cobrança. Você não terá mais acesso ao painel de faturamento.",
"account_delete_dialog_description": "Isso excluirá permanentemente sua conta, incluindo todos os dados armazenados no servidor. Após a exclusão, seu nome de usuário ficará indisponível por 7 dias. Se você realmente deseja prosseguir, confirme sua senha na caixa abaixo.",
"account_upgrade_dialog_proration_info": "<strong>Prorrogação</strong>: Ao atualizar entre planos pagos, a diferença de preço será <strong>cobrada imediatamente</strong>. Ao fazer downgrade para um nível inferior, o saldo será usado para pagar futuras cobranças.",
"action_bar_mute_notifications": "Mutar notificações",
"action_bar_unmute_notifications": "Desmutar notificações",
"alert_notification_permission_denied_title": "Notificações estão bloqueadas",
"alert_notification_permission_denied_description": "Por favor, reative elas no seu navegador",
"alert_notification_ios_install_required_title": "Requer instalação no iOS",
"notifications_actions_failed_notification": "Ação mal sucedida",
"publish_dialog_checkbox_markdown": "Formatar como Markdown",
"subscribe_dialog_subscribe_use_another_background_info": "Notificações de outros servidores não serão recebidas quando o web app não estiver aberto",
"account_usage_basis_ip_description": "As estatísticas e limites de uso desta conta são baseados no seu endereço IP, portanto, podem ser compartilhados com outros usuários. Os limites mostrados acima são aproximados com base nos limites de taxa existentes.",
"account_usage_cannot_create_portal_session": "Não foi possível abrir o portal de cobrança",
"account_delete_description": "Deletar sua conta permanentemente",
"account_delete_dialog_button_cancel": "Cancelar",
"account_delete_dialog_button_submit": "Deletar conta permanentemente",
"account_upgrade_dialog_interval_monthly": "Mensal",
"account_upgrade_dialog_interval_yearly_discount_save": "desconto de {{discount}}%",
"account_upgrade_dialog_interval_yearly_discount_save_up_to": "desconto de até {{discount}}%",
"account_upgrade_dialog_cancel_warning": "Isso <strong>cancelará sua assinatura</strong> e fará downgrade de sua conta em {{date}}. Nessa data, as reservas de tópicos, bem como as mensagens armazenadas em cache no servidor <strong>serão excluídas</strong>.",
"account_upgrade_dialog_reservations_warning_one": "O nível selecionado permite menos tópicos reservados do que o nível atual. Antes de alterar seu nível, <strong>exclua pelo menos uma reserva</strong>. Você pode remover reservas nas <Link>Configurações</Link>.",
"account_upgrade_dialog_reservations_warning_other": "O nível selecionado permite menos tópicos reservados do que o seu nível atual. Antes de mudar seu nível, <strong>por favor exclua ao menos {{count}} reservas</strong>. Você pode remover reservas nas <Link>Configurações</Link>.",
"account_upgrade_dialog_tier_features_no_reservations": "Sem tópicos reservados",
"account_upgrade_dialog_tier_features_messages_one": "{{messages}} mensagen diária",
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} email diário",
"account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} tópico reservado",
"account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} tópicos reservados",
"account_upgrade_dialog_tier_features_emails_other": "{{emails}} emails diários",
"account_upgrade_dialog_tier_features_messages_other": "{{messages}} mensagens diárias",
"account_upgrade_dialog_tier_current_label": "Atual",
"account_upgrade_dialog_tier_price_per_month": "mês",
"account_upgrade_dialog_button_cancel": "Cancelar",
"account_upgrade_dialog_tier_selected_label": "Selecionado",
"account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} por arquivo",
"account_tokens_table_last_access_header": "Último acesso",
"account_upgrade_dialog_button_cancel_subscription": "Cancelar assinatura",
"account_tokens_table_never_expires": "Nunca expira",
"account_upgrade_dialog_tier_price_billed_yearly": "{{price}} cobrado anualmente. Salvar {{save}}.",
"account_upgrade_dialog_tier_features_no_calls": "Nenhuma chamada",
"account_tokens_table_token_header": "Token",
"account_upgrade_dialog_button_update_subscription": "Atualizar assinatura",
"account_tokens_table_current_session": "Sessão atual do navegador",
"account_tokens_table_copied_to_clipboard": "Token de acesso copiado",
"account_tokens_title": "Tokens de Acesso",
"account_upgrade_dialog_button_redirect_signup": "Cadastre-se agora",
"account_upgrade_dialog_tier_features_calls_one": "{{calls}} chamadas diárias",
"account_upgrade_dialog_tier_features_calls_other": "{{calls}} chamadas telefônicas diárias",
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} armazenamento total",
"account_upgrade_dialog_tier_price_billed_monthly": "{{price}} por ano. Cobrado mensalmente.",
"account_upgrade_dialog_button_pay_now": "Pague agora para assinar",
"account_tokens_table_expires_header": "Expira",
"prefs_users_description_no_sync": "Usuários e senhas não estão sincronizados com a sua conta.",
"account_tokens_description": "Use tokens de acesso ao publicar e assinar por meio da API ntfy, para que você não precise enviar as credenciais da sua conta. Consulte a <Link>documentação</Link> para saber mais.",
"account_tokens_table_cannot_delete_or_edit": "Não é possível editar ou excluir o token da sessão atual",
"account_tokens_dialog_title_edit": "Editar token de acesso",
"account_tokens_dialog_title_delete": "Excluir token de acesso",
"prefs_reservations_table_everyone_read_write": "Todos podem publicar e se inscrever",
"prefs_reservations_table_everyone_read_only": "Posso publicar e me inscrever, todos podem se inscrever",
"prefs_reservations_limit_reached": "Você atingiu seu limite de tópicos reservados.",
"prefs_reservations_delete_button": "Redefinir o acesso ao tópico",
"prefs_reservations_edit_button": "Editar acesso ao tópico",
"prefs_reservations_table_everyone_write_only": "Eu posso publicar e me inscrever, todos podem publicar",
"prefs_reservations_table_not_subscribed": "Não inscrito",
"prefs_reservations_table_click_to_subscribe": "Clique para se inscrever",
"reservation_delete_dialog_action_keep_title": "Manter mensagens e anexos em cache",
"account_tokens_table_label_header": "Rótulo",
"account_tokens_table_last_origin_tooltip": "Do endereço IP {{ip}}, clique para pesquisar",
"account_tokens_dialog_title_create": "Criar token de acesso",
"account_tokens_delete_dialog_title": "Excluir token de acesso",
"account_tokens_dialog_label": "Rótulo, por exemplo, notificações de Radarr",
"account_tokens_dialog_expires_never": "O token nunca expira",
"prefs_reservations_dialog_title_edit": "Editar tópico reservado",
"prefs_notifications_web_push_enabled_description": "As notificações são recebidas mesmo quando o aplicativo Web não está em execução (via Web Push)",
"prefs_notifications_web_push_disabled_description": "As notificações são recebidas quando o aplicativo Web está em execução (via WebSocket)",
"account_upgrade_dialog_billing_contact_website": "Para perguntas sobre faturamento, consulte nosso <Link>website</Link>.",
"account_tokens_table_create_token_button": "Criar token de acesso",
"account_tokens_dialog_button_cancel": "Cancelar",
"account_tokens_dialog_button_update": "Atualizar token",
"prefs_reservations_table": "Tabela de tópicos reservados",
"prefs_reservations_table_everyone_deny_all": "Somente eu posso publicar e me inscrever",
"account_tokens_delete_dialog_description": "Antes de excluir um token de acesso, certifique-se de que nenhum aplicativo ou script o esteja usando ativamente. <strong>Esta ação não pode ser desfeita</strong>.",
"account_tokens_delete_dialog_submit_button": "Excluir token permanentemente",
"account_tokens_dialog_expires_x_hours": "O token expira em {{hours}} horas",
"account_tokens_dialog_expires_x_days": "O token expira em {{days}} dias",
"prefs_reservations_description": "Você pode reservar nomes de tópicos para uso pessoal aqui. A reserva de um tópico lhe dá propriedade sobre ele e permite que você defina permissões de acesso para outros usuários sobre o tópico.",
"prefs_reservations_dialog_access_label": "Acesso",
"account_upgrade_dialog_billing_contact_email": "Para questões de cobrança, <Link>entre em contato conosco</Link> diretamente.",
"account_tokens_dialog_button_create": "Criar token",
"account_tokens_dialog_expires_label": "O token de acesso expira em",
"account_tokens_dialog_expires_unchanged": "Deixar a data de validade inalterada",
"prefs_notifications_web_push_title": "Notificações em segundo plano",
"prefs_notifications_web_push_enabled": "Ativado para {{server}}",
"prefs_notifications_web_push_disabled": "Desativado",
"prefs_appearance_theme_title": "Tema",
"prefs_users_table_cannot_delete_or_edit": "Não é possível excluir ou editar o usuário conectado",
"prefs_appearance_theme_system": "Sistema (padrão)",
"prefs_appearance_theme_dark": "Modo escuro",
"prefs_appearance_theme_light": "Modo claro",
"prefs_reservations_title": "Tópicos reservados",
"prefs_reservations_add_button": "Adicionar tópico reservado",
"prefs_reservations_table_topic_header": "Tópico",
"prefs_reservations_table_access_header": "Acesso",
"prefs_reservations_dialog_title_add": "Reservar tópico",
"prefs_reservations_dialog_title_delete": "Excluir reserva de tópico",
"prefs_reservations_dialog_description": "A reserva de um tópico lhe dá propriedade sobre ele e permite definir permissões de acesso para outros usuários sobre o tópico.",
"prefs_reservations_dialog_topic_label": "Tópico",
"reservation_delete_dialog_description": "A remoção de uma reserva abre mão da propriedade sobre o tópico e permite que outros o reservem. Você pode manter ou excluir as mensagens e os anexos existentes.",
"reservation_delete_dialog_action_keep_description": "As mensagens e os anexos armazenados em cache no servidor ficarão visíveis publicamente para as pessoas que souberem o nome do tópico.",
"reservation_delete_dialog_action_delete_title": "Excluir mensagens e anexos armazenados em cache",
"reservation_delete_dialog_action_delete_description": "As mensagens e os anexos armazenados em cache serão excluídos permanentemente. Essa ação não pode ser desfeita.",
"reservation_delete_dialog_submit_button": "Excluir reserva",
"error_boundary_button_reload_ntfy": "Recarregar ntfy",
"web_push_subscription_expiring_title": "As notificações serão pausadas",
"web_push_subscription_expiring_body": "Abra o ntfy para continuar recebendo notificações",
"web_push_unknown_notification_title": "Notificação desconhecida recebida do servidor",
"web_push_unknown_notification_body": "Talvez seja necessário atualizar o ntfy abrindo o aplicativo da Web"
}

View File

@@ -27,8 +27,8 @@
"alert_notification_permission_required_title": "Notificările sunt dezactivate",
"alert_notification_permission_required_button": "Permite acum",
"alert_not_supported_title": "Notificările nu sunt acceptate",
"alert_not_supported_description": "Notificările nu sunt acceptate în browser.",
"alert_notification_permission_required_description": "Permite browser-ului să afișeze notificări.",
"alert_not_supported_description": "Notificările nu sunt acceptate în browserul tău",
"alert_notification_permission_required_description": "Permite browser-ului să afișeze notificări",
"notifications_list": "Lista de notificări",
"notifications_list_item": "Notificare",
"notifications_mark_read": "Marchează ca citit",
@@ -102,9 +102,9 @@
"publish_dialog_emoji_picker_show": "Alege un emoji",
"notifications_loading": "Încărcare notificări…",
"publish_dialog_priority_low": "Prioritate joasă",
"signup_form_username": "Nume de utilizator",
"signup_form_button_submit": "Înscrie-te",
"common_copy_to_clipboard": "Copiază în clipboard",
"signup_form_username": "Utilizator",
"signup_form_button_submit": "Înregistrare",
"common_copy_to_clipboard": "Copiază",
"signup_form_toggle_password_visibility": "Schimbă vizibilitatea parolei",
"signup_title": "Crează un cont ntfy",
"signup_already_have_account": "Deja ai un cont? Autentifică-te!",
@@ -123,5 +123,124 @@
"message_bar_show_dialog": "Arată dialogul de publicare",
"signup_error_username_taken": "Numele de utilizator {{username}} este deja folosit",
"login_title": "Autentifică-te în contul ntfy",
"action_bar_reservation_add": "Rezervă topicul"
"action_bar_reservation_add": "Rezervă topicul",
"action_bar_mute_notifications": "Oprește notificările",
"action_bar_unmute_notifications": "Pornește notificările",
"nav_topics_title": "Subiecte abonate",
"publish_dialog_chip_attach_url_label": "Atașează fișier prin URL",
"publish_dialog_call_label": "Apel telefonic",
"publish_dialog_button_cancel_sending": "Anulează trimiterea",
"subscribe_dialog_subscribe_title": "Abonează-te la subiect",
"subscribe_dialog_login_password_label": "Parolă",
"subscribe_dialog_login_button_login": "Autentificare",
"subscribe_dialog_error_user_not_authorized": "Utilizatorul {{username}} nu este autorizat",
"account_basics_title": "Cont",
"account_basics_username_title": "Nume de utilizator",
"account_basics_username_description": "Hei, ești tu ❤",
"subscribe_dialog_error_topic_already_reserved": "Subiectul este deja rezervat",
"publish_dialog_attached_file_title": "Fișier atașat:",
"publish_dialog_attached_file_filename_placeholder": "Nume fișier atașat",
"publish_dialog_attached_file_remove": "Elimină fișierul atașat",
"emoji_picker_search_placeholder": "Caută emoji",
"nav_button_muted": "Notificări dezactivate",
"alert_notification_permission_denied_title": "Notificările sunt blocate",
"alert_notification_ios_install_required_description": "Apasă pe butonul Partajare și Adăugați la ecranul principal pentru a porni notificările pe iOS",
"alert_notification_ios_install_required_title": "Instalare iOS necesară",
"alert_notification_permission_denied_description": "Repornește-le în browserul tău",
"alert_not_supported_context_description": "Notificările sunt acceptate doar prin HTTPS. Aceasta este o limitare a <mdnLink>API-ului de notificări</mdnLink>.",
"notifications_actions_failed_notification": "Acțiune nereușită",
"publish_dialog_email_placeholder": "Adresă către care se va redirecționa notificarea, ex. phil@example.com",
"publish_dialog_email_reset": "Șterge redirecționare email",
"publish_dialog_call_item": "Apelează numărul de telefon {{number}}",
"publish_dialog_attach_label": "URL atașament",
"publish_dialog_attach_placeholder": "Atașează fișier prin URL, ex. https://f-droid.org/F-Droid.apk",
"publish_dialog_attach_reset": "Șterge atașament URL",
"publish_dialog_filename_label": "Nume fișier",
"publish_dialog_filename_placeholder": "Nume fișier atașament",
"publish_dialog_delay_label": "Întârziere",
"publish_dialog_call_reset": "Șterge apel telefonic",
"publish_dialog_delay_placeholder": "Întârzie livrarea, ex. {{unixTimestamp}}, {{relativeTime}}, sau \"{{naturalLanguage}}\" (doar engleză)",
"publish_dialog_delay_reset": "Șterge livrare întârziată",
"publish_dialog_other_features": "Alte funcționalități:",
"publish_dialog_chip_click_label": "Accesează URL-ul",
"publish_dialog_chip_email_label": "Redirecționează către email",
"publish_dialog_chip_call_label": "Apel telefonic",
"publish_dialog_chip_call_no_verified_numbers_tooltip": "Nu există numere de telefon verificate",
"publish_dialog_chip_attach_file_label": "Atașează fișier local",
"publish_dialog_chip_delay_label": "Întârziere livrare",
"publish_dialog_chip_topic_label": "Schimbă subiectul",
"publish_dialog_details_examples_description": "Pentru exemple și o descriere detaliată a tuturor funcțiilor de trimitere, vă rugăm să consultați <docsLink>documentația</docsLink>.",
"publish_dialog_button_cancel": "Anulează",
"publish_dialog_button_send": "Trimite",
"publish_dialog_checkbox_markdown": "Formatează ca Markdown",
"publish_dialog_checkbox_publish_another": "Publică altul",
"publish_dialog_drop_file_here": "Trage fișierul aici",
"emoji_picker_search_clear": "Șterge căutarea",
"subscribe_dialog_subscribe_description": "Subiectele nu pot fi protejate prin parolă, așa că alege un nume care să nu fie ușor de ghicit. Odată abonat, poți utiliza metodele PUT/POST pentru a trimite notificări.",
"subscribe_dialog_subscribe_topic_placeholder": "Nume subiect, de exemplu, phil_alerts",
"subscribe_dialog_subscribe_use_another_label": "Foloseșste alt server",
"subscribe_dialog_subscribe_use_another_background_info": "Notificările de la alte servere nu vor fi primite atunci când aplicația web nu este deschisă",
"subscribe_dialog_subscribe_base_url_label": "URL serviciu",
"subscribe_dialog_subscribe_button_generate_topic_name": "Generează nume",
"subscribe_dialog_subscribe_button_cancel": "Anulează",
"subscribe_dialog_subscribe_button_subscribe": "Abonează-te",
"subscribe_dialog_login_title": "Autentificare necesară",
"subscribe_dialog_login_description": "Acest subiect este protejat prin parolă. Vă rugăm să introduceți numele de utilizator și parola pentru a vă abona.",
"subscribe_dialog_login_username_label": "Nume de utilizator, de exemplu, phil",
"subscribe_dialog_error_user_anonymous": "anonim",
"account_basics_tier_interval_monthly": "lunar",
"account_basics_password_dialog_title": "Schimbă parola",
"account_basics_password_dialog_current_password_label": "Parola actuală",
"account_basics_phone_numbers_copied_to_clipboard": "Numărul de telefon a fost copiat",
"account_basics_username_admin_tooltip": "Sunteți administrator",
"account_basics_tier_paid_until": "Abonamentul este plătit până la {{date}}, și se va reînnoi automat",
"account_basics_tier_payment_overdue": "Plata dvs. este restantă. Actualizați metoda de plată sau contul dvs. va fi retrogradat în curând.",
"account_basics_tier_interval_yearly": "anual",
"account_basics_tier_upgrade_button": "Upgrade la Pro",
"account_basics_phone_numbers_title": "Numere de telefon",
"account_basics_password_description": "Schimbă parola contului",
"account_basics_password_dialog_confirm_password_label": "Confirmă parola",
"account_basics_password_dialog_button_submit": "Schimbă parola",
"account_basics_password_dialog_current_password_incorrect": "Parola este incorectă",
"account_basics_phone_numbers_dialog_description": "Pentru a folosi funcția de notificare prin apel, trebuie să adăugați și să verificați cel puțin un număr de telefon. Verificare poate fi făcută prin SMS sau apel vocal.",
"account_basics_phone_numbers_description": "Pentru notificări prin apel",
"account_basics_phone_numbers_dialog_verify_button_sms": "Trimite SMS",
"account_basics_phone_numbers_no_phone_numbers_yet": "Încă nu există numere de telefon",
"account_basics_phone_numbers_dialog_title": "Adaugă număr de telefon",
"account_basics_phone_numbers_dialog_number_label": "Număr de telefon",
"account_basics_phone_numbers_dialog_number_placeholder": "e.x. +1222333444",
"account_basics_phone_numbers_dialog_verify_button_call": "Sună-mă",
"account_basics_phone_numbers_dialog_code_label": "Cod de verificare",
"account_basics_phone_numbers_dialog_code_placeholder": "e.x. 123456",
"account_basics_phone_numbers_dialog_check_verification_button": "Confirmă codul",
"account_basics_phone_numbers_dialog_channel_sms": "SMS",
"account_basics_phone_numbers_dialog_channel_call": "Apel",
"account_usage_title": "Utilizare",
"account_usage_unlimited": "Nelimitat",
"account_usage_limits_reset_daily": "Limitele de utilizare sunt resetate zilnic la miezul nopții (UTC)",
"account_basics_tier_title": "Tip de cont",
"account_usage_of_limit": "din {{limit}}",
"account_basics_tier_admin": "Administrator",
"account_basics_tier_admin_suffix_with_tier": "(cu nivelul {{tier}})",
"account_basics_tier_admin_suffix_no_tier": "(niciun nivel)",
"account_basics_tier_basic": "De bază",
"account_basics_tier_change_button": "Schimbă",
"account_basics_password_dialog_new_password_label": "Parola nouă",
"account_basics_password_title": "Parolă",
"account_basics_tier_description": "Nivelul de putere al contului",
"account_basics_tier_free": "Gratuit",
"account_delete_description": "Șterge definitiv contul tău",
"account_usage_messages_title": "Mesaje publicate",
"account_basics_tier_manage_billing_button": "Gestionare facturare",
"account_usage_emails_title": "Emailuri trimise",
"account_usage_calls_title": "Apeluri telefonice efectuate",
"account_usage_calls_none": "Nu se pot efectua apeluri telefonice cu acest cont",
"account_usage_reservations_title": "Subiecte rezervate",
"account_usage_cannot_create_portal_session": "Nu s-a putut deschide portalul de facturare",
"account_delete_title": "Șterge contul",
"account_usage_attachment_storage_description": "{{filesize}} per fișier, șters după {{expiry}}",
"account_usage_attachment_storage_title": "Stocare atașamente",
"account_usage_basis_ip_description": "Statistica și limitele de utilizare pentru acest cont se bazează pe adresa ta IP, așadar pot fi partajate cu alți utilizatori. Limitele afișate mai sus sunt aproximative, bazate pe limitele de viteză existente.",
"account_usage_reservations_none": "Nu există subiecte rezervate pentru acest cont",
"account_basics_tier_canceled_subscription": "Abonamentul tău a fost anulat și va fi retrogradat la un cont gratuit în data de {{date}}."
}

View File

@@ -8,7 +8,7 @@
"notifications_none_for_topic_description": "Чтобы отправить уведомление на данную тему, просто сделаете PUT или POST-запрос на URL-адрес этой темы.",
"notifications_none_for_any_description": "Чтобы отправить уведомление на тему, просто сделаете PUT или POST-запрос на её URL-адрес. Вот пример с использованием одной из ваших тем.",
"notifications_no_subscriptions_title": "Похоже, что у вас ещё нет подписок.",
"alert_notification_permission_required_description": "Разрешите браузеру показывать уведомления.",
"alert_notification_permission_required_description": "Предоставьте браузеру разрешение на отображение уведомлений на рабочем столе",
"notifications_no_subscriptions_description": "Нажмите на ссылку \"{{linktext}}\", чтобы создать или подписаться на тему. После этого Вы сможете отправлять сообщения используя PUT или POST-запросы и получать уведомления здесь.",
"notifications_example": "Пример",
"notifications_more_details": "Для более подробной информации, посетите <websiteLink>наш сайт</websiteLink> или <docsLink>документацию</docsLink>.",
@@ -41,7 +41,7 @@
"publish_dialog_email_label": "Электронная почта",
"message_bar_error_publishing": "Ошибка публикации уведомления",
"alert_not_supported_title": "Уведомления не поддерживаются",
"alert_not_supported_description": "Уведомления не поддерживаются вашим браузером.",
"alert_not_supported_description": "Уведомления не поддерживаются в вашем браузере",
"notifications_copied_to_clipboard": "Скопировано в буфер обмена",
"notifications_attachment_open_button": "Открыть вложение",
"notifications_none_for_topic_title": "Вы ещё не получали уведомления для этой темы.",
@@ -67,7 +67,7 @@
"subscribe_dialog_subscribe_title": "Подписаться на тему",
"publish_dialog_button_cancel": "Отмена",
"subscribe_dialog_subscribe_description": "Темы могут быть не защищены паролем, поэтому укажите сложное имя. После подписки Вы сможете отправлять уведомления используя PUT/POST-запросы.",
"prefs_users_description": "Добавляйте/удаляйте пользователей для защищенных тем. Обратите внимание, что имя пользователя и пароль хранятся в локальном хранилище браузера.",
"prefs_users_description": "Вы можете управлять пользователями для защищённых тем. Учтите, что имя учётные данные хранятся в локальном хранилище браузера.",
"error_boundary_description": "Это не должно было случиться. Нам очень жаль. <br/>Если Вы можете уделить минуту своего времени, пожалуйста <githubLink>сообщите об этом на GitHub</githubLink>, или дайте нам знать через <discordLink>Discord</discordLink> или <matrixLink>Matrix</matrixLink>.",
"publish_dialog_email_placeholder": "Адрес для пересылки уведомления. Например, phil@example.com",
"publish_dialog_attach_placeholder": "Прикрепите файл по URL. Например, https://f-droid.org/F-Droid.apk",
@@ -96,36 +96,36 @@
"subscribe_dialog_subscribe_button_subscribe": "Подписаться",
"subscribe_dialog_login_title": "Требуется авторизация",
"subscribe_dialog_login_description": "Эта тема защищена паролем. Пожалуйста, введите имя пользователя и пароль, чтобы подписаться.",
"subscribe_dialog_login_username_label": "Имя пользователя. Например, phil",
"subscribe_dialog_login_username_label": "Имя пользователя. Например, oleg",
"subscribe_dialog_login_password_label": "Пароль",
"common_back": "Назад",
"subscribe_dialog_login_button_login": "Войти",
"subscribe_dialog_error_user_not_authorized": "Пользователь {{username}} не авторизован",
"subscribe_dialog_error_user_anonymous": "анонимный пользователь",
"prefs_notifications_title": "Уведомления",
"prefs_notifications_sound_title": "Звук уведомления",
"prefs_notifications_sound_description_none": "Уведомления не воспроизводят никаких звуков при получении",
"prefs_notifications_sound_title": "Звук уведомлений",
"prefs_notifications_sound_description_none": "При получении уведомлений не звуки не проигрываются",
"prefs_notifications_sound_no_sound": "Без звука",
"prefs_notifications_min_priority_title": "Минимальный приоритет",
"prefs_notifications_min_priority_description_any": "Показывать все уведомления, независимо от приоритета",
"prefs_notifications_min_priority_description_any": "Показывать все уведомления, независимо от их приоритета",
"prefs_notifications_min_priority_description_x_or_higher": "Показывать уведомления, если приоритет {{number}} ({{name}}) или выше",
"prefs_notifications_min_priority_description_max": "Показывать уведомления, если приоритет равен 5 (максимальный)",
"prefs_notifications_min_priority_any": "Любой приоритет",
"prefs_notifications_min_priority_low_and_higher": "Низкий приоритет и выше",
"prefs_notifications_min_priority_max_only": "Только максимальный приоритет",
"prefs_notifications_delete_after_title": "Удалить уведомления",
"prefs_notifications_delete_after_title": "Удаление уведомлений",
"prefs_notifications_delete_after_never": "Никогда",
"prefs_notifications_delete_after_three_hours": "Через три часа",
"prefs_notifications_sound_description_some": "Уведомления воспроизводят звук {{sound}}",
"prefs_notifications_sound_description_some": "При уведомлениях проигрывается звук {{sound}}",
"prefs_notifications_min_priority_default_and_higher": "Стандартный приоритет и выше",
"prefs_notifications_delete_after_one_day": "Через день",
"prefs_notifications_delete_after_one_week": "Через неделю",
"prefs_notifications_delete_after_one_month": "Через месяц",
"prefs_notifications_delete_after_never_description": "Уведомления никогда не удаляются автоматически",
"prefs_notifications_delete_after_three_hours_description": "Уведомления автоматически удаляются через три часа",
"prefs_notifications_delete_after_one_day_description": "Уведомления автоматически удаляются через один день",
"prefs_notifications_delete_after_one_week_description": "Уведомления автоматически удаляются через неделю",
"prefs_notifications_delete_after_one_month_description": "Уведомления автоматически удаляются через месяц",
"prefs_notifications_delete_after_three_hours_description": "Уведомления удаляются автоматически через три часа",
"prefs_notifications_delete_after_one_day_description": "Уведомления удаляются автоматически через один день",
"prefs_notifications_delete_after_one_week_description": "Уведомления удаляются автоматически через неделю",
"prefs_notifications_delete_after_one_month_description": "Уведомления удаляются автоматически через месяц",
"prefs_users_title": "Управление пользователями",
"prefs_users_add_button": "Добавить пользователя",
"prefs_users_table_user_header": "Пользователь",
@@ -133,7 +133,7 @@
"prefs_users_dialog_title_add": "Добавить пользователя",
"prefs_users_dialog_title_edit": "Редактировать пользователя",
"prefs_users_dialog_base_url_label": "URL-адрес сервера. Например, https://ntfy.sh",
"prefs_users_dialog_username_label": "Имя пользователя. Например, phil",
"prefs_users_dialog_username_label": "Имя пользователя. Например, oleg",
"prefs_users_dialog_password_label": "Пароль",
"common_cancel": "Отмена",
"common_add": "Добавить",
@@ -157,11 +157,11 @@
"emoji_picker_search_clear": "Сбросить поиск",
"account_upgrade_dialog_cancel_warning": "Это действие <strong>отменит Вашу подписку</strong> и переведет Вашую учетную запись на бесплатное обслуживание {{date}}. При наступлении этой даты, все резервирования и сообщения в кэше <strong>будут удалены</strong>.",
"account_tokens_table_create_token_button": "Создать токен доступа",
"account_tokens_table_last_origin_tooltip": "с IP-адреса {{ip}}, нажмите для подробностей",
"account_tokens_table_last_origin_tooltip": "С IP-адреса {{ip}}, нажмите для подробностей",
"account_tokens_dialog_title_edit": "Изменить токен доступа",
"account_delete_dialog_button_cancel": "Отмена",
"account_delete_dialog_billing_warning": "Удаление учетной записи также отменяет все платные подписки. У Вас не будет доступа к порталу оплаты.",
"account_delete_dialog_description": "Это действие безвозвратно удалит Вашу учетную запись, включая все Ваши данные хранящиеся на сервере. После удаления, Ваше имя пользователя не будет доступно для регистрации в течении 7 дней. Если Вы действительно хотите продолжить, пожалуйста введите Ваш пароль ниже.",
"account_delete_dialog_description": "Это действие безвозвратно удалит вашу учётную запись, включая все данные, хранящиеся на сервере. После удаления имя пользователя вашей учётной записи не будет доступно для регистрации в течение 7 дней. Если вы точно хотите продолжить, пожалуйста, введите свой пароль ниже.",
"account_delete_dialog_label": "Пароль",
"reservation_delete_dialog_action_keep_description": "Сообщения и вложения которые находятся в кэше сервера станут доступны всем, кто знает имя темы.",
"prefs_reservations_table": "Список зарезервированных тем",
@@ -173,7 +173,7 @@
"prefs_reservations_table_not_subscribed": "Не подписан",
"prefs_reservations_table_everyone_deny_all": "Только я могу публиковать и подписываться",
"prefs_reservations_table_everyone_read_write": "Все могут публиковать и подписываться",
"prefs_reservations_table_click_to_subscribe": "Нажмите чтобы подписаться",
"prefs_reservations_table_click_to_subscribe": "Нажмите, чтобы подписаться",
"prefs_reservations_dialog_title_add": "Зарезервировать тему",
"prefs_reservations_dialog_title_delete": "Удалить резервирование",
"prefs_reservations_dialog_title_edit": "Изменение резервированной темы",
@@ -202,7 +202,7 @@
"account_tokens_dialog_expires_never": "Токен никогда не истекает",
"prefs_notifications_sound_play": "Воспроизводить выбранный звук",
"account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} зарезервированных тем",
"account_upgrade_dialog_tier_features_emails_other": "{{emails}} эл. сообщений в день",
"account_upgrade_dialog_tier_features_emails_other": "{{emails}} эл. писем в день",
"account_basics_tier_free": "Бесплатный",
"account_tokens_dialog_title_create": "Создать токен доступа",
"account_tokens_dialog_title_delete": "Удалить токен доступа",
@@ -215,11 +215,11 @@
"account_upgrade_dialog_tier_current_label": "Текущая",
"account_upgrade_dialog_button_cancel": "Отмена",
"prefs_users_edit_button": "Редактировать пользователя",
"account_basics_tier_upgrade_button": "Подписаться на Pro",
"account_basics_tier_upgrade_button": "Обновить до Pro",
"account_basics_tier_paid_until": "Подписка оплачена до {{date}} и будет продляться автоматически",
"account_basics_tier_change_button": "Изменить",
"account_delete_dialog_button_submit": "Безвозвратно удалить учетную запись",
"account_upgrade_dialog_title": "Изменить уровень учетной записи",
"account_delete_dialog_button_submit": "Безвозвратно удалить эту учётную запись",
"account_upgrade_dialog_title": "Изменить уровень учётной записи",
"account_usage_basis_ip_description": "Статистика и ограничения на использование учитываются по IP-адресу, поэтому они могут совмещаться с другими пользователями. Уровни, указанные выше, примерно соответствуют текущим ограничениям.",
"publish_dialog_topic_reset": "Сбросить тему",
"account_basics_tier_admin_suffix_no_tier": "(без подписки)",
@@ -231,9 +231,9 @@
"signup_form_toggle_password_visibility": "Показать/скрыть пароль",
"signup_disabled": "Регистрация недоступна",
"signup_error_username_taken": "Имя пользователя {{username}} уже занято",
"signup_title": "Создать учетную запись ntfy",
"signup_already_have_account": "Уже есть учетная запись? Войдите!",
"signup_error_creation_limit_reached": "Лимит на создание учетных записей исчерпан",
"signup_title": "Создать учётную запись ntfy",
"signup_already_have_account": "Уже есть учётная запись? Войдите!",
"signup_error_creation_limit_reached": "Исчерпано ограничение создания учётных записей",
"login_form_button_submit": "Вход",
"login_link_signup": "Регистрация",
"login_disabled": "Вход недоступен",
@@ -249,22 +249,22 @@
"message_bar_publish": "Опубликовать сообщение",
"nav_button_muted": "Уведомления заглушены",
"nav_button_connecting": "установка соединения",
"action_bar_account": "Учетная запись",
"login_title": "Вход в Вашу учетную запись ntfy",
"action_bar_account": "Учётная запись",
"login_title": "Войдите в учётную запись ntfy",
"action_bar_reservation_limit_reached": "Лимит исчерпан",
"action_bar_toggle_mute": "Заглушить/разрешить уведомления",
"nav_button_account": "Учетная запись",
"nav_upgrade_banner_label": "Подпишитесь на ntfy Pro",
"nav_button_account": "Учётная запись",
"nav_upgrade_banner_label": "Подписка ntfy Pro",
"message_bar_show_dialog": "Открыть диалог публикации",
"notifications_list": "Список уведомлений",
"notifications_list_item": "Уведомление",
"notifications_mark_read": "Пометить как прочтенное",
"notifications_mark_read": "Пометить как прочитанное",
"notifications_priority_x": "Приоритет {{priority}}",
"notifications_attachment_image": "Приложенное изображение",
"notifications_attachment_file_audio": "звуковой файл",
"notifications_attachment_file_video": "видео файл",
"notifications_attachment_file_image": "графический файл",
"notifications_attachment_file_app": "исполняемый файл Android",
"notifications_attachment_file_app": "Исполняемый файл Android",
"notifications_attachment_file_document": "другой тип файла",
"notifications_actions_not_supported": "Действие не поддерживается в веб-приложении",
"display_name_dialog_title": "Изменить псевдоним",
@@ -279,12 +279,12 @@
"subscribe_dialog_subscribe_base_url_label": "URL-адрес сервера",
"subscribe_dialog_subscribe_button_generate_topic_name": "Сгенерировать случайное имя",
"subscribe_dialog_error_topic_already_reserved": "Тема уже зарезервирована",
"account_basics_title": "Учетная запись",
"account_basics_title": "Учётная запись",
"account_basics_username_title": "Имя пользователя",
"account_basics_username_admin_tooltip": "Вы Администратор",
"account_basics_username_admin_tooltip": "Вы администратор",
"account_basics_password_title": "Пароль",
"account_basics_username_description": "Это Вы! :)",
"account_basics_password_description": "Смена пароля учетной записи",
"account_basics_username_description": "Это вы! :)",
"account_basics_password_description": "Смена пароля учётной записи",
"account_basics_password_dialog_title": "Смена пароля",
"account_basics_password_dialog_current_password_label": "Текущий пароль",
"account_basics_password_dialog_current_password_incorrect": "Введен неверный пароль",
@@ -292,11 +292,11 @@
"account_usage_of_limit": "из {{limit}}",
"account_usage_unlimited": "Неограниченно",
"account_usage_limits_reset_daily": "Ограничения сбрасываются ежедневно в полночь (UTC)",
"account_basics_tier_description": "Уровень Вашей учетной записи",
"account_basics_tier_description": "Уровень вашей учётной записи",
"account_basics_tier_admin": "Администратор",
"account_basics_tier_admin_suffix_with_tier": "(с {{tier}} подпиской)",
"account_basics_tier_payment_overdue": "У Вас задолженность по оплате. Пожалуйста проверьте метод оплаты, иначе Вы скоро потеряете преимущества Вашей подписки.",
"account_basics_tier_canceled_subscription": "Ваша подписка была отменена; учетная запись перейдет на бесплатное обслуживание {{date}}.",
"account_basics_tier_admin_suffix_with_tier": "(с подпиской {{tier}})",
"account_basics_tier_payment_overdue": "У вас имеется задолженность по оплате. Пожалуйста, проверьте метод оплаты, иначе скоро вы утратите преимущества подписки.",
"account_basics_tier_canceled_subscription": "Ваша подписка была отменена. Учётная запись перейдет на бесплатное обслуживание {{date}}.",
"account_basics_tier_manage_billing_button": "Управление оплатой",
"account_usage_messages_title": "Опубликованные сообщения",
"account_usage_emails_title": "Отправленные электронные сообщения",
@@ -305,8 +305,8 @@
"account_usage_attachment_storage_title": "Хранение вложений",
"account_usage_attachment_storage_description": "{{filesize}} за файл, удаляются спустя {{expiry}}",
"account_usage_cannot_create_portal_session": "Невозможно открыть портал оплаты",
"account_delete_title": "Удалить учетную запись",
"account_delete_description": "Безвозвратно удалить Вашу учетную запись",
"account_delete_title": "Удаление учётной записи",
"account_delete_description": "Безвозвратное удаление этой учётной записи",
"account_upgrade_dialog_button_redirect_signup": "Зарегистрироваться",
"account_upgrade_dialog_button_pay_now": "Оплатить и подписаться",
"account_upgrade_dialog_button_cancel_subscription": "Отменить подписку",
@@ -319,8 +319,8 @@
"account_tokens_table_expires_header": "Истекает",
"account_tokens_dialog_label": "Название, например Radarr notifications",
"prefs_reservations_title": "Зарезервированные темы",
"prefs_reservations_description": "Здесь Вы можете резервировать темы для личного пользования. Резервирование дает Вам возможность управлять темой и настраивать правила доступа к ней для пользователей.",
"prefs_reservations_limit_reached": "Вы исчерпали Ваш лимит на количество зарезервированных тем.",
"prefs_reservations_description": "Здесь вы можете резервировать темы для личного пользования. Резервирование дает возможность управления темой и настройки правил доступа к ней для других пользователей.",
"prefs_reservations_limit_reached": "Лимит количества зарезервированных тем исчерпан.",
"prefs_reservations_add_button": "Добавить тему",
"prefs_reservations_edit_button": "Настройка доступа",
"prefs_reservations_delete_button": "Сбросить правила доступа",
@@ -334,12 +334,12 @@
"alert_not_supported_context_description": "Уведомления поддерживаются только по протоколу HTTPS. Это ограничение <mdnLink>Notifications API</mdnLink>.",
"notifications_delete": "Удалить",
"notifications_new_indicator": "Новое уведомление",
"notifications_actions_http_request_title": "Сделать HTTP {{method}}-запрос на {{url}}",
"notifications_actions_http_request_title": "Отправить HTTP {{method}}-запрос на {{url}}",
"display_name_dialog_placeholder": "Псевдоним",
"account_basics_password_dialog_new_password_label": "Новый пароль",
"account_basics_password_dialog_confirm_password_label": "Подтвердите пароль",
"account_basics_password_dialog_button_submit": "Сменить пароль",
"account_basics_tier_title": "Тип учетной записи",
"account_basics_tier_title": "Тип учётной записи",
"error_boundary_unsupported_indexeddb_description": "Веб-приложение ntfy использует IndexedDB, который не поддерживается Вашим браузером в приватном режиме.<br/><br/>Хотя это и не лучший вариант, использовать веб-приложение ntfy в приватном режиме не имеет особого смысла, так как все данные храняться в локальном хранилище браузера. Вы можете узнать больше в <githubLink>этом отчете на GitHub</githubLink> или связавшись с нами через <discordLink>Discord</discordLink> или <matrixLink>Matrix</matrixLink>.",
"account_basics_tier_interval_monthly": "ежемесячно",
"account_basics_tier_interval_yearly": "ежегодно",
@@ -356,11 +356,11 @@
"publish_dialog_call_reset": "Удалить вызов",
"account_basics_phone_numbers_dialog_description": "Для того что бы использовать возможность уведомлений о вызовах, нужно добавить и проверить хотя бы один номер телефона. Проверить можно используя SMS или звонок.",
"account_basics_phone_numbers_dialog_title": "Добавить номер телефона",
"account_basics_phone_numbers_dialog_number_placeholder": "например +1222333444",
"account_basics_phone_numbers_dialog_code_placeholder": "например 123456",
"account_basics_phone_numbers_dialog_number_placeholder": "например, +72223334444",
"account_basics_phone_numbers_dialog_code_placeholder": "например, 123456",
"account_basics_phone_numbers_dialog_verify_button_sms": "Отправить SMS",
"account_usage_calls_title": "Совершённые вызовы",
"account_usage_calls_none": "Невозможно совершать вызовы с этим аккаунтом",
"account_usage_calls_none": "Невозможно совершать вызовы с этой учётной записью",
"publish_dialog_chip_call_no_verified_numbers_tooltip": "Нет проверенных номеров",
"account_basics_phone_numbers_copied_to_clipboard": "Номер телефона скопирован в буфер обмена",
"account_upgrade_dialog_tier_features_no_calls": "Нет вызовов",
@@ -371,8 +371,8 @@
"account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} зарезервированная тема",
"account_basics_phone_numbers_no_phone_numbers_yet": "Телефонных номеров пока нет",
"publish_dialog_chip_call_label": "Звонок",
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} ежедневное письмо",
"account_upgrade_dialog_tier_features_messages_one": "{{messages}} ежедневное сообщения",
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} эл. письмо в день",
"account_upgrade_dialog_tier_features_messages_one": "{{messages}} сообщение в день",
"account_basics_phone_numbers_description": "Для уведомлений о телефонных звонках",
"publish_dialog_call_label": "Звонок",
"account_basics_phone_numbers_dialog_channel_call": "Позвонить",
@@ -380,5 +380,28 @@
"account_basics_phone_numbers_dialog_code_label": "Проверочный код",
"account_basics_phone_numbers_dialog_verify_button_call": "Позвонить мне",
"publish_dialog_call_item": "Вызов телефонного номера {{number}}",
"account_basics_phone_numbers_dialog_channel_sms": "SMS"
"account_basics_phone_numbers_dialog_channel_sms": "SMS",
"action_bar_mute_notifications": "Заглушить уведомления",
"action_bar_unmute_notifications": "Разрешить уведомления",
"alert_notification_permission_denied_title": "Уведомления не разрешены",
"alert_notification_permission_denied_description": "Пожалуйста, разрешите отправку уведомлений браузере",
"alert_notification_ios_install_required_title": "iOS требует установку",
"alert_notification_ios_install_required_description": "Нажмите на значок \"Поделиться\" и \"Добавить на главный экран\", чтобы включить уведомления на iOS",
"error_boundary_button_reload_ntfy": "Перезагрузить ntfy",
"web_push_subscription_expiring_title": "Уведомления будут приостановлены",
"web_push_subscription_expiring_body": "Откройте ntfy, чтобы продолжать получать уведомления",
"web_push_unknown_notification_title": "Получено неизвестное уведомление от сервера",
"web_push_unknown_notification_body": "Вам может потребоваться обновить ntfy, для этого откройте веб-приложение",
"prefs_notifications_web_push_title": "Фоновые уведомления",
"prefs_notifications_web_push_enabled_description": "Уведомления приходят даже когда веб-приложение не запущено (через Web Push)",
"prefs_notifications_web_push_disabled_description": "Уведомления приходят, когда веб-приложение запущено (через WebSocket)",
"prefs_appearance_theme_title": "Тема",
"prefs_notifications_web_push_enabled": "Включено для {{server}}",
"prefs_notifications_web_push_disabled": "Выключено",
"notifications_actions_failed_notification": "Неудачное действие",
"publish_dialog_checkbox_markdown": "Форматировать как Markdown",
"subscribe_dialog_subscribe_use_another_background_info": "Уведомления с других серверов не будут получены, когда веб-приложение не открыто",
"prefs_appearance_theme_system": "Как в системе (по умолчанию)",
"prefs_appearance_theme_dark": "Тёмная",
"prefs_appearance_theme_light": "Светлая"
}

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