Compare commits
1429 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86157fc7f6 | ||
|
|
279c164bf5 | ||
|
|
743b00e59c | ||
|
|
eddf654b96 | ||
|
|
886be722bc | ||
|
|
6886ca24b1 | ||
|
|
856f150958 | ||
|
|
5a1aa68ead | ||
|
|
cc9f9c0d24 | ||
|
|
8deb2df88d | ||
|
|
603273ab9d | ||
|
|
64b0bd63af | ||
|
|
220372d65a | ||
|
|
353fedb93f | ||
|
|
dfd12528f3 | ||
|
|
6d5cc6aeac | ||
|
|
9f3883eaf0 | ||
|
|
a0ebd64461 | ||
|
|
dafd130fe5 | ||
|
|
11e9e1e6a0 | ||
|
|
b23f6632b1 | ||
|
|
6bacf7dafc | ||
|
|
0e200b96e0 | ||
|
|
3ce56879ae | ||
|
|
48efdffa57 | ||
|
|
9135bb277b | ||
|
|
711899ad35 | ||
|
|
01435d5fea | ||
|
|
8ce2188b28 | ||
|
|
c1ee163cab | ||
|
|
fcf57a04e1 | ||
|
|
0ad06e808f | ||
|
|
2cc4bf7d28 | ||
|
|
5ae3774f19 | ||
|
|
94eb121f38 | ||
|
|
e81be48bf3 | ||
|
|
db4a4776d3 | ||
|
|
3d54260f79 | ||
|
|
a712d78e4c | ||
|
|
ea3008c707 | ||
|
|
2e39a1329c | ||
|
|
ff2b8167b4 | ||
|
|
96638b516c | ||
|
|
dd9b36cf0a | ||
|
|
44f20f6b4c | ||
|
|
a3c16d81f8 | ||
|
|
c0a5a1fb35 | ||
|
|
b5f24b12dc | ||
|
|
898a52ec63 | ||
|
|
7066ba92a6 | ||
|
|
8960459520 | ||
|
|
f581af6d27 | ||
|
|
88016a2549 | ||
|
|
a7603d1dfb | ||
|
|
65e2af9efb | ||
|
|
090cd720c1 | ||
|
|
7bc8edf7c3 | ||
|
|
13768d4289 | ||
|
|
37d71051de | ||
|
|
5ad3de2904 | ||
|
|
66ea25c18b | ||
|
|
1ab7ca876c | ||
|
|
fd8cd5ca91 | ||
|
|
7cffbfcd6d | ||
|
|
dffee9ea7d | ||
|
|
239959e2a4 | ||
|
|
75abf2e245 | ||
|
|
bfbe73aea3 | ||
|
|
b3721c1b71 | ||
|
|
544f7ef1cb | ||
|
|
2dd152df3f | ||
|
|
2856793eff | ||
|
|
16bec00b38 | ||
|
|
f51e99dc80 | ||
|
|
aca9a77498 | ||
|
|
1c2550d749 | ||
|
|
1c32ee7613 | ||
|
|
f356309f70 | ||
|
|
39936a95f8 | ||
|
|
e18d64933f | ||
|
|
20ccee5d7d | ||
|
|
3575abcde3 | ||
|
|
2cd444ece0 | ||
|
|
eca1ed4d8d | ||
|
|
4ce9508fd3 | ||
|
|
4f9f1292f1 | ||
|
|
d64376c5ac | ||
|
|
275487b3fd | ||
|
|
9301546e6d | ||
|
|
49b3c724cf | ||
|
|
7091775234 | ||
|
|
90667e7176 | ||
|
|
414bbb0171 | ||
|
|
6e6d35157d | ||
|
|
c0f57a448d | ||
|
|
b348bce06e | ||
|
|
06577c99f2 | ||
|
|
f48a9aef2c | ||
|
|
293dda3e79 | ||
|
|
f52b29931f | ||
|
|
3dea1b12cf | ||
|
|
d83be77727 | ||
|
|
6215113c91 | ||
|
|
954ac89b51 | ||
|
|
76fbe19fd1 | ||
|
|
b27c4cf95d | ||
|
|
d915552788 | ||
|
|
c66ad4d7e3 | ||
|
|
152a6b96d1 | ||
|
|
f642b09ebf | ||
|
|
ec414d29be | ||
|
|
0fbd3e33f3 | ||
|
|
17870cb471 | ||
|
|
4e03c96108 | ||
|
|
6cacdd47f2 | ||
|
|
a380860cab | ||
|
|
7f842eaeb1 | ||
|
|
3e019bfe88 | ||
|
|
430135606b | ||
|
|
62a79d9802 | ||
|
|
d9d02dbbc1 | ||
|
|
e0a9e3aa56 | ||
|
|
ce878a5d03 | ||
|
|
43fe1b9ad8 | ||
|
|
b59f451c6a | ||
|
|
693d2d630f | ||
|
|
025c2963a0 | ||
|
|
76bf4a3de7 | ||
|
|
b7ae47d61c | ||
|
|
4fa265ed28 | ||
|
|
b531bc95ea | ||
|
|
da6f6f528c | ||
|
|
926d8b981b | ||
|
|
997923dd98 | ||
|
|
8131d0d883 | ||
|
|
a326dc034f | ||
|
|
736905c1e8 | ||
|
|
857d56c281 | ||
|
|
cef61b2c48 | ||
|
|
b483891bcb | ||
|
|
bb9bbdf736 | ||
|
|
9e1636a3f7 | ||
|
|
315326ffc0 | ||
|
|
65188b1f07 | ||
|
|
5fefcd1296 | ||
|
|
4c4a5725d8 | ||
|
|
ab2a686937 | ||
|
|
01bb0eeccd | ||
|
|
7cabc8bcec | ||
|
|
95f4e58ca0 | ||
|
|
692a1fa532 | ||
|
|
8293a24cf9 | ||
|
|
83b5470bc5 | ||
|
|
2aae3577cb | ||
|
|
54ded9db9a | ||
|
|
db2b3a0dd8 | ||
|
|
0b4bcf573e | ||
|
|
f9a88f841e | ||
|
|
c4291cc23e | ||
|
|
fd0595b547 | ||
|
|
b59e18bed8 | ||
|
|
1956ffbf02 | ||
|
|
e647c68cb9 | ||
|
|
53dce3013d | ||
|
|
7615aa86ad | ||
|
|
07ef3e5656 | ||
|
|
2804acf0f5 | ||
|
|
498b632796 | ||
|
|
61e496fc4c | ||
|
|
83e74b014e | ||
|
|
364696e059 | ||
|
|
546c94ba98 | ||
|
|
203d739d38 | ||
|
|
89fd37bc2c | ||
|
|
7856ab5dfb | ||
|
|
773be05bb1 | ||
|
|
c1a6e9a429 | ||
|
|
c9a0a40805 | ||
|
|
439049624c | ||
|
|
a6078037c0 | ||
|
|
9c5c17441f | ||
|
|
b56d250708 | ||
|
|
a714fe2618 | ||
|
|
e863872d0e | ||
|
|
77406f3496 | ||
|
|
157add4835 | ||
|
|
50f3563477 | ||
|
|
e08f3670d1 | ||
|
|
4f6f45a9c0 | ||
|
|
3de04b27ab | ||
|
|
ec1f97b726 | ||
|
|
569d89e8f8 | ||
|
|
f2f146e39b | ||
|
|
18d08298cc | ||
|
|
ebb386af58 | ||
|
|
b105ed6727 | ||
|
|
1916376f8d | ||
|
|
965110b2c3 | ||
|
|
c8ac104043 | ||
|
|
f6bd0a8d51 | ||
|
|
e39498702d | ||
|
|
9b97067b10 | ||
|
|
f72f0d800f | ||
|
|
5244e0be14 | ||
|
|
6eb25f68ac | ||
|
|
efe7c3fa70 | ||
|
|
ce4b2ae9a0 | ||
|
|
ba86e08ffe | ||
|
|
2d9e2356b1 | ||
|
|
fe5c844a21 | ||
|
|
97410db301 | ||
|
|
887751cd5d | ||
|
|
044326068c | ||
|
|
57a51ab2da | ||
|
|
998dbd9054 | ||
|
|
a5a55bd43a | ||
|
|
00409d834b | ||
|
|
d9ab7cc78d | ||
|
|
99a2ca8802 | ||
|
|
ea338ae4fa | ||
|
|
32fa8d43c1 | ||
|
|
0f166e0a1d | ||
|
|
46e423fc40 | ||
|
|
eac523dcf9 | ||
|
|
4225ce2f42 | ||
|
|
d35dfc14d1 | ||
|
|
cef228f880 | ||
|
|
bcfb50b35a | ||
|
|
c4c4916bc8 | ||
|
|
81463614c9 | ||
|
|
15a7f86344 | ||
|
|
3c1da90f47 | ||
|
|
27151d1cac | ||
|
|
a1c6dd2085 | ||
|
|
8f930acfb8 | ||
|
|
08d44703c3 | ||
|
|
82282419fe | ||
|
|
e290d1307f | ||
|
|
747c5c9fff | ||
|
|
9f987e66fa | ||
|
|
b91ff5f0b5 | ||
|
|
23ec7702fc | ||
|
|
f8082d9481 | ||
|
|
d9ecee7200 | ||
|
|
149c13e9d8 | ||
|
|
07e9670a09 | ||
|
|
0e67228605 | ||
|
|
2578236d8d | ||
|
|
fe545423c5 | ||
|
|
f3c67f1d71 | ||
|
|
27b3a89247 | ||
|
|
1470afb715 | ||
|
|
b495a744c9 | ||
|
|
d2b5917e2b | ||
|
|
52ca98611c | ||
|
|
1b394e9bb8 | ||
|
|
0d36ab8af3 | ||
|
|
141ddb3a51 | ||
|
|
f99801a2e6 | ||
|
|
4457e9e26f | ||
|
|
4eb7dc563c | ||
|
|
269373d75d | ||
|
|
ef275ac0c1 | ||
|
|
f59df0f40a | ||
|
|
214f70e62f | ||
|
|
51af114b2e | ||
|
|
83bf9d4d6c | ||
|
|
f298d947bd | ||
|
|
d87d8a2db4 | ||
|
|
50c564d8a2 | ||
|
|
c807b5db21 | ||
|
|
4d1baae6d0 | ||
|
|
34bc551303 | ||
|
|
0847a6406e | ||
|
|
006f73af7d | ||
|
|
f4a74dac57 | ||
|
|
1f34c39eb0 | ||
|
|
8783c86cd6 | ||
|
|
892e82ceb8 | ||
|
|
8b4834929d | ||
|
|
f0d5392e9e | ||
|
|
dde07adbdc | ||
|
|
57df16dd62 | ||
|
|
ae62e0d955 | ||
|
|
4603802f62 | ||
|
|
610792b902 | ||
|
|
b1e935da45 | ||
|
|
93e14b73bb | ||
|
|
81a486adc1 | ||
|
|
8bf4727a1c | ||
|
|
2a468493f9 | ||
|
|
3ac3e2ec7c | ||
|
|
fea0f301d2 | ||
|
|
1ce08a18c0 | ||
|
|
8d6f1eecdf | ||
|
|
c0b5151bae | ||
|
|
650f492d7d | ||
|
|
1f2c76e63d | ||
|
|
efef587671 | ||
|
|
3c8ac4a1e1 | ||
|
|
f5247c50f4 | ||
|
|
1edbda4f31 | ||
|
|
de7b7218e4 | ||
|
|
19a4e95a3a | ||
|
|
4578835a8f | ||
|
|
aead619dea | ||
|
|
deeefee8c0 | ||
|
|
5e380e147f | ||
|
|
ba5c3a164d | ||
|
|
47da3aeea6 | ||
|
|
9ed96e5d8b | ||
|
|
04aff72631 | ||
|
|
6fbcd85d17 | ||
|
|
8f60294c5b | ||
|
|
677b44ce61 | ||
|
|
000248e6aa | ||
|
|
359c789c34 | ||
|
|
34e9a771ce | ||
|
|
60b8588129 | ||
|
|
7eeaeb8398 | ||
|
|
c99d8b66c2 | ||
|
|
960f690dd6 | ||
|
|
54514454bf | ||
|
|
d8c8f31846 | ||
|
|
5ccc131e73 | ||
|
|
ae27c3a5ab | ||
|
|
48cb816111 | ||
|
|
ff904a5ca6 | ||
|
|
8e7de80353 | ||
|
|
9c8a8f8795 | ||
|
|
df73c6f655 | ||
|
|
c1e657db8b | ||
|
|
62c8a13ed4 | ||
|
|
994266ab04 | ||
|
|
a41e3a1e76 | ||
|
|
16900d2c10 | ||
|
|
86bec660bf | ||
|
|
30301c8a7f | ||
|
|
7b470a7f6f | ||
|
|
9d5891963a | ||
|
|
de8e3bc2aa | ||
|
|
d3f7aa7008 | ||
|
|
bbfaf2fc4d | ||
|
|
db4ac158e3 | ||
|
|
7a33e16945 | ||
|
|
eac49feb04 | ||
|
|
849884c947 | ||
|
|
2cb4d089ab | ||
|
|
dc797f8594 | ||
|
|
061677a78b | ||
|
|
b4f15ec9d4 | ||
|
|
af17661053 | ||
|
|
635ec88c4f | ||
|
|
905f048ab4 | ||
|
|
7f86108379 | ||
|
|
425e6d064e | ||
|
|
ebb61fcccf | ||
|
|
9f72eb804d | ||
|
|
42af71e546 | ||
|
|
df818cfebc | ||
|
|
0de1990c01 | ||
|
|
f40023aa23 | ||
|
|
5765a707fc | ||
|
|
5eb84f759b | ||
|
|
df7dd9c498 | ||
|
|
6fe3913aee | ||
|
|
0ad9716241 | ||
|
|
f4c37ccfb9 | ||
|
|
7182d3a4e5 | ||
|
|
eecd3245f0 | ||
|
|
4dc3b38c95 | ||
|
|
9edab24d4c | ||
|
|
3b627b27b3 | ||
|
|
80462f7ee5 | ||
|
|
65e377ec63 | ||
|
|
45e1707d3b | ||
|
|
0581a9e680 | ||
|
|
0fb60ae72d | ||
|
|
e36e4856c9 | ||
|
|
fa48639517 | ||
|
|
2b40ad9a12 | ||
|
|
ad7ab18fb7 | ||
|
|
8f9dafce20 | ||
|
|
69cf773834 | ||
|
|
b2b9891a58 | ||
|
|
3bf02d3cd9 | ||
|
|
8777990d2d | ||
|
|
70f0e7ccc7 | ||
|
|
adfacf820e | ||
|
|
35e15cfd9d | ||
|
|
4e2a884da5 | ||
|
|
29cf4f16d1 | ||
|
|
609c9fa37d | ||
|
|
2eb5eb3e29 | ||
|
|
a92306b181 | ||
|
|
047cc22dba | ||
|
|
f31d777b69 | ||
|
|
ac983cd9bc | ||
|
|
dd45fd90b7 | ||
|
|
e76e6274a3 | ||
|
|
161ce468fe | ||
|
|
04df6f1390 | ||
|
|
79852fec59 | ||
|
|
92de1b5a88 | ||
|
|
fc93de9a28 | ||
|
|
ae9fa85676 | ||
|
|
b26666f635 | ||
|
|
70a9301e25 | ||
|
|
86c548ae37 | ||
|
|
1e1b2be464 | ||
|
|
1b8906f1fd | ||
|
|
b81f7b21a9 | ||
|
|
db2dc09189 | ||
|
|
5f6b7e6f82 | ||
|
|
6daf4141c6 | ||
|
|
41083cfd07 | ||
|
|
c03f795508 | ||
|
|
58d7cb8ef8 | ||
|
|
8acf0f4350 | ||
|
|
236b7b7a16 | ||
|
|
871883f6e9 | ||
|
|
a92c8a9ec9 | ||
|
|
1c6aa49fca | ||
|
|
49d258706d | ||
|
|
bbce1200b4 | ||
|
|
94d0c5a335 | ||
|
|
7835fc65c4 | ||
|
|
dc6b8ece1e | ||
|
|
f595dff66f | ||
|
|
0514ea4ac0 | ||
|
|
1598087e1f | ||
|
|
3709ea689a | ||
|
|
f4aba12546 | ||
|
|
521fe791b0 | ||
|
|
6d15b9face | ||
|
|
9fbe7804dd | ||
|
|
faa4dcbcee | ||
|
|
ad3e7960ce | ||
|
|
3234189cd2 | ||
|
|
e64a0bd8c9 | ||
|
|
97a59f19e0 | ||
|
|
7067d8aa77 | ||
|
|
5999653456 | ||
|
|
9ce6b03450 | ||
|
|
7e916516e0 | ||
|
|
09c2b4bdca | ||
|
|
978ee81df3 | ||
|
|
86f2ab8a55 | ||
|
|
e4aff00455 | ||
|
|
e88f24bae7 | ||
|
|
b4797ef212 | ||
|
|
2f8c0e4d5d | ||
|
|
56231f9288 | ||
|
|
ef7c7c7b09 | ||
|
|
88e4b8f0e6 | ||
|
|
090bdd93ba | ||
|
|
790044e899 | ||
|
|
7aab7d387f | ||
|
|
a461aafb91 | ||
|
|
1569c22a65 | ||
|
|
2091ceb4d2 | ||
|
|
ec337b5de9 | ||
|
|
5a245f889c | ||
|
|
ee595067ba | ||
|
|
bd4b5e9e1b | ||
|
|
786e588397 | ||
|
|
2dfb53ec53 | ||
|
|
a30c5eb9cf | ||
|
|
d96d4b03c7 | ||
|
|
3257ce91ef | ||
|
|
f563b671c8 | ||
|
|
ea1cda5f92 | ||
|
|
36ba27ba09 | ||
|
|
60eccba2fa | ||
|
|
aec4b97fae | ||
|
|
389ae682a5 | ||
|
|
3f21da7768 | ||
|
|
0ad266a495 | ||
|
|
bd192edf1e | ||
|
|
d1ac8d03e0 | ||
|
|
44b7c2f198 | ||
|
|
cdae5493e2 | ||
|
|
f110472204 | ||
|
|
03aeb707f2 | ||
|
|
3f1342c05b | ||
|
|
8b95b1a213 | ||
|
|
d4dfd3f657 | ||
|
|
c1d718ee68 | ||
|
|
bd08a120cd | ||
|
|
c9126e7aa9 | ||
|
|
db9b974e47 | ||
|
|
889a6f03f8 | ||
|
|
6af8d03470 | ||
|
|
950ba1e2e1 | ||
|
|
6b2cfb1d1d | ||
|
|
35458230a8 | ||
|
|
bd39cf4b54 | ||
|
|
f739a3067e | ||
|
|
2344eee2c6 | ||
|
|
5822a2ec41 | ||
|
|
a49cafbadb | ||
|
|
0aee6252bb | ||
|
|
0e6a483b2f | ||
|
|
20c014ba8d | ||
|
|
926967b6e7 | ||
|
|
6345e7f864 | ||
|
|
80bc600ff0 | ||
|
|
758828e7aa | ||
|
|
4c179b7d9d | ||
|
|
27398e7d72 | ||
|
|
19f8a35588 | ||
|
|
8feb0f1a2e | ||
|
|
9241b0550c | ||
|
|
136b656ccb | ||
|
|
c844c24a16 | ||
|
|
90f21ba408 | ||
|
|
b843c69c16 | ||
|
|
630f2957de | ||
|
|
d243c22510 | ||
|
|
fbf325a630 | ||
|
|
84f421a464 | ||
|
|
d38c149263 | ||
|
|
fc3624cd50 | ||
|
|
78533e27fe | ||
|
|
02e46c1d03 | ||
|
|
81f05b3f15 | ||
|
|
eb700b4b6c | ||
|
|
89c884ab4d | ||
|
|
0fe0b0c9d7 | ||
|
|
bad3ef43b7 | ||
|
|
903ef71b6f | ||
|
|
5726f8e9ba | ||
|
|
5b10cd660b | ||
|
|
333d901661 | ||
|
|
112efaae90 | ||
|
|
61bb8a0286 | ||
|
|
be2bebf517 | ||
|
|
a4cf40907b | ||
|
|
6562ba6987 | ||
|
|
6da554d1e5 | ||
|
|
72f36f8296 | ||
|
|
e8685baf15 | ||
|
|
f8085f8686 | ||
|
|
3139c13e50 | ||
|
|
4a2b5676d9 | ||
|
|
a12195d3c7 | ||
|
|
412e78c4d0 | ||
|
|
22bdc91630 | ||
|
|
e94c2fef52 | ||
|
|
694363013d | ||
|
|
fb6a408cca | ||
|
|
89437019fb | ||
|
|
a095ab56bb | ||
|
|
92905fd860 | ||
|
|
01c216d506 | ||
|
|
999678565b | ||
|
|
3454a5ca16 | ||
|
|
63c96b4e80 | ||
|
|
003fec5f83 | ||
|
|
f0d8f0ad8e | ||
|
|
20cca8e888 | ||
|
|
49a548252c | ||
|
|
21dbcf65dc | ||
|
|
dee213d90c | ||
|
|
19b99e8285 | ||
|
|
0c68b6a2c7 | ||
|
|
76b753062d | ||
|
|
ceec0bc71d | ||
|
|
6ecd96cf6e | ||
|
|
8d38672baf | ||
|
|
36a149dd7a | ||
|
|
1249d9473a | ||
|
|
5941a8f2a6 | ||
|
|
2e8daa962c | ||
|
|
3f4d0ef3ea | ||
|
|
0fba690d02 | ||
|
|
5211d06f2c | ||
|
|
7121d14bfa | ||
|
|
d5a1e38082 | ||
|
|
3ad61c4736 | ||
|
|
9d3fc20e58 | ||
|
|
0be467f809 | ||
|
|
ec75ce0787 | ||
|
|
d11b1007ef | ||
|
|
c542dd8c6f | ||
|
|
37697aed27 | ||
|
|
4360d157b2 | ||
|
|
c3c4d65f99 | ||
|
|
ffd7645c0b | ||
|
|
043738a475 | ||
|
|
fb52ad6fdb | ||
|
|
29318f9d61 | ||
|
|
030f7266f7 | ||
|
|
9692de1469 | ||
|
|
eab90a0275 | ||
|
|
e6f70f8e41 | ||
|
|
499b0dd839 | ||
|
|
31d0c812ce | ||
|
|
d37f861f6b | ||
|
|
0a49a8d88b | ||
|
|
b63ef0defb | ||
|
|
f8068ef561 | ||
|
|
2608687e98 | ||
|
|
02564a40c7 | ||
|
|
bdd49f4e16 | ||
|
|
33b603def5 | ||
|
|
6eff5553b5 | ||
|
|
7cac03c1ec | ||
|
|
b33918f267 | ||
|
|
f68ad6acdf | ||
|
|
a533bf9efb | ||
|
|
66ea805cde | ||
|
|
7c3b6e4521 | ||
|
|
9cb3d056fe | ||
|
|
4111bee0c4 | ||
|
|
e4c2b938d3 | ||
|
|
fc7cf5933f | ||
|
|
e4d22ebd8b | ||
|
|
69d6e0f890 | ||
|
|
ecab7fbf65 | ||
|
|
75887e4a62 | ||
|
|
130039f5c8 | ||
|
|
bec0d4807b | ||
|
|
5ee62033b5 | ||
|
|
3e02d7b0bb | ||
|
|
290ed1124e | ||
|
|
fc62682334 | ||
|
|
28404565d2 | ||
|
|
f8548e9d46 | ||
|
|
d90b290cd2 | ||
|
|
21c6776269 | ||
|
|
7fed392e0c | ||
|
|
913b59b5e3 | ||
|
|
4692ca7b7f | ||
|
|
af16542d02 | ||
|
|
5511812e30 | ||
|
|
547b09a7e5 | ||
|
|
b9c176ddba | ||
|
|
f971377cbb | ||
|
|
a04f2f9c9a | ||
|
|
763eafd5dd | ||
|
|
9247dac50d | ||
|
|
de65d07518 | ||
|
|
1966f80855 | ||
|
|
4b2e38320d | ||
|
|
83356f565e | ||
|
|
7fd5f0b29d | ||
|
|
03737dbf5c | ||
|
|
867cf28080 | ||
|
|
b2eb5b94bd | ||
|
|
df7d6baec5 | ||
|
|
a4f5c8dee7 | ||
|
|
4c0ec3f75b | ||
|
|
12bbe9a1ae | ||
|
|
0a589f6242 | ||
|
|
ab2dd6136e | ||
|
|
4d64515e45 | ||
|
|
411597ecc2 | ||
|
|
1a426da913 | ||
|
|
7936c38feb | ||
|
|
d0beaa900f | ||
|
|
f4bf8fd9bb | ||
|
|
d866cb2fd9 | ||
|
|
0ab6171962 | ||
|
|
b7c2898b9c | ||
|
|
d155ebb3a4 | ||
|
|
3d5bb92774 | ||
|
|
a2f1a45097 | ||
|
|
b43fff7c7e | ||
|
|
b186c7a324 | ||
|
|
e50b6f4075 | ||
|
|
e5342d5eca | ||
|
|
a25fc1daaa | ||
|
|
6e75108aa9 | ||
|
|
0735a5cb48 | ||
|
|
088aede833 | ||
|
|
2119954f67 | ||
|
|
220b012ae3 | ||
|
|
e1278a5e92 | ||
|
|
0ae62d36d2 | ||
|
|
70729edb2b | ||
|
|
de2f7d3e9b | ||
|
|
8c69234e28 | ||
|
|
a9b7c4530b | ||
|
|
76fc015775 | ||
|
|
ef302d22a9 | ||
|
|
7ab8ef73a3 | ||
|
|
7616b24e30 | ||
|
|
05c1b264f2 | ||
|
|
35ccfdb8d8 | ||
|
|
d744204052 | ||
|
|
6c3aca4cd6 | ||
|
|
d54db74f5a | ||
|
|
4e1980c2cc | ||
|
|
40f990fffe | ||
|
|
8931f25ac5 | ||
|
|
94f60fb5b8 | ||
|
|
01b397a31a | ||
|
|
f2cd1edc57 | ||
|
|
243123fd7e | ||
|
|
36b33030f3 | ||
|
|
17709f2fb7 | ||
|
|
a8c17c1856 | ||
|
|
8ac920d28c | ||
|
|
239c620707 | ||
|
|
f2e600d681 | ||
|
|
6486db99fa | ||
|
|
766ca05e15 | ||
|
|
6c41f69db7 | ||
|
|
cae696c323 | ||
|
|
42dc8bc3f5 | ||
|
|
aecf0a5f25 | ||
|
|
896a1b007f | ||
|
|
fc5973751a | ||
|
|
9ca5133c76 | ||
|
|
509f59ac3c | ||
|
|
9d8482a119 | ||
|
|
34343fae02 | ||
|
|
1ff6ecf5d8 | ||
|
|
f2f7ad8253 | ||
|
|
78de5d4866 | ||
|
|
9449b0b875 | ||
|
|
b86c50c60a | ||
|
|
34f90facb2 | ||
|
|
99a0c72d49 | ||
|
|
c4d9e397ab | ||
|
|
3f49f51847 | ||
|
|
1d2b759dc0 | ||
|
|
8e62d2af6f | ||
|
|
7dfcda9306 | ||
|
|
b6983e6866 | ||
|
|
4cfc64e528 | ||
|
|
020968961e | ||
|
|
2cfe18c9c8 | ||
|
|
39dc90e795 | ||
|
|
fe4101d26c | ||
|
|
80861d33b4 | ||
|
|
1383b41062 | ||
|
|
99c03b0e83 | ||
|
|
bb4b5d2bc8 | ||
|
|
4916806298 | ||
|
|
f350b81da4 | ||
|
|
1e306e08c9 | ||
|
|
518505fa9d | ||
|
|
1f4a0601f8 | ||
|
|
0347fdc192 | ||
|
|
d207f3ca26 | ||
|
|
f2d6f09671 | ||
|
|
0f44d20da5 | ||
|
|
aaf53d5d3f | ||
|
|
154e3945c9 | ||
|
|
0a82729650 | ||
|
|
5876c923a2 | ||
|
|
a2b56154a7 | ||
|
|
4b1468cfd8 | ||
|
|
00fe639a95 | ||
|
|
94781c89f3 | ||
|
|
6e2f9a5bdd | ||
|
|
a3312f69fb | ||
|
|
87bcf59bd4 | ||
|
|
4817e33942 | ||
|
|
141cbad5ad | ||
|
|
aaa4976c7d | ||
|
|
2525ef71a9 | ||
|
|
75a2cb9236 | ||
|
|
6b82cb2b7a | ||
|
|
260e24f68b | ||
|
|
3c8fabe409 | ||
|
|
1c3ed3ea40 | ||
|
|
497f45e5cd | ||
|
|
f9a411c307 | ||
|
|
7d755ce604 | ||
|
|
f64dbcb6b2 | ||
|
|
0b41356179 | ||
|
|
0928d99813 | ||
|
|
e724aace49 | ||
|
|
b65044712b | ||
|
|
22f48c5ad3 | ||
|
|
1b4d55fb30 | ||
|
|
ad8b22482b | ||
|
|
780c149c81 | ||
|
|
859a4e4f79 | ||
|
|
d0c2b00fbf | ||
|
|
37091f25a8 | ||
|
|
7f1855ad4d | ||
|
|
b42958de9f | ||
|
|
73eaf7b8b6 | ||
|
|
52361a1c48 | ||
|
|
9b48d674b4 | ||
|
|
c0fab933a5 | ||
|
|
df6d760844 | ||
|
|
c6b6c81c83 | ||
|
|
c2d6e0e316 | ||
|
|
5acc6ad0c4 | ||
|
|
a533f352b2 | ||
|
|
262bb723d9 | ||
|
|
a97b6de37e | ||
|
|
9f738e4a85 | ||
|
|
8895bd77c1 | ||
|
|
404fd4c720 | ||
|
|
058de2d761 | ||
|
|
16d490474d | ||
|
|
bd2088c480 | ||
|
|
c42f6289f6 | ||
|
|
92cfa4040b | ||
|
|
3f3af275e7 | ||
|
|
28c653043e | ||
|
|
abe7275f0c | ||
|
|
d4af2be7a0 | ||
|
|
8dd4c3e3c0 | ||
|
|
af25f164ed | ||
|
|
64ede0f11c | ||
|
|
d3565c9b87 | ||
|
|
c332c132fa | ||
|
|
b3534aecda | ||
|
|
8e04912201 | ||
|
|
909f9b3d24 | ||
|
|
cad38573d7 | ||
|
|
a3663e43e4 | ||
|
|
6d451785f0 | ||
|
|
7791901b2d | ||
|
|
2afe1fbeed | ||
|
|
e2097e856e | ||
|
|
03e7a3ea65 | ||
|
|
27f8cc0e52 | ||
|
|
1aa82ff06a | ||
|
|
0ff1f6520a | ||
|
|
ff2a354333 | ||
|
|
543709336c | ||
|
|
afd6d2e0ee | ||
|
|
32efbd5823 | ||
|
|
6dbdabf9fd | ||
|
|
75d57b9f04 | ||
|
|
554547b431 | ||
|
|
b811da6b83 | ||
|
|
ca6bc1dcb0 | ||
|
|
7c3fd42a86 | ||
|
|
04f12d1e2f | ||
|
|
c6b8ea90b7 | ||
|
|
7f8fb8d571 | ||
|
|
f8cfb084e0 | ||
|
|
70b084457a | ||
|
|
6c12244587 | ||
|
|
e7c0365079 | ||
|
|
43b11de596 | ||
|
|
ef45ea5a50 | ||
|
|
483edb70bf | ||
|
|
7516d25bc6 | ||
|
|
2f2918bd3b | ||
|
|
7a5572ad7c | ||
|
|
73d2b3363b | ||
|
|
5c9cebf059 | ||
|
|
ba0cc7fbf9 | ||
|
|
b7f37138f8 | ||
|
|
53a451671c | ||
|
|
65dff6e8e3 | ||
|
|
03a2de961d | ||
|
|
b94310a4cc | ||
|
|
9c594da847 | ||
|
|
93e62de3d2 | ||
|
|
a3efbb3466 | ||
|
|
aaf01b98d2 | ||
|
|
af037b9d70 | ||
|
|
5dafd7e4a7 | ||
|
|
e2b5f4a9fb | ||
|
|
2e58f0db10 | ||
|
|
26b31acbae | ||
|
|
66e96244ef | ||
|
|
4dc0183901 | ||
|
|
d33eded060 | ||
|
|
5913142389 | ||
|
|
66ef28c2e2 | ||
|
|
19c30fc411 | ||
|
|
50bed826d0 | ||
|
|
b5851dd6d4 | ||
|
|
ff1ee7d292 | ||
|
|
9455428048 | ||
|
|
0f919f3d49 | ||
|
|
d556a675e9 | ||
|
|
bfc1fa5181 | ||
|
|
d9387dac99 | ||
|
|
4818ee57b6 | ||
|
|
addb5efebb | ||
|
|
8adb9ee633 | ||
|
|
418fc98d1a | ||
|
|
beffe4a1f2 | ||
|
|
ef15b44a1b | ||
|
|
bc802bfc77 | ||
|
|
d10a5df3df | ||
|
|
b05d27ce45 | ||
|
|
e61c9fdde9 | ||
|
|
d2e2791729 | ||
|
|
68a7756621 | ||
|
|
42063cbd5c | ||
|
|
a407a2e0f8 | ||
|
|
6ec1ccf7a3 | ||
|
|
044f4182d0 | ||
|
|
bae30d79c9 | ||
|
|
25a60969fb | ||
|
|
528a67722b | ||
|
|
d29dc95962 | ||
|
|
fc3d4dcf5e | ||
|
|
3d4218324f | ||
|
|
6a10bac017 | ||
|
|
f6fbb45978 | ||
|
|
dee16f543d | ||
|
|
9959d1aa43 | ||
|
|
76146c4e74 | ||
|
|
8a8023fcf8 | ||
|
|
4b0d1e448d | ||
|
|
6748a2f2f3 | ||
|
|
4c4d772a5f | ||
|
|
85740d810b | ||
|
|
2305ebca24 | ||
|
|
59bf388534 | ||
|
|
3066b95a6d | ||
|
|
1bd77a83bd | ||
|
|
d0b7336da7 | ||
|
|
c80f71bd9b | ||
|
|
15fa3b7d9f | ||
|
|
e2d7f2cf29 | ||
|
|
3031fb910f | ||
|
|
d999dbe0a0 | ||
|
|
60d5e66e34 | ||
|
|
c6964502c4 | ||
|
|
ca2633ff82 | ||
|
|
a1625c7f15 | ||
|
|
30a913c05c | ||
|
|
1d02933481 | ||
|
|
62c2ec0614 | ||
|
|
45ca20dec9 | ||
|
|
de362d2322 | ||
|
|
115e6e9cf8 | ||
|
|
f17538b7df | ||
|
|
6f68c8cd1f | ||
|
|
f565302a0f | ||
|
|
6a3f169a47 | ||
|
|
6bd8875375 | ||
|
|
02dd72ba57 | ||
|
|
63629efae7 | ||
|
|
9015b27803 | ||
|
|
a5f0670f7f | ||
|
|
d7db395016 | ||
|
|
99eef493d2 | ||
|
|
0d395249ff | ||
|
|
5cf1da974a | ||
|
|
2f0ec88f40 | ||
|
|
d9d3c4a724 | ||
|
|
bc4d4f424a | ||
|
|
67459650d4 | ||
|
|
c31bce1e2d | ||
|
|
3e3b556108 | ||
|
|
723daf9497 | ||
|
|
f77958fc35 | ||
|
|
ea9f2c6e35 | ||
|
|
3691e59af1 | ||
|
|
7d20238423 | ||
|
|
31131db756 | ||
|
|
17e634c563 | ||
|
|
a7dc3d84e0 | ||
|
|
b80aec90d0 | ||
|
|
8544733048 | ||
|
|
140fdcca81 | ||
|
|
2e08c48742 | ||
|
|
4800bb05d2 | ||
|
|
384cabede5 | ||
|
|
4e9eeb1fa1 | ||
|
|
a534cc9eca | ||
|
|
e52132c85b | ||
|
|
76667ffcf9 | ||
|
|
8ba4b72b37 | ||
|
|
81e1417ce5 | ||
|
|
c1576b5b19 | ||
|
|
86cc3b9607 | ||
|
|
c7f85e6283 | ||
|
|
6a93dc9d54 | ||
|
|
dfd08b337c | ||
|
|
2d1f2f319f | ||
|
|
68f82b9182 | ||
|
|
c8f880c701 | ||
|
|
f2d3f0bdf9 | ||
|
|
9f8c63c7d5 | ||
|
|
2b5a1a7a1c | ||
|
|
499b2fb0d6 | ||
|
|
b7679c7826 | ||
|
|
ce01a66ff3 | ||
|
|
7582be1a39 | ||
|
|
f989fd0743 | ||
|
|
097e84aeed | ||
|
|
faadb5148f | ||
|
|
8d9fa31f3d | ||
|
|
56ed4f0515 | ||
|
|
43981bb675 | ||
|
|
cd38511ad4 | ||
|
|
53f13fd811 | ||
|
|
77cc52e4ac | ||
|
|
35cb4606f6 | ||
|
|
d01ed355e0 | ||
|
|
495fb24b9a | ||
|
|
911fe9e9f8 | ||
|
|
311ffc3672 | ||
|
|
7a1488fcd3 | ||
|
|
9f255aee25 | ||
|
|
67603e58bf | ||
|
|
4267c0d9b6 | ||
|
|
88eb728fe3 | ||
|
|
26c835cdd1 | ||
|
|
7d3d697a20 | ||
|
|
798ee3c23c | ||
|
|
7581058c93 | ||
|
|
4f0ddfc30d | ||
|
|
0b918464c1 | ||
|
|
57bd37ef2f | ||
|
|
9fa1288dbc | ||
|
|
55eed868fa | ||
|
|
abb1baeecd | ||
|
|
5784b07f14 | ||
|
|
8e1e0b3740 | ||
|
|
3f42e0e945 | ||
|
|
9146e439d2 | ||
|
|
7a14a0b81f | ||
|
|
9247475ab2 | ||
|
|
6b4c04c390 | ||
|
|
e8216ae9e7 | ||
|
|
365a0b2832 | ||
|
|
f78389b6ef | ||
|
|
0d231d8bd9 | ||
|
|
d838790b8f | ||
|
|
9ce3545901 | ||
|
|
64ac111d55 | ||
|
|
e9f170a197 | ||
|
|
e359499e79 | ||
|
|
48a5a55e2f | ||
|
|
4828e3a691 | ||
|
|
e607944ad1 | ||
|
|
d790ad91e2 | ||
|
|
4f39c7c155 | ||
|
|
8db569e8a5 | ||
|
|
f3932e4b65 | ||
|
|
d40b776205 | ||
|
|
9dbac2cb33 | ||
|
|
9216dbe28a | ||
|
|
95cfe16676 | ||
|
|
dabb6a481f | ||
|
|
d294a692d2 | ||
|
|
0266c707cc | ||
|
|
0b3e268f2c | ||
|
|
12df164245 | ||
|
|
d51ca20992 | ||
|
|
4a1adaeab2 | ||
|
|
fd5bfd161d | ||
|
|
0c496ca223 | ||
|
|
175ab5ea76 | ||
|
|
5627097a6c | ||
|
|
94fb23ba17 | ||
|
|
dbd8ed14bf | ||
|
|
789078e916 | ||
|
|
833293ad77 | ||
|
|
a8d3297c4e | ||
|
|
532fd3c560 | ||
|
|
0c937d02df | ||
|
|
8a800a4cb2 | ||
|
|
8f6f97b8e4 | ||
|
|
79df1c9040 | ||
|
|
9a71c3d8dc | ||
|
|
74788893e9 | ||
|
|
5c0ecc0250 | ||
|
|
c0ac2c95ca | ||
|
|
be4c80e201 | ||
|
|
32a110b601 | ||
|
|
48d1f7887d | ||
|
|
dd02267f9b | ||
|
|
142a297552 | ||
|
|
9aeea4d9fa | ||
|
|
e8ecd6b006 | ||
|
|
71b961d3f3 | ||
|
|
271056a4aa | ||
|
|
141565d9d2 | ||
|
|
c400c5571f | ||
|
|
d266579be1 | ||
|
|
f61c67e6be | ||
|
|
5f6d753cb7 | ||
|
|
8211b4cc24 | ||
|
|
000a3e005c | ||
|
|
d7aacb8b24 | ||
|
|
6615aea5dc | ||
|
|
27a4e58fb1 | ||
|
|
4c7dc4c1ba | ||
|
|
5ce78660cf | ||
|
|
89f5cc577e | ||
|
|
dc7dd836c6 | ||
|
|
88c6b4adae | ||
|
|
020996ea04 | ||
|
|
30a8f66db2 | ||
|
|
9ba733d4e0 | ||
|
|
fafe478e5c | ||
|
|
b7bb4459f9 | ||
|
|
3cd61d8278 | ||
|
|
2d45e397a7 | ||
|
|
ff7e894e4c | ||
|
|
7db25d71dd | ||
|
|
2283cc4ce6 | ||
|
|
341e84f643 | ||
|
|
c43a1166e2 | ||
|
|
6e95d62726 | ||
|
|
b197ea3ab6 | ||
|
|
fa418eef16 | ||
|
|
83eb4c39e5 | ||
|
|
2dcad150eb | ||
|
|
eebe4f8920 | ||
|
|
4dc89f6bc5 | ||
|
|
9403873a7b | ||
|
|
ad36f5db46 | ||
|
|
e96e35b40b | ||
|
|
aeb60735dc | ||
|
|
67948d0767 | ||
|
|
e2120bc66d | ||
|
|
67b9d2eaf6 | ||
|
|
7083ed9f6b | ||
|
|
790fd43369 | ||
|
|
6b38499bdc | ||
|
|
cf050cc289 | ||
|
|
390d42c607 | ||
|
|
8ccfa5c3fb | ||
|
|
8073bb4e24 | ||
|
|
9e19183471 | ||
|
|
ae3e8a0094 | ||
|
|
2d0c043dfd | ||
|
|
a8def0aed2 | ||
|
|
4e44b034bd | ||
|
|
e6c83b6efb | ||
|
|
1dbcfe3c6e | ||
|
|
58992fc795 | ||
|
|
eb220544a3 | ||
|
|
9d5556c7f5 | ||
|
|
1abcc88fce | ||
|
|
2e8292a65f | ||
|
|
4704b2a0e4 | ||
|
|
9e4eafe8d5 | ||
|
|
966ffe1669 | ||
|
|
9d38aeb863 | ||
|
|
d3ac976d05 | ||
|
|
4ce6fdcc5a | ||
|
|
75a4b5bd88 | ||
|
|
2f5acee798 | ||
|
|
46798ac322 | ||
|
|
a8db08c7d4 | ||
|
|
f3db0e083e | ||
|
|
18edff9afe | ||
|
|
03aa67ed68 | ||
|
|
46f34ca1e3 | ||
|
|
0f0074cbab | ||
|
|
47ad024ec7 | ||
|
|
4944e3ae4b | ||
|
|
7aa3d8f59b | ||
|
|
0c25425346 | ||
|
|
4648f83669 | ||
|
|
44913c1668 | ||
|
|
20c7650e51 | ||
|
|
e8139ad655 | ||
|
|
9e0687e142 | ||
|
|
7f3e4b5f47 | ||
|
|
7b23158e0a | ||
|
|
f94bb1aa30 | ||
|
|
a9fef387fa | ||
|
|
ff5c854192 | ||
|
|
733ef4664b | ||
|
|
e89c62174d | ||
|
|
78e437057c | ||
|
|
7cdd86c99f | ||
|
|
c045f4d21f | ||
|
|
c65b83a6f5 | ||
|
|
2b2753be21 | ||
|
|
fe3db1375a | ||
|
|
2e9eff69d7 | ||
|
|
f58c1e4c84 | ||
|
|
dc8932cd95 | ||
|
|
04cc71af90 | ||
|
|
44d189179d | ||
|
|
d084a415f3 | ||
|
|
953efbee47 | ||
|
|
807f24723d | ||
|
|
453bf435b0 | ||
|
|
ca25b80bfb | ||
|
|
afb585e6fd | ||
|
|
2e7f474775 | ||
|
|
bd39072596 | ||
|
|
7d46f1eed9 | ||
|
|
b222541ea8 | ||
|
|
1368dae849 | ||
|
|
578ccf1643 | ||
|
|
217c660ba0 | ||
|
|
11f8984127 | ||
|
|
232c889ce3 | ||
|
|
02524ca101 | ||
|
|
38bd4f3ce3 | ||
|
|
3101f93d22 | ||
|
|
da17e4ee8a | ||
|
|
d178be7576 | ||
|
|
4d90e32fe9 | ||
|
|
9056d68fc9 | ||
|
|
c16da26780 | ||
|
|
c50633d990 | ||
|
|
517341b5d7 | ||
|
|
e1dd0c64e2 | ||
|
|
e7bf165934 | ||
|
|
a90bd4cd06 | ||
|
|
d1e59fe08c | ||
|
|
6bb5274d83 | ||
|
|
b7c121e78e | ||
|
|
1251a4adab | ||
|
|
4cacc02520 | ||
|
|
7812eb9d19 | ||
|
|
d625a003b8 | ||
|
|
e21327cec5 | ||
|
|
7ccc5be9b4 | ||
|
|
9ebeb7f12f | ||
|
|
d3be1fa359 | ||
|
|
e3d530cb90 | ||
|
|
951c90763a | ||
|
|
59011c8a32 | ||
|
|
8319f1cf26 | ||
|
|
f558b4dbe9 | ||
|
|
d7eb1206fe | ||
|
|
fa29da1a32 | ||
|
|
a64e365add | ||
|
|
c87549e71a | ||
|
|
ca5d736a71 | ||
|
|
2e27f58963 | ||
|
|
6f230a796e | ||
|
|
9e44db78a2 | ||
|
|
a859ed9f58 | ||
|
|
6f6a2d1f69 | ||
|
|
206ea312bf | ||
|
|
3f8784c8a8 | ||
|
|
1761ec0207 | ||
|
|
ceedca4e27 | ||
|
|
ffbf288c9b | ||
|
|
f8a00dd411 | ||
|
|
6a5b5b3763 | ||
|
|
6bd4c8fb71 | ||
|
|
df2872bebd | ||
|
|
0393145f42 | ||
|
|
da06ae4485 | ||
|
|
e10442f6ca | ||
|
|
5379474c41 | ||
|
|
168ad8bf1b | ||
|
|
89cf84b63e | ||
|
|
b3a299ce22 | ||
|
|
7838b253b4 | ||
|
|
7140f18574 | ||
|
|
5345b9063c | ||
|
|
4ad0fb1f57 | ||
|
|
57eabd3aa5 | ||
|
|
df8b18bbb1 | ||
|
|
3b3e6ac2cd | ||
|
|
8ddfd2459d | ||
|
|
25d3a66f91 | ||
|
|
f13a654fe8 | ||
|
|
3e594ec210 | ||
|
|
3cdd300f1c | ||
|
|
af540f0cf7 | ||
|
|
8753bc0283 | ||
|
|
e3b86bc812 | ||
|
|
db9a4f8dee | ||
|
|
f23d09f83f | ||
|
|
0c1cec2ae6 | ||
|
|
b154ce5b0c | ||
|
|
fc1087a42b | ||
|
|
92c384374a | ||
|
|
ac029c389e | ||
|
|
79a3259c86 | ||
|
|
2c81773d01 | ||
|
|
496d6e74b0 | ||
|
|
5e18ced7d2 | ||
|
|
7c574d73de | ||
|
|
deb4f24856 | ||
|
|
4b9e0c5c38 | ||
|
|
69b01bc468 | ||
|
|
f998d4d2ad | ||
|
|
ed0c1abd2f | ||
|
|
04b7b4284a | ||
|
|
6e21bb742f | ||
|
|
e17cf676f4 | ||
|
|
f14f0aaa26 | ||
|
|
fae5e7ead6 | ||
|
|
4fdbd42f50 | ||
|
|
4f4165f46f | ||
|
|
8f87e9008b | ||
|
|
7c69b96fc7 | ||
|
|
5b7c500ca8 | ||
|
|
028f3aad14 | ||
|
|
4fa0655438 | ||
|
|
97fc287b78 | ||
|
|
625b13280f | ||
|
|
539ba43cd1 | ||
|
|
49bd6129ff | ||
|
|
cea434a57c | ||
|
|
214efbde36 | ||
|
|
bd81aef1c9 | ||
|
|
c1db1e4df7 | ||
|
|
f99159ee5b | ||
|
|
d674e0280a | ||
|
|
ebd4367dda | ||
|
|
d4767caf30 | ||
|
|
a26a6be62b | ||
|
|
f4e6874ff0 | ||
|
|
53750e42c5 | ||
|
|
97fe5c3219 | ||
|
|
8b1e9336e7 | ||
|
|
4b7681b311 | ||
|
|
3c2d9040df | ||
|
|
931d3ced09 | ||
|
|
559f09e7be | ||
|
|
5b8520b4e0 | ||
|
|
eb0805a470 | ||
|
|
7677c50b0e | ||
|
|
5bc51eefd9 | ||
|
|
23c1983d3d | ||
|
|
f9e2d6ddcb | ||
|
|
113b7c8a08 | ||
|
|
fa2c09316c | ||
|
|
1b98ea2f99 | ||
|
|
3863357207 | ||
|
|
1c0162c434 | ||
|
|
63f295a41d | ||
|
|
683f6811aa | ||
|
|
b9add76697 | ||
|
|
9d42f9a598 | ||
|
|
6edc7cf29b | ||
|
|
c997e4911a | ||
|
|
9eb94a565d | ||
|
|
d14c4df846 | ||
|
|
d2fa768151 | ||
|
|
6ad3b2e802 | ||
|
|
98671ac695 | ||
|
|
bce305514c | ||
|
|
16dcb54442 | ||
|
|
0a5c21172c | ||
|
|
70d66b7b53 | ||
|
|
0dedbcda35 | ||
|
|
4a8ed8e65f | ||
|
|
95c4490285 | ||
|
|
8a0be007c9 | ||
|
|
ef467d00ae | ||
|
|
918b4e3d61 | ||
|
|
59a5077713 | ||
|
|
35eac5b9ad | ||
|
|
6b1f72fec9 | ||
|
|
824ec39d46 | ||
|
|
cfa8d92af1 | ||
|
|
91d2603fe0 | ||
|
|
6be95f8285 | ||
|
|
4783cb1211 | ||
|
|
113ff55426 | ||
|
|
f2f4bbdbd5 | ||
|
|
d931ce8acc | ||
|
|
b1c0d57fb9 | ||
|
|
b3d11f09ba | ||
|
|
1ccf659781 | ||
|
|
3ad639daed | ||
|
|
dc5dbdf6e5 | ||
|
|
e3998d5fce | ||
|
|
8ad1089053 | ||
|
|
1a6b076e87 | ||
|
|
9db9678952 | ||
|
|
037d1d647d | ||
|
|
cb9be5b732 | ||
|
|
99b9792875 | ||
|
|
9471429cb3 | ||
|
|
ea538338cf | ||
|
|
5825f20e98 | ||
|
|
35ad4a0c03 | ||
|
|
b5b4997957 | ||
|
|
69dcc380a3 | ||
|
|
8e04eeaacd | ||
|
|
c63ca95867 | ||
|
|
d6c0ae130f | ||
|
|
e1339ccde7 | ||
|
|
7c1d892779 | ||
|
|
5f2e238a30 | ||
|
|
f69065ca79 | ||
|
|
d88dbbc90f | ||
|
|
1c731a3cef | ||
|
|
6cd72683ad | ||
|
|
e86bdf46db | ||
|
|
0adbd87387 | ||
|
|
286ae43d1a | ||
|
|
a75fb08ef1 | ||
|
|
58a0c2a6c6 | ||
|
|
d050956007 | ||
|
|
bdae48afba | ||
|
|
cb5c4c5483 | ||
|
|
e91f07a081 | ||
|
|
7d96be6fb3 | ||
|
|
46c798c71a | ||
|
|
037a51a9d0 | ||
|
|
4596e4bcab | ||
|
|
9b30ada880 | ||
|
|
96d711e19e | ||
|
|
5af5565fb1 | ||
|
|
29c9551548 | ||
|
|
23c5d4e345 | ||
|
|
ff5bf4acd0 | ||
|
|
34c42c55f6 | ||
|
|
07e5b28868 | ||
|
|
06a0654a5a | ||
|
|
8cc23117fe | ||
|
|
f8c4f20a8f | ||
|
|
8053e992e4 | ||
|
|
9db96140e2 | ||
|
|
502d0a0abd | ||
|
|
80b0a94f7e | ||
|
|
338cab1660 | ||
|
|
b8836d674a | ||
|
|
c6a96d19e2 | ||
|
|
bcb24aecd3 | ||
|
|
d72ae47d1f | ||
|
|
a5d2fc172b | ||
|
|
bbab81a1a2 | ||
|
|
78a1ca81e3 | ||
|
|
f090d1313e | ||
|
|
afa4efa140 | ||
|
|
d2b88005f0 | ||
|
|
9eb1f6a186 | ||
|
|
2d8d5b3b95 | ||
|
|
93344bcd69 | ||
|
|
e3d6f692e8 | ||
|
|
7b29bf9f42 |
3
.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
||||
dist
|
||||
*/node_modules
|
||||
Dockerfile*
|
||||
11
.git-blame-ignore-revs
Normal file
@@ -0,0 +1,11 @@
|
||||
# https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view
|
||||
|
||||
# Run prettier (https://github.com/binwiederhier/ntfy/pull/746)
|
||||
6f6a2d1f693070bf72e89d86748080e4825c9164
|
||||
c87549e71a10bc789eac8036078228f06e515a8e
|
||||
ca5d736a7169eb6b4b0d849e061d5bf9565dcc53
|
||||
2e27f58963feb9e4d1c573d4745d07770777fa7d
|
||||
|
||||
# Run eslint (https://github.com/binwiederhier/ntfy/pull/748)
|
||||
f558b4dbe9bb5b9e0e87fada1215de2558353173
|
||||
8319f1cf26113167fb29fe12edaff5db74caf35f
|
||||
BIN
.github/images/logo.png
vendored
Normal file
|
After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 297 KiB After Width: | Height: | Size: 297 KiB |
|
Before Width: | Height: | Size: 225 KiB After Width: | Height: | Size: 225 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 224 KiB |
|
Before Width: | Height: | Size: 473 KiB After Width: | Height: | Size: 473 KiB |
43
.github/workflows/build.yaml
vendored
@@ -1,39 +1,24 @@
|
||||
name: build
|
||||
on: [push, pull_request]
|
||||
on: [ push, pull_request ]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.19.x'
|
||||
-
|
||||
name: Install node
|
||||
uses: actions/setup-node@v2
|
||||
go-version: '1.24.x'
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
-
|
||||
name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
-
|
||||
name: Cache Go and npm modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
~/go/bin
|
||||
~/.npm
|
||||
web/node_modules
|
||||
key: ${{ runner.os }}-ntfy-${{ hashFiles('**/go.sum', '**/package.lock') }}
|
||||
restore-keys: ${{ runner.os }}-ntfy-
|
||||
-
|
||||
name: Install dependencies
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: './web/package-lock.json'
|
||||
- name: Install dependencies
|
||||
run: make build-deps-ubuntu
|
||||
-
|
||||
name: Build all the things
|
||||
- name: Build all the things
|
||||
run: make build
|
||||
-
|
||||
name: Print build results and checksums
|
||||
- name: Print build results and checksums
|
||||
run: make cli-build-results
|
||||
|
||||
2
.github/workflows/docs.yaml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
run: |
|
||||
cd build/ntfy-docs.github.io
|
||||
git config user.name "GitHub Actions Bot"
|
||||
git config user.email "<>"
|
||||
git config user.email "<actions@github.com>"
|
||||
git add docs/
|
||||
git commit -m "Updated docs"
|
||||
git push origin main
|
||||
|
||||
44
.github/workflows/release.yaml
vendored
@@ -7,44 +7,28 @@ jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.19.x'
|
||||
-
|
||||
name: Install node
|
||||
uses: actions/setup-node@v2
|
||||
go-version: '1.24.x'
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
-
|
||||
name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
-
|
||||
name: Cache Go and npm modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
~/go/bin
|
||||
~/.npm
|
||||
web/node_modules
|
||||
key: ${{ runner.os }}-ntfy-${{ hashFiles('**/go.sum', '**/package.lock') }}
|
||||
restore-keys: ${{ runner.os }}-ntfy-
|
||||
-
|
||||
name: Docker login
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: './web/package-lock.json'
|
||||
- name: Docker login
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
-
|
||||
name: Install dependencies
|
||||
- name: Install dependencies
|
||||
run: make build-deps-ubuntu
|
||||
-
|
||||
name: Build and publish
|
||||
- name: Build and publish
|
||||
run: make release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
-
|
||||
name: Print build results and checksums
|
||||
- name: Print build results and checksums
|
||||
run: make cli-build-results
|
||||
|
||||
52
.github/workflows/test.yaml
vendored
@@ -1,48 +1,30 @@
|
||||
name: test
|
||||
on: [push, pull_request]
|
||||
on: [ push, pull_request ]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.19.x'
|
||||
-
|
||||
name: Install node
|
||||
uses: actions/setup-node@v2
|
||||
go-version: '1.24.x'
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
-
|
||||
name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
-
|
||||
name: Cache Go and npm modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
~/go/bin
|
||||
~/.npm
|
||||
web/node_modules
|
||||
key: ${{ runner.os }}-ntfy-${{ hashFiles('**/go.sum', '**/package.lock') }}
|
||||
restore-keys: ${{ runner.os }}-ntfy-
|
||||
-
|
||||
name: Install dependencies
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: './web/package-lock.json'
|
||||
- name: Install dependencies
|
||||
run: make build-deps-ubuntu
|
||||
-
|
||||
name: Build docs (required for tests)
|
||||
- name: Build docs (required for tests)
|
||||
run: make docs
|
||||
-
|
||||
name: Build web app (required for tests)
|
||||
- name: Build web app (required for tests)
|
||||
run: make web
|
||||
-
|
||||
name: Run tests, formatting, vetting and linting
|
||||
- name: Run tests, formatting, vetting and linting
|
||||
run: make check
|
||||
-
|
||||
name: Run coverage
|
||||
- name: Run coverage
|
||||
run: make coverage
|
||||
-
|
||||
name: Upload coverage to codecov.io
|
||||
- name: Upload coverage to codecov.io
|
||||
run: make coverage-upload
|
||||
|
||||
4
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
dist/
|
||||
dev-dist/
|
||||
build/
|
||||
.idea/
|
||||
.vscode/
|
||||
@@ -12,3 +13,6 @@ secrets/
|
||||
node_modules/
|
||||
.DS_Store
|
||||
__pycache__
|
||||
web/dev-dist/
|
||||
venv/
|
||||
cmd/key-file.yaml
|
||||
|
||||
115
.goreleaser.yml
@@ -1,76 +1,72 @@
|
||||
version: 2
|
||||
before:
|
||||
hooks:
|
||||
- go mod download
|
||||
- go mod tidy
|
||||
builds:
|
||||
-
|
||||
id: ntfy_linux_amd64
|
||||
- id: ntfy_linux_amd64
|
||||
binary: ntfy
|
||||
env:
|
||||
- CGO_ENABLED=1 # required for go-sqlite3
|
||||
tags: [sqlite_omit_load_extension,osusergo,netgo]
|
||||
tags: [ sqlite_omit_load_extension,osusergo,netgo ]
|
||||
ldflags:
|
||||
- "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
|
||||
goos: [linux]
|
||||
goarch: [amd64]
|
||||
-
|
||||
id: ntfy_linux_armv6
|
||||
goos: [ linux ]
|
||||
goarch: [ amd64 ]
|
||||
- id: ntfy_linux_armv6
|
||||
binary: ntfy
|
||||
env:
|
||||
- CGO_ENABLED=1 # required for go-sqlite3
|
||||
- CC=arm-linux-gnueabi-gcc # apt install gcc-arm-linux-gnueabi
|
||||
tags: [sqlite_omit_load_extension,osusergo,netgo]
|
||||
tags: [ sqlite_omit_load_extension,osusergo,netgo ]
|
||||
ldflags:
|
||||
- "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
|
||||
goos: [linux]
|
||||
goarch: [arm]
|
||||
goarm: [6]
|
||||
-
|
||||
id: ntfy_linux_armv7
|
||||
goos: [ linux ]
|
||||
goarch: [ arm ]
|
||||
goarm: [ 6 ]
|
||||
- id: ntfy_linux_armv7
|
||||
binary: ntfy
|
||||
env:
|
||||
- CGO_ENABLED=1 # required for go-sqlite3
|
||||
- CC=arm-linux-gnueabi-gcc # apt install gcc-arm-linux-gnueabi
|
||||
tags: [sqlite_omit_load_extension,osusergo,netgo]
|
||||
tags: [ sqlite_omit_load_extension,osusergo,netgo ]
|
||||
ldflags:
|
||||
- "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
|
||||
goos: [linux]
|
||||
goarch: [arm]
|
||||
goarm: [7]
|
||||
-
|
||||
id: ntfy_linux_arm64
|
||||
goos: [ linux ]
|
||||
goarch: [ arm ]
|
||||
goarm: [ 7 ]
|
||||
- id: ntfy_linux_arm64
|
||||
binary: ntfy
|
||||
env:
|
||||
- CGO_ENABLED=1 # required for go-sqlite3
|
||||
- CC=aarch64-linux-gnu-gcc # apt install gcc-aarch64-linux-gnu
|
||||
tags: [sqlite_omit_load_extension,osusergo,netgo]
|
||||
tags: [ sqlite_omit_load_extension,osusergo,netgo ]
|
||||
ldflags:
|
||||
- "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
|
||||
goos: [linux]
|
||||
goarch: [arm64]
|
||||
-
|
||||
id: ntfy_windows_amd64
|
||||
goos: [ linux ]
|
||||
goarch: [ arm64 ]
|
||||
- id: ntfy_windows_amd64
|
||||
binary: ntfy
|
||||
env:
|
||||
- CGO_ENABLED=0 # explicitly disable, since we don't need go-sqlite3
|
||||
tags: [noserver] # don't include server files
|
||||
- CGO_ENABLED=1 # required for go-sqlite3
|
||||
- CC=x86_64-w64-mingw32-gcc # apt install gcc-mingw-w64-x86-64
|
||||
tags: [ sqlite_omit_load_extension,osusergo,netgo ]
|
||||
ldflags:
|
||||
- "-X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
|
||||
goos: [windows]
|
||||
goarch: [amd64]
|
||||
- "-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
|
||||
goos: [ windows ]
|
||||
goarch: [amd64 ]
|
||||
-
|
||||
id: ntfy_darwin_all
|
||||
binary: ntfy
|
||||
env:
|
||||
- CGO_ENABLED=0 # explicitly disable, since we don't need go-sqlite3
|
||||
tags: [noserver] # don't include server files
|
||||
tags: [ noserver ] # don't include server files
|
||||
ldflags:
|
||||
- "-X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
|
||||
goos: [darwin]
|
||||
goarch: [amd64, arm64] # will be combined to "universal binary" (see below)
|
||||
goos: [ darwin ]
|
||||
goarch: [ amd64, arm64 ] # will be combined to "universal binary" (see below)
|
||||
nfpms:
|
||||
-
|
||||
package_name: ntfy
|
||||
- package_name: ntfy
|
||||
homepage: https://heckel.io/ntfy
|
||||
maintainer: Philipp C. Heckel <philipp.heckel@gmail.com>
|
||||
description: Simple pub-sub notification service
|
||||
@@ -90,6 +86,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
|
||||
@@ -97,16 +95,15 @@ nfpms:
|
||||
- dst: /var/lib/ntfy
|
||||
type: dir
|
||||
- dst: /usr/share/ntfy/logo.png
|
||||
src: web/public/static/img/ntfy.png
|
||||
src: web/public/static/images/ntfy.png
|
||||
scripts:
|
||||
preinstall: "scripts/preinst.sh"
|
||||
postinstall: "scripts/postinst.sh"
|
||||
preremove: "scripts/prerm.sh"
|
||||
postremove: "scripts/postrm.sh"
|
||||
archives:
|
||||
-
|
||||
id: ntfy_linux
|
||||
builds:
|
||||
- id: ntfy_linux
|
||||
ids:
|
||||
- ntfy_linux_amd64
|
||||
- ntfy_linux_armv6
|
||||
- ntfy_linux_armv7
|
||||
@@ -119,40 +116,32 @@ archives:
|
||||
- server/ntfy.service
|
||||
- client/client.yml
|
||||
- client/ntfy-client.service
|
||||
replacements:
|
||||
amd64: x86_64
|
||||
-
|
||||
id: ntfy_windows
|
||||
builds:
|
||||
- client/user/ntfy-client.service
|
||||
- id: ntfy_windows
|
||||
ids:
|
||||
- ntfy_windows_amd64
|
||||
format: zip
|
||||
formats: [ zip ]
|
||||
wrap_in_directory: true
|
||||
files:
|
||||
- LICENSE
|
||||
- README.md
|
||||
- client/client.yml
|
||||
replacements:
|
||||
amd64: x86_64
|
||||
-
|
||||
id: ntfy_darwin
|
||||
builds:
|
||||
- id: ntfy_darwin
|
||||
ids:
|
||||
- ntfy_darwin_all
|
||||
wrap_in_directory: true
|
||||
files:
|
||||
- LICENSE
|
||||
- README.md
|
||||
- client/client.yml
|
||||
replacements:
|
||||
darwin: macOS
|
||||
universal_binaries:
|
||||
-
|
||||
id: ntfy_darwin_all
|
||||
- id: ntfy_darwin_all
|
||||
replace: true
|
||||
name_template: ntfy
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-next"
|
||||
version_template: "{{ .Tag }}-next"
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
@@ -170,14 +159,14 @@ dockers:
|
||||
- image_templates:
|
||||
- &arm64v8_image "binwiederhier/ntfy:{{ .Tag }}-arm64v8"
|
||||
use: buildx
|
||||
dockerfile: Dockerfile
|
||||
dockerfile: Dockerfile-arm
|
||||
goarch: arm64
|
||||
build_flag_templates:
|
||||
- "--platform=linux/arm64/v8"
|
||||
- image_templates:
|
||||
- &armv7_image "binwiederhier/ntfy:{{ .Tag }}-armv7"
|
||||
use: buildx
|
||||
dockerfile: Dockerfile
|
||||
dockerfile: Dockerfile-arm
|
||||
goarch: arm
|
||||
goarm: 7
|
||||
build_flag_templates:
|
||||
@@ -185,7 +174,7 @@ dockers:
|
||||
- image_templates:
|
||||
- &armv6_image "binwiederhier/ntfy:{{ .Tag }}-armv6"
|
||||
use: buildx
|
||||
dockerfile: Dockerfile
|
||||
dockerfile: Dockerfile-arm
|
||||
goarch: arm
|
||||
goarm: 6
|
||||
build_flag_templates:
|
||||
@@ -203,3 +192,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
|
||||
|
||||
@@ -9,6 +9,7 @@ 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"
|
||||
|
||||
RUN apk add --no-cache tzdata
|
||||
COPY ntfy /usr/bin
|
||||
|
||||
EXPOSE 80/tcp
|
||||
|
||||
18
Dockerfile-arm
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM alpine
|
||||
|
||||
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/"
|
||||
LABEL org.opencontainers.image.source="https://github.com/binwiederhier/ntfy"
|
||||
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"
|
||||
|
||||
# Alpine does not support adding "tzdata" on ARM anymore, see
|
||||
# https://github.com/binwiederhier/ntfy/issues/894
|
||||
|
||||
COPY ntfy /usr/bin
|
||||
|
||||
EXPOSE 80/tcp
|
||||
ENTRYPOINT ["ntfy"]
|
||||
62
Dockerfile-build
Normal file
@@ -0,0 +1,62 @@
|
||||
FROM golang:1.24-bullseye as builder
|
||||
|
||||
ARG VERSION=dev
|
||||
ARG COMMIT=unknown
|
||||
ARG NODE_MAJOR=18
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential ca-certificates curl gnupg \
|
||||
&& mkdir -p /etc/apt/keyrings \
|
||||
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
|
||||
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" >> /etc/apt/sources.list.d/nodesource.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
ADD Makefile .
|
||||
|
||||
# docs
|
||||
ADD ./requirements.txt .
|
||||
RUN make docs-deps
|
||||
ADD ./mkdocs.yml .
|
||||
ADD ./docs ./docs
|
||||
RUN make docs-build
|
||||
|
||||
# web
|
||||
ADD ./web/package.json ./web/package-lock.json ./web/
|
||||
RUN make web-deps
|
||||
ADD ./web ./web
|
||||
RUN make web-build
|
||||
|
||||
# cli & server
|
||||
ADD go.mod go.sum main.go ./
|
||||
ADD ./client ./client
|
||||
ADD ./cmd ./cmd
|
||||
ADD ./log ./log
|
||||
ADD ./server ./server
|
||||
ADD ./user ./user
|
||||
ADD ./util ./util
|
||||
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/"
|
||||
LABEL org.opencontainers.image.source="https://github.com/binwiederhier/ntfy"
|
||||
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
|
||||
|
||||
EXPOSE 80/tcp
|
||||
ENTRYPOINT ["ntfy"]
|
||||
103
Makefile
@@ -1,4 +1,6 @@
|
||||
MAKEFLAGS := --jobs=1
|
||||
PYTHON := python3
|
||||
PIP := pip3
|
||||
VERSION := $(shell git describe --tag)
|
||||
COMMIT := $(shell git rev-parse --short HEAD)
|
||||
|
||||
@@ -29,12 +31,19 @@ help:
|
||||
@echo "Build server & client (without GoReleaser):"
|
||||
@echo " make cli-linux-server - Build client & server (no GoReleaser, current arch, Linux)"
|
||||
@echo " make cli-darwin-server - Build client & server (no GoReleaser, current arch, macOS)"
|
||||
@echo " make cli-windows-server - Build client & server (no GoReleaser, amd64 only, Windows)"
|
||||
@echo " make cli-client - Build client only (no GoReleaser, current arch, Linux/macOS/Windows)"
|
||||
@echo
|
||||
@echo "Build dev Docker:"
|
||||
@echo " make docker-dev - Build client & server for current architecture using Docker only"
|
||||
@echo
|
||||
@echo "Build web app:"
|
||||
@echo " make web - Build the web app"
|
||||
@echo " make web-deps - Install web app dependencies (npm install the universe)"
|
||||
@echo " make web-build - Actually build the web app"
|
||||
@echo " make web-lint - Run eslint on the web app"
|
||||
@echo " make web-fmt - Run prettier on the web app"
|
||||
@echo " make web-fmt-check - Run prettier on the web app, but don't change anything"
|
||||
@echo
|
||||
@echo "Build documentation:"
|
||||
@echo " make docs - Build the documentation"
|
||||
@@ -80,40 +89,46 @@ build: web docs cli
|
||||
update: web-deps-update cli-deps-update docs-deps-update
|
||||
docker pull alpine
|
||||
|
||||
docker-dev:
|
||||
docker build \
|
||||
--file ./Dockerfile-build \
|
||||
--tag binwiederhier/ntfy:$(VERSION) \
|
||||
--tag binwiederhier/ntfy:dev \
|
||||
--build-arg VERSION=$(VERSION) \
|
||||
--build-arg COMMIT=$(COMMIT) \
|
||||
./
|
||||
|
||||
|
||||
# Ubuntu-specific
|
||||
|
||||
build-deps-ubuntu:
|
||||
sudo apt update
|
||||
sudo apt install -y \
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
curl \
|
||||
gcc-aarch64-linux-gnu \
|
||||
gcc-arm-linux-gnueabi \
|
||||
gcc-mingw-w64-x86-64 \
|
||||
python3 \
|
||||
python3-venv \
|
||||
jq
|
||||
which pip3 || sudo apt install -y python3-pip
|
||||
which pip3 || sudo apt-get install -y python3-pip
|
||||
|
||||
|
||||
# Documentation
|
||||
|
||||
docs: docs-deps docs-build
|
||||
|
||||
docs-build: .PHONY
|
||||
@if ! /bin/echo -e "import sys\nif sys.version_info < (3,8):\n exit(1)" | python3; then \
|
||||
if which python3.8; then \
|
||||
echo "python3.8 $(shell which mkdocs) build"; \
|
||||
python3.8 $(shell which mkdocs) build; \
|
||||
else \
|
||||
echo "ERROR: Python version too low. mkdocs-material needs >= 3.8"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
else \
|
||||
echo "mkdocs build"; \
|
||||
mkdocs build; \
|
||||
fi
|
||||
docs-venv: .PHONY
|
||||
$(PYTHON) -m venv ./venv
|
||||
|
||||
docs-deps: .PHONY
|
||||
pip3 install -r requirements.txt
|
||||
docs-build: docs-venv
|
||||
(. venv/bin/activate && $(PYTHON) -m mkdocs build)
|
||||
|
||||
docs-deps: docs-venv
|
||||
(. venv/bin/activate && $(PIP) install -r requirements.txt)
|
||||
|
||||
docs-deps-update: .PHONY
|
||||
pip3 install -r requirements.txt --upgrade
|
||||
(. venv/bin/activate && $(PIP) install -r requirements.txt --upgrade)
|
||||
|
||||
|
||||
# Web app
|
||||
@@ -127,8 +142,7 @@ web-build:
|
||||
&& rm -rf ../server/site \
|
||||
&& mv build ../server/site \
|
||||
&& rm \
|
||||
../server/site/config.js \
|
||||
../server/site/asset-manifest.json
|
||||
../server/site/config.js
|
||||
|
||||
web-deps:
|
||||
cd web && npm install
|
||||
@@ -137,29 +151,37 @@ web-deps:
|
||||
web-deps-update:
|
||||
cd web && npm update
|
||||
|
||||
web-fmt:
|
||||
cd web && npm run format
|
||||
|
||||
web-fmt-check:
|
||||
cd web && npm run format:check
|
||||
|
||||
web-lint:
|
||||
cd web && npm run lint
|
||||
|
||||
# Main server/client build
|
||||
|
||||
cli: cli-deps
|
||||
goreleaser build --snapshot --rm-dist
|
||||
goreleaser build --snapshot --clean
|
||||
|
||||
cli-linux-amd64: cli-deps-static-sites
|
||||
goreleaser build --snapshot --rm-dist --id ntfy_linux_amd64
|
||||
goreleaser build --snapshot --clean --id ntfy_linux_amd64
|
||||
|
||||
cli-linux-armv6: cli-deps-static-sites cli-deps-gcc-armv6-armv7
|
||||
goreleaser build --snapshot --rm-dist --id ntfy_linux_armv6
|
||||
goreleaser build --snapshot --clean --id ntfy_linux_armv6
|
||||
|
||||
cli-linux-armv7: cli-deps-static-sites cli-deps-gcc-armv6-armv7
|
||||
goreleaser build --snapshot --rm-dist --id ntfy_linux_armv7
|
||||
goreleaser build --snapshot --clean --id ntfy_linux_armv7
|
||||
|
||||
cli-linux-arm64: cli-deps-static-sites cli-deps-gcc-arm64
|
||||
goreleaser build --snapshot --rm-dist --id ntfy_linux_arm64
|
||||
goreleaser build --snapshot --clean --id ntfy_linux_arm64
|
||||
|
||||
cli-windows-amd64: cli-deps-static-sites
|
||||
goreleaser build --snapshot --rm-dist --id ntfy_windows_amd64
|
||||
goreleaser build --snapshot --clean --id ntfy_windows_amd64
|
||||
|
||||
cli-darwin-all: cli-deps-static-sites
|
||||
goreleaser build --snapshot --rm-dist --id ntfy_darwin_all
|
||||
goreleaser build --snapshot --clean --id ntfy_darwin_all
|
||||
|
||||
cli-linux-server: cli-deps-static-sites
|
||||
# This is a target to build the CLI (including the server) manually.
|
||||
@@ -181,6 +203,16 @@ cli-darwin-server: cli-deps-static-sites
|
||||
-ldflags \
|
||||
"-linkmode=external -s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(shell date +%s)"
|
||||
|
||||
cli-windows-server: cli-deps-static-sites
|
||||
# This is a target to build the CLI (including the server) for Windows.
|
||||
# Use this for Windows development, if you really don't want to install GoReleaser ...
|
||||
mkdir -p dist/ntfy_windows_server server/docs
|
||||
CC=x86_64-w64-mingw32-gcc GOOS=windows GOARCH=amd64 CGO_ENABLED=1 go build \
|
||||
-o dist/ntfy_windows_server/ntfy.exe \
|
||||
-tags sqlite_omit_load_extension,osusergo,netgo \
|
||||
-ldflags \
|
||||
"-s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(shell date +%s)"
|
||||
|
||||
cli-client: cli-deps-static-sites
|
||||
# This is a target to build the CLI (excluding the server) manually. This should work on Linux/macOS/Windows.
|
||||
# Use this for development, if you really don't want to install GoReleaser ...
|
||||
@@ -193,14 +225,14 @@ cli-client: cli-deps-static-sites
|
||||
|
||||
cli-deps: cli-deps-static-sites cli-deps-all cli-deps-gcc
|
||||
|
||||
cli-deps-gcc: cli-deps-gcc-armv6-armv7 cli-deps-gcc-arm64
|
||||
cli-deps-gcc: cli-deps-gcc-armv6-armv7 cli-deps-gcc-arm64 cli-deps-gcc-windows
|
||||
|
||||
cli-deps-static-sites:
|
||||
mkdir -p server/docs server/site
|
||||
touch server/docs/index.html server/site/app.html
|
||||
|
||||
cli-deps-all:
|
||||
go install github.com/goreleaser/goreleaser@latest
|
||||
go install github.com/goreleaser/goreleaser/v2@latest
|
||||
|
||||
cli-deps-gcc-armv6-armv7:
|
||||
which arm-linux-gnueabi-gcc || { echo "ERROR: ARMv6/ARMv7 cross compiler not installed. On Ubuntu, run: apt install gcc-arm-linux-gnueabi"; exit 1; }
|
||||
@@ -208,11 +240,14 @@ cli-deps-gcc-armv6-armv7:
|
||||
cli-deps-gcc-arm64:
|
||||
which aarch64-linux-gnu-gcc || { echo "ERROR: ARM64 cross compiler not installed. On Ubuntu, run: apt install gcc-aarch64-linux-gnu"; exit 1; }
|
||||
|
||||
cli-deps-gcc-windows:
|
||||
which x86_64-w64-mingw32-gcc || { echo "ERROR: Windows cross compiler not installed. On Ubuntu, run: apt install gcc-mingw-w64-x86-64"; exit 1; }
|
||||
|
||||
cli-deps-update:
|
||||
go get -u
|
||||
go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
go install golang.org/x/lint/golint@latest
|
||||
go install github.com/goreleaser/goreleaser@latest
|
||||
go install github.com/goreleaser/goreleaser/v2@latest
|
||||
|
||||
cli-build-results:
|
||||
cat dist/config.yaml
|
||||
@@ -226,7 +261,7 @@ cli-build-results:
|
||||
|
||||
# Test/check targets
|
||||
|
||||
check: test fmt-check vet lint staticcheck
|
||||
check: test web-fmt-check fmt-check vet web-lint lint staticcheck
|
||||
|
||||
test: .PHONY
|
||||
go test $(shell go list ./... | grep -vE 'ntfy/(test|examples|tools)')
|
||||
@@ -253,7 +288,7 @@ coverage-upload:
|
||||
|
||||
# Lint/formatting targets
|
||||
|
||||
fmt:
|
||||
fmt: web-fmt
|
||||
gofmt -s -w .
|
||||
|
||||
fmt-check:
|
||||
@@ -281,7 +316,7 @@ release: clean cli-deps release-checks docs web check
|
||||
goreleaser release --clean
|
||||
|
||||
release-snapshot: clean cli-deps docs web check
|
||||
goreleaser release --snapshot --skip-publish --clean
|
||||
goreleaser release --snapshot --clean
|
||||
|
||||
release-checks:
|
||||
$(eval LATEST_TAG := $(shell git describe --abbrev=0 --tags | cut -c2-))
|
||||
|
||||
169
README.md
@@ -1,15 +1,27 @@
|
||||

|
||||
<div align="center" markdown="1">
|
||||
<sup>Special thanks to:</sup>
|
||||
<br>
|
||||
<br>
|
||||
<a href="https://go.warp.dev/ntfy">
|
||||
<img alt="Warp sponsorship" width="400" src="https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Github/Sponsor/Warp-Github-LG-02.png">
|
||||
</a>
|
||||
|
||||
### [Warp, built for coding with multiple AI agents.](https://go.warp.dev/ntfy)
|
||||
[Available for MacOS, Linux, & Windows](https://go.warp.dev/ntfy)<br>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||

|
||||
|
||||
# ntfy.sh | Send push notifications to your phone or desktop via PUT/POST
|
||||
[](https://github.com/binwiederhier/ntfy/releases/latest)
|
||||
[](https://pkg.go.dev/heckel.io/ntfy)
|
||||
[](https://pkg.go.dev/heckel.io/ntfy/v2)
|
||||
[](https://github.com/binwiederhier/ntfy/actions)
|
||||
[](https://goreportcard.com/report/github.com/binwiederhier/ntfy)
|
||||
[](https://codecov.io/gh/binwiederhier/ntfy)
|
||||
[](https://discord.gg/cT7ECsZj9w)
|
||||
[](https://matrix.to/#/#ntfy:matrix.org)
|
||||
[](https://matrix.to/#/#ntfy-space:matrix.org)
|
||||
[](https://www.reddit.com/r/ntfy/)
|
||||
[](https://ntfy.statuspage.io/)
|
||||
[](https://gitpod.io/#https://github.com/binwiederhier/ntfy)
|
||||
|
||||
@@ -18,18 +30,24 @@ notification service. With ntfy, you can **send notifications to your phone or d
|
||||
**without having to sign up or pay any fees**. If you'd like to run your own instance of the service, you can easily do
|
||||
so since ntfy is open source.
|
||||
|
||||
You can access the free version of ntfy at **[ntfy.sh](https://ntfy.sh)**. There is also an [open source Android app](https://github.com/binwiederhier/ntfy-android)
|
||||
You can access the free version of ntfy at **[ntfy.sh](https://ntfy.sh)**. There is also an [open-source Android app](https://github.com/binwiederhier/ntfy-android)
|
||||
available on [Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy) or [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/),
|
||||
as well as an [open source iOS app](https://github.com/binwiederhier/ntfy-ios) available on the [App Store](https://apps.apple.com/us/app/ntfy/id1625396347).
|
||||
|
||||
<p>
|
||||
<img src="web/public/static/img/screenshot-curl.png" height="180">
|
||||
<img src="web/public/static/img/screenshot-web-detail.png" height="180">
|
||||
<img src="web/public/static/img/screenshot-phone-main.jpg" height="180">
|
||||
<img src="web/public/static/img/screenshot-phone-detail.jpg" height="180">
|
||||
<img src="web/public/static/img/screenshot-phone-notification.jpg" height="180">
|
||||
<img src=".github/images/screenshot-curl.png" height="180">
|
||||
<img src=".github/images/screenshot-web-detail.png" height="180">
|
||||
<img src=".github/images/screenshot-phone-main.jpg" height="180">
|
||||
<img src=".github/images/screenshot-phone-detail.jpg" height="180">
|
||||
<img src=".github/images/screenshot-phone-notification.jpg" height="180">
|
||||
</p>
|
||||
|
||||
## [ntfy Pro](https://ntfy.sh/app) 💸 🎉
|
||||
I now offer paid plans for [ntfy.sh](https://ntfy.sh/) if you don't want to self-host, or you want to support the development of
|
||||
ntfy (→ [Purchase via web app](https://ntfy.sh/app)). You can **buy a plan for as low as $5/month**.
|
||||
You can also donate via [GitHub Sponsors](https://github.com/sponsors/binwiederhier), and [Liberapay](https://liberapay.com/ntfy).
|
||||
I would be very humbled by your sponsorship. ❤️
|
||||
|
||||
## **[Documentation](https://ntfy.sh/docs/)**
|
||||
|
||||
[Getting started](https://ntfy.sh/docs/) |
|
||||
@@ -38,35 +56,33 @@ as well as an [open source iOS app](https://github.com/binwiederhier/ntfy-ios) a
|
||||
[Install / Self-hosting](https://ntfy.sh/docs/install/) |
|
||||
[Building](https://ntfy.sh/docs/develop/)
|
||||
|
||||
## Chat / forum
|
||||
## Chat/forum
|
||||
There are a few ways to get in touch with me and/or the rest of the community. Feel free to use any of these methods. Whatever
|
||||
works best for you:
|
||||
|
||||
* [Discord server](https://discord.gg/cT7ECsZj9w) - direct chat with the community
|
||||
* [Matrix room #ntfy](https://matrix.to/#/#ntfy:matrix.org) (+ [Matrix space](https://matrix.to/#/#ntfy-space:matrix.org)) - same chat, bridged from Discord
|
||||
* [Reddit r/ntfy](https://www.reddit.com/r/ntfy/) - asynchronous forum (_new as of October 2022_)
|
||||
* [GitHub issues](https://github.com/binwiederhier/ntfy/issues) - questions, features, bugs
|
||||
* [Email](https://heckel.io/about) - reach me directly (_I usually prefer the other methods_)
|
||||
|
||||
## Announcements / beta testers
|
||||
## Announcements/beta testers
|
||||
For announcements of new releases and cutting-edge beta versions, please subscribe to the [ntfy.sh/announcements](https://ntfy.sh/announcements)
|
||||
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 and all 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 already 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>
|
||||
|
||||
<a href="https://go.warp.dev/ntfy"><img src="https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Logos/Warp-Wordmark-Black.png" width="160px"></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>
|
||||
@@ -126,15 +142,102 @@ account costs. Even small donations are very much appreciated. A big fat **Thank
|
||||
<a href="https://github.com/caseodilla"><img src="https://github.com/caseodilla.png" width="40px" /></a>
|
||||
<a href="https://github.com/0xAF"><img src="https://github.com/0xAF.png" width="40px" /></a>
|
||||
<a href="https://github.com/soonoo"><img src="https://github.com/soonoo.png" width="40px" /></a>
|
||||
<a href="https://github.com/nichu42"><img src="https://github.com/soonoo.png" width="40px" /></a>
|
||||
<a href="https://github.com/nichu42"><img src="https://github.com/nichu42.png" width="40px" /></a>
|
||||
<a href="https://github.com/samliebow"><img src="https://github.com/samliebow.png" width="40px" /></a>
|
||||
<a href="https://github.com/johman10"><img src="https://github.com/johman10.png" width="40px" /></a>
|
||||
<a href="https://github.com/R-Gld"><img src="https://github.com/R-Gld.png" width="40px" /></a>
|
||||
<a href="https://github.com/FingerlessGlov3s"><img src="https://github.com/FingerlessGlov3s.png" width="40px" /></a>
|
||||
<a href="https://github.com/Twisterado"><img src="https://github.com/Twisterado.png" width="40px" /></a>
|
||||
<a href="https://github.com/ScrumpyJack"><img src="https://github.com/ScrumpyJack.png" width="40px" /></a>
|
||||
<a href="https://github.com/andrejarrell"><img src="https://github.com/andrejarrell.png" width="40px" /></a>
|
||||
<a href="https://github.com/oaustegard"><img src="https://github.com/oaustegard.png" width="40px" /></a>
|
||||
<a href="https://github.com/CreativeWarlock"><img src="https://github.com/CreativeWarlock.png" width="40px" /></a>
|
||||
<a href="https://github.com/darkdragon-001"><img src="https://github.com/darkdragon-001.png" width="40px" /></a>
|
||||
<a href="https://github.com/jonathan-kosgei"><img src="https://github.com/jonathan-kosgei.png" width="40px" /></a>
|
||||
<a href="https://github.com/KevinWang15"><img src="https://github.com/KevinWang15.png" width="40px" /></a>
|
||||
<a href="https://github.com/darkmattercoder"><img src="https://github.com/darkmattercoder.png" width="40px" /></a>
|
||||
<a href="https://github.com/bmcgonag"><img src="https://github.com/bmcgonag.png" width="40px" /></a>
|
||||
<a href="https://github.com/skorokithakis"><img src="https://github.com/skorokithakis.png" width="40px" /></a>
|
||||
<a href="https://github.com/eenturk"><img src="https://github.com/eenturk.png" width="40px" /></a>
|
||||
<a href="https://github.com/spirossi"><img src="https://github.com/spirossi.png" width="40px" /></a>
|
||||
<a href="https://github.com/teomarcdhio"><img src="https://github.com/teomarcdhio.png" width="40px" /></a>
|
||||
<a href="https://github.com/MarcMichalsky"><img src="https://github.com/MarcMichalsky.png" width="40px" /></a>
|
||||
<a href="https://github.com/LuckVintage"><img src="https://github.com/LuckVintage.png" width="40px" /></a>
|
||||
<a href="https://github.com/spartan"><img src="https://github.com/spartan.png" width="40px" /></a>
|
||||
<a href="https://github.com/alexandzors"><img src="https://github.com/alexandzors.png" width="40px" /></a>
|
||||
<a href="https://github.com/dkramer95"><img src="https://github.com/dkramer95.png" width="40px" /></a>
|
||||
<a href="https://github.com/YezGotIt"><img src="https://github.com/YezGotIt.png" width="40px" /></a>
|
||||
<a href="https://github.com/thomasskou"><img src="https://github.com/thomasskou.png" width="40px" /></a>
|
||||
<a href="https://github.com/surfernv"><img src="https://github.com/surfernv.png" width="40px" /></a>
|
||||
<a href="https://github.com/richardleach"><img src="https://github.com/richardleach.png" width="40px" /></a>
|
||||
<a href="https://github.com/bear"><img src="https://github.com/bear.png" width="40px" /></a>
|
||||
<a href="https://github.com/cminter"><img src="https://github.com/cminter.png" width="40px" /></a>
|
||||
<a href="https://github.com/bahur142"><img src="https://github.com/bahur142.png" width="40px" /></a>
|
||||
<a href="https://github.com/pgwiebes"><img src="https://github.com/pgwiebes.png" width="40px" /></a>
|
||||
<a href="https://github.com/ralhei"><img src="https://github.com/ralhei.png" width="40px" /></a>
|
||||
<a href="https://github.com/TechMDW"><img src="https://github.com/TechMDW.png" width="40px" /></a>
|
||||
<a href="https://github.com/ubipo"><img src="https://github.com/ubipo.png" width="40px" /></a>
|
||||
<a href="https://github.com/tka85"><img src="https://github.com/tka85.png" width="40px" /></a>
|
||||
<a href="https://github.com/beekeeb"><img src="https://github.com/beekeeb.png" width="40px" /></a>
|
||||
<a href="https://github.com/Emiliaaah"><img src="https://github.com/Emiliaaah.png" width="40px" /></a>
|
||||
<a href="https://github.com/zark0s"><img src="https://github.com/zark0s.png" width="40px" /></a>
|
||||
<a href="https://github.com/tomershvueli"><img src="https://github.com/tomershvueli.png" width="40px" /></a>
|
||||
<a href="https://github.com/CataIana"><img src="https://github.com/CataIana.png" width="40px" /></a>
|
||||
<a href="https://github.com/ajay-actuary"><img src="https://github.com/ajay-actuary.png" width="40px" /></a>
|
||||
<a href="https://github.com/mursec"><img src="https://github.com/mursec.png" width="40px" /></a>
|
||||
<a href="https://github.com/FrameXX"><img src="https://github.com/FrameXX.png" width="40px" /></a>
|
||||
<a href="https://github.com/vovayartsev"><img src="https://github.com/vovayartsev.png" width="40px" /></a>
|
||||
<a href="https://github.com/dwain-lab"><img src="https://github.com/dwain-lab.png" width="40px" /></a>
|
||||
<a href="https://github.com/brookmg"><img src="https://github.com/brookmg.png" width="40px" /></a>
|
||||
<a href="https://github.com/siebej"><img src="https://github.com/siebej.png" width="40px" /></a>
|
||||
<a href="https://github.com/rxsantos"><img src="https://github.com/rxsantos.png" width="40px" /></a>
|
||||
<a href="https://github.com/hermannx5"><img src="https://github.com/hermannx5.png" width="40px" /></a>
|
||||
<a href="https://github.com/rwxd"><img src="https://github.com/rwxd.png" width="40px" /></a>
|
||||
<a href="https://github.com/Integral-Tech"><img src="https://github.com/Integral-Tech.png" width="40px" /></a>
|
||||
<a href="https://github.com/TheTomik1"><img src="https://github.com/TheTomik1.png" width="40px" /></a>
|
||||
<a href="https://github.com/dav23r"><img src="https://github.com/dav23r.png" width="40px" /></a>
|
||||
<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 providing their awesome [IntelliJ IDEA](https://www.jetbrains.com/idea/) to me for free,
|
||||
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.**
|
||||
|
||||
@@ -144,7 +247,7 @@ _Please be sure to read the complete [Code of Conduct](CODE_OF_CONDUCT.md)._
|
||||
Made with ❤️ by [Philipp C. Heckel](https://heckel.io).
|
||||
The project is dual licensed under the [Apache License 2.0](LICENSE) and the [GPLv2 License](LICENSE.GPLv2).
|
||||
|
||||
Third party libraries and resources:
|
||||
Third-party libraries and resources:
|
||||
* [github.com/urfave/cli](https://github.com/urfave/cli) (MIT) is used to drive the CLI
|
||||
* [Mixkit sounds](https://mixkit.co/free-sound-effects/notification/) (Mixkit Free License) are used as notification sounds
|
||||
* [Sounds from notificationsounds.com](https://notificationsounds.com) (Creative Commons Attribution) are used as notification sounds
|
||||
@@ -164,3 +267,5 @@ Third party libraries and resources:
|
||||
* [Regex for auto-linking](https://github.com/bryanwoods/autolink-js) (MIT) is used to highlight links (the library is not used)
|
||||
* [Statically linking go-sqlite3](https://www.arp242.net/static-go.html)
|
||||
* [Linked tabs in mkdocs](https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/#linked-tabs)
|
||||
* [webpush-go](https://github.com/SherClockHolmes/webpush-go) (MIT) is used to send web push notifications
|
||||
* [Sprig](https://github.com/Masterminds/sprig) (MIT) is used to add template parsing functions
|
||||
|
||||
@@ -6,5 +6,7 @@ As of today, I only support the latest version of ntfy. Please make sure you sta
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report severe security issues privately via ntfy@heckel.io, [Discord](https://discord.gg/cT7ECsZj9w),
|
||||
or [Matrix](https://matrix.to/#/#ntfy:matrix.org) (my username is `binwiederhier`).
|
||||
Please report security vulnerabilities privately via email to [security@mail.ntfy.sh](mailto:security@mail.ntfy.sh).
|
||||
|
||||
You can also reach me on [Discord](https://discord.gg/cT7ECsZj9w) or [Matrix](https://matrix.to/#/#ntfy:matrix.org)
|
||||
(my username is `binwiederhier`).
|
||||
|
||||
BIN
assets/sponsors/magicbell.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
@@ -7,27 +7,29 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"heckel.io/ntfy/log"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Event type constants
|
||||
const (
|
||||
MessageEvent = "message"
|
||||
KeepaliveEvent = "keepalive"
|
||||
OpenEvent = "open"
|
||||
PollRequestEvent = "poll_request"
|
||||
// MessageEvent identifies a message event
|
||||
MessageEvent = "message"
|
||||
)
|
||||
|
||||
const (
|
||||
maxResponseBytes = 4096
|
||||
)
|
||||
|
||||
var (
|
||||
topicRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`) // Same as in server/server.go
|
||||
)
|
||||
|
||||
// Client is the ntfy client that can be used to publish and subscribe to ntfy topics
|
||||
type Client struct {
|
||||
Messages chan *Message
|
||||
@@ -96,8 +98,14 @@ func (c *Client) Publish(topic, message string, options ...PublishOption) (*Mess
|
||||
// To pass title, priority and tags, check out WithTitle, WithPriority, WithTagsList, WithDelay, WithNoCache,
|
||||
// WithNoFirebase, and the generic WithHeader.
|
||||
func (c *Client) PublishReader(topic string, body io.Reader, options ...PublishOption) (*Message, error) {
|
||||
topicURL := c.expandTopicURL(topic)
|
||||
req, _ := http.NewRequest("POST", topicURL, body)
|
||||
topicURL, err := c.expandTopicURL(topic)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequest("POST", topicURL, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, option := range options {
|
||||
if err := option(req); err != nil {
|
||||
return nil, err
|
||||
@@ -133,11 +141,14 @@ func (c *Client) PublishReader(topic string, body io.Reader, options ...PublishO
|
||||
// By default, all messages will be returned, but you can change this behavior using a SubscribeOption.
|
||||
// See WithSince, WithSinceAll, WithSinceUnixTime, WithScheduled, and the generic WithQueryParam.
|
||||
func (c *Client) Poll(topic string, options ...SubscribeOption) ([]*Message, error) {
|
||||
topicURL, err := c.expandTopicURL(topic)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx := context.Background()
|
||||
messages := make([]*Message, 0)
|
||||
msgChan := make(chan *Message)
|
||||
errChan := make(chan error)
|
||||
topicURL := c.expandTopicURL(topic)
|
||||
log.Debug("%s Polling from topic", util.ShortTopicURL(topicURL))
|
||||
options = append(options, WithPoll())
|
||||
go func() {
|
||||
@@ -166,15 +177,18 @@ func (c *Client) Poll(topic string, options ...SubscribeOption) ([]*Message, err
|
||||
// Example:
|
||||
//
|
||||
// c := client.New(client.NewConfig())
|
||||
// subscriptionID := c.Subscribe("mytopic")
|
||||
// subscriptionID, _ := c.Subscribe("mytopic")
|
||||
// for m := range c.Messages {
|
||||
// fmt.Printf("New message: %s", m.Message)
|
||||
// }
|
||||
func (c *Client) Subscribe(topic string, options ...SubscribeOption) string {
|
||||
func (c *Client) Subscribe(topic string, options ...SubscribeOption) (string, error) {
|
||||
topicURL, err := c.expandTopicURL(topic)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
subscriptionID := util.RandomString(10)
|
||||
topicURL := c.expandTopicURL(topic)
|
||||
log.Debug("%s Subscribing to topic", util.ShortTopicURL(topicURL))
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
c.subscriptions[subscriptionID] = &subscription{
|
||||
@@ -183,7 +197,7 @@ func (c *Client) Subscribe(topic string, options ...SubscribeOption) string {
|
||||
cancel: cancel,
|
||||
}
|
||||
go handleSubscribeConnLoop(ctx, c.Messages, topicURL, subscriptionID, options...)
|
||||
return subscriptionID
|
||||
return subscriptionID, nil
|
||||
}
|
||||
|
||||
// Unsubscribe unsubscribes from a topic that has been previously subscribed to using the unique
|
||||
@@ -199,31 +213,16 @@ func (c *Client) Unsubscribe(subscriptionID string) {
|
||||
sub.cancel()
|
||||
}
|
||||
|
||||
// UnsubscribeAll unsubscribes from a topic that has been previously subscribed with Subscribe.
|
||||
// If there are multiple subscriptions matching the topic, all of them are unsubscribed from.
|
||||
//
|
||||
// A topic can be either a full URL (e.g. https://myhost.lan/mytopic), a short URL which is then prepended https://
|
||||
// (e.g. myhost.lan -> https://myhost.lan), or a short name which is expanded using the default host in the
|
||||
// config (e.g. mytopic -> https://ntfy.sh/mytopic).
|
||||
func (c *Client) UnsubscribeAll(topic string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
topicURL := c.expandTopicURL(topic)
|
||||
for _, sub := range c.subscriptions {
|
||||
if sub.topicURL == topicURL {
|
||||
delete(c.subscriptions, sub.ID)
|
||||
sub.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) expandTopicURL(topic string) string {
|
||||
func (c *Client) expandTopicURL(topic string) (string, error) {
|
||||
if strings.HasPrefix(topic, "http://") || strings.HasPrefix(topic, "https://") {
|
||||
return topic
|
||||
return topic, nil
|
||||
} else if strings.Contains(topic, "/") {
|
||||
return fmt.Sprintf("https://%s", topic)
|
||||
return fmt.Sprintf("https://%s", topic), nil
|
||||
}
|
||||
return fmt.Sprintf("%s/%s", c.config.DefaultHost, topic)
|
||||
if !topicRegex.MatchString(topic) {
|
||||
return "", fmt.Errorf("invalid topic name: %s", topic)
|
||||
}
|
||||
return fmt.Sprintf("%s/%s", c.config.DefaultHost, topic), nil
|
||||
}
|
||||
|
||||
func handleSubscribeConnLoop(ctx context.Context, msgChan chan *Message, topicURL, subcriptionID string, options ...SubscribeOption) {
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
|
||||
# Default credentials will be used with "ntfy publish" and "ntfy subscribe" if no other credentials are provided.
|
||||
# You can set a default token to use or a default user:password combination, but not both. For an empty password,
|
||||
# use empty double-quotes ("")
|
||||
# use empty double-quotes ("").
|
||||
#
|
||||
# To override the default user:password combination or default token for a particular subscription (e.g., to send
|
||||
# no Authorization header), set the user:pass/token for the subscription to empty double-quotes ("").
|
||||
|
||||
# default-token:
|
||||
|
||||
@@ -18,7 +21,7 @@
|
||||
# default-command:
|
||||
|
||||
# Subscriptions to topics and their actions. This option is primarily used by the systemd service,
|
||||
# or if you cann "ntfy subscribe --from-config" directly.
|
||||
# or if you can "ntfy subscribe --from-config" directly.
|
||||
#
|
||||
# Example:
|
||||
# subscribe:
|
||||
|
||||
@@ -3,9 +3,9 @@ package client_test
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
"heckel.io/ntfy/client"
|
||||
"heckel.io/ntfy/log"
|
||||
"heckel.io/ntfy/test"
|
||||
"heckel.io/ntfy/v2/client"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/test"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -21,7 +21,7 @@ func TestClient_Publish_Subscribe(t *testing.T) {
|
||||
defer test.StopServer(t, s, port)
|
||||
c := client.New(newTestConfig(port))
|
||||
|
||||
subscriptionID := c.Subscribe("mytopic")
|
||||
subscriptionID, _ := c.Subscribe("mytopic")
|
||||
time.Sleep(time.Second)
|
||||
|
||||
msg, err := c.Publish("mytopic", "some message")
|
||||
|
||||
@@ -2,6 +2,7 @@ package client
|
||||
|
||||
import (
|
||||
"gopkg.in/yaml.v2"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"os"
|
||||
)
|
||||
|
||||
@@ -10,6 +11,9 @@ const (
|
||||
DefaultBaseURL = "https://ntfy.sh"
|
||||
)
|
||||
|
||||
// DefaultConfigFile is the default path to the client config file (set in config_*.go)
|
||||
var DefaultConfigFile string
|
||||
|
||||
// Config is the config struct for a Client
|
||||
type Config struct {
|
||||
DefaultHost string `yaml:"default-host"`
|
||||
@@ -23,9 +27,9 @@ type Config struct {
|
||||
// Subscribe is the struct for a Subscription within Config
|
||||
type Subscribe struct {
|
||||
Topic string `yaml:"topic"`
|
||||
User string `yaml:"user"`
|
||||
User *string `yaml:"user"`
|
||||
Password *string `yaml:"password"`
|
||||
Token string `yaml:"token"`
|
||||
Token *string `yaml:"token"`
|
||||
Command string `yaml:"command"`
|
||||
If map[string]string `yaml:"if"`
|
||||
}
|
||||
@@ -44,6 +48,7 @@ func NewConfig() *Config {
|
||||
|
||||
// LoadConfig loads the Client config from a yaml file
|
||||
func LoadConfig(filename string) (*Config, error) {
|
||||
log.Debug("Loading client config from %s", filename)
|
||||
b, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
18
client/config_darwin.go
Normal file
@@ -0,0 +1,18 @@
|
||||
//go:build darwin
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func init() {
|
||||
u, err := user.Current()
|
||||
if err == nil && u.Uid == "0" {
|
||||
DefaultConfigFile = "/etc/ntfy/client.yml"
|
||||
} else if configDir, err := os.UserConfigDir(); err == nil {
|
||||
DefaultConfigFile = filepath.Join(configDir, "ntfy", "client.yml")
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package client_test
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"heckel.io/ntfy/client"
|
||||
"heckel.io/ntfy/v2/client"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -37,7 +37,7 @@ subscribe:
|
||||
require.Equal(t, 4, len(conf.Subscribe))
|
||||
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
|
||||
require.Equal(t, "", conf.Subscribe[0].Command)
|
||||
require.Equal(t, "phil", conf.Subscribe[0].User)
|
||||
require.Equal(t, "phil", *conf.Subscribe[0].User)
|
||||
require.Equal(t, "mypass", *conf.Subscribe[0].Password)
|
||||
require.Equal(t, "echo-this", conf.Subscribe[1].Topic)
|
||||
require.Equal(t, `echo "Message received: $message"`, conf.Subscribe[1].Command)
|
||||
@@ -67,7 +67,7 @@ subscribe:
|
||||
require.Equal(t, 1, len(conf.Subscribe))
|
||||
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
|
||||
require.Equal(t, "", conf.Subscribe[0].Command)
|
||||
require.Equal(t, "phil", conf.Subscribe[0].User)
|
||||
require.Equal(t, "phil", *conf.Subscribe[0].User)
|
||||
require.Equal(t, "", *conf.Subscribe[0].Password)
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ subscribe:
|
||||
require.Equal(t, 1, len(conf.Subscribe))
|
||||
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
|
||||
require.Equal(t, "", conf.Subscribe[0].Command)
|
||||
require.Equal(t, "phil", conf.Subscribe[0].User)
|
||||
require.Equal(t, "phil", *conf.Subscribe[0].User)
|
||||
require.Nil(t, conf.Subscribe[0].Password)
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ subscribe:
|
||||
require.Equal(t, 1, len(conf.Subscribe))
|
||||
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
|
||||
require.Equal(t, "", conf.Subscribe[0].Command)
|
||||
require.Equal(t, "phil", conf.Subscribe[0].User)
|
||||
require.Equal(t, "phil", *conf.Subscribe[0].User)
|
||||
require.Nil(t, conf.Subscribe[0].Password)
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ subscribe:
|
||||
require.Equal(t, "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", conf.DefaultToken)
|
||||
require.Equal(t, 1, len(conf.Subscribe))
|
||||
require.Equal(t, "mytopic", conf.Subscribe[0].Topic)
|
||||
require.Equal(t, "", conf.Subscribe[0].User)
|
||||
require.Nil(t, conf.Subscribe[0].User)
|
||||
require.Nil(t, conf.Subscribe[0].Password)
|
||||
require.Equal(t, "", conf.Subscribe[0].Token)
|
||||
require.Nil(t, conf.Subscribe[0].Token)
|
||||
}
|
||||
|
||||
18
client/config_unix.go
Normal file
@@ -0,0 +1,18 @@
|
||||
//go:build linux || dragonfly || freebsd || netbsd || openbsd
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func init() {
|
||||
u, err := user.Current()
|
||||
if err == nil && u.Uid == "0" {
|
||||
DefaultConfigFile = "/etc/ntfy/client.yml"
|
||||
} else if configDir, err := os.UserConfigDir(); err == nil {
|
||||
DefaultConfigFile = filepath.Join(configDir, "ntfy", "client.yml")
|
||||
}
|
||||
}
|
||||
14
client/config_windows.go
Normal file
@@ -0,0 +1,14 @@
|
||||
//go:build windows
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func init() {
|
||||
if configDir, err := os.UserConfigDir(); err == nil {
|
||||
DefaultConfigFile = filepath.Join(configDir, "ntfy", "client.yml")
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -72,11 +72,27 @@ func WithAttach(attach string) PublishOption {
|
||||
return WithHeader("X-Attach", attach)
|
||||
}
|
||||
|
||||
// WithMarkdown instructs the server to interpret the message body as Markdown
|
||||
func WithMarkdown() PublishOption {
|
||||
return WithHeader("X-Markdown", "yes")
|
||||
}
|
||||
|
||||
// WithTemplate instructs the server to use a specific template for the message. If templateName is is "yes" or "1",
|
||||
// the server will interpret the message and title as a template.
|
||||
func WithTemplate(templateName string) PublishOption {
|
||||
return WithHeader("X-Template", templateName)
|
||||
}
|
||||
|
||||
// WithFilename sets a filename for the attachment, and/or forces the HTTP body to interpreted as an attachment
|
||||
func WithFilename(filename string) PublishOption {
|
||||
return WithHeader("X-Filename", filename)
|
||||
}
|
||||
|
||||
// WithSequenceID sets a sequence ID for the message, allowing updates to existing notifications
|
||||
func WithSequenceID(sequenceID string) PublishOption {
|
||||
return WithHeader("X-Sequence-ID", sequenceID)
|
||||
}
|
||||
|
||||
// WithEmail instructs the server to also send the message to the given e-mail address
|
||||
func WithEmail(email string) PublishOption {
|
||||
return WithHeader("X-Email", email)
|
||||
@@ -92,6 +108,11 @@ func WithBearerAuth(token string) PublishOption {
|
||||
return WithHeader("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
}
|
||||
|
||||
// WithEmptyAuth clears the Authorization header
|
||||
func WithEmptyAuth() PublishOption {
|
||||
return RemoveHeader("Authorization")
|
||||
}
|
||||
|
||||
// WithNoCache instructs the server not to cache the message server-side
|
||||
func WithNoCache() PublishOption {
|
||||
return WithHeader("X-Cache", "no")
|
||||
@@ -182,3 +203,13 @@ func WithQueryParam(param, value string) RequestOption {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveHeader is a generic option to remove a header from a request
|
||||
func RemoveHeader(header string) RequestOption {
|
||||
return func(r *http.Request) error {
|
||||
if header != "" {
|
||||
delete(r.Header, header)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
10
client/user/ntfy-client.service
Normal 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
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/urfave/cli/v2"
|
||||
"heckel.io/ntfy/user"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -105,8 +105,10 @@ func changeAccess(c *cli.Context, manager *user.Manager, username string, topic
|
||||
return err
|
||||
}
|
||||
u, err := manager.User(username)
|
||||
if err == user.ErrUserNotFound {
|
||||
if errors.Is(err, user.ErrUserNotFound) {
|
||||
return fmt.Errorf("user %s does not exist", username)
|
||||
} else if err != nil {
|
||||
return err
|
||||
} else if u.Role == user.RoleAdmin {
|
||||
return fmt.Errorf("user %s is an admin user, access control entries have no effect", username)
|
||||
}
|
||||
@@ -114,13 +116,13 @@ func changeAccess(c *cli.Context, manager *user.Manager, username string, topic
|
||||
return err
|
||||
}
|
||||
if permission.IsReadWrite() {
|
||||
fmt.Fprintf(c.App.ErrWriter, "granted read-write access to topic %s\n\n", topic)
|
||||
fmt.Fprintf(c.App.Writer, "granted read-write access to topic %s\n\n", topic)
|
||||
} else if permission.IsRead() {
|
||||
fmt.Fprintf(c.App.ErrWriter, "granted read-only access to topic %s\n\n", topic)
|
||||
fmt.Fprintf(c.App.Writer, "granted read-only access to topic %s\n\n", topic)
|
||||
} else if permission.IsWrite() {
|
||||
fmt.Fprintf(c.App.ErrWriter, "granted write-only access to topic %s\n\n", topic)
|
||||
fmt.Fprintf(c.App.Writer, "granted write-only access to topic %s\n\n", topic)
|
||||
} else {
|
||||
fmt.Fprintf(c.App.ErrWriter, "revoked all access to topic %s\n\n", topic)
|
||||
fmt.Fprintf(c.App.Writer, "revoked all access to topic %s\n\n", topic)
|
||||
}
|
||||
return showUserAccess(c, manager, username)
|
||||
}
|
||||
@@ -138,7 +140,7 @@ func resetAllAccess(c *cli.Context, manager *user.Manager) error {
|
||||
if err := manager.ResetAccess("", ""); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(c.App.ErrWriter, "reset access for all users")
|
||||
fmt.Fprintln(c.App.Writer, "reset access for all users")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -146,7 +148,7 @@ func resetUserAccess(c *cli.Context, manager *user.Manager, username string) err
|
||||
if err := manager.ResetAccess(username, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(c.App.ErrWriter, "reset access for user %s\n\n", username)
|
||||
fmt.Fprintf(c.App.Writer, "reset access for user %s\n\n", username)
|
||||
return showUserAccess(c, manager, username)
|
||||
}
|
||||
|
||||
@@ -154,7 +156,7 @@ func resetUserTopicAccess(c *cli.Context, manager *user.Manager, username string
|
||||
if err := manager.ResetAccess(username, topic); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(c.App.ErrWriter, "reset access for user %s and topic %s\n\n", username, topic)
|
||||
fmt.Fprintf(c.App.Writer, "reset access for user %s and topic %s\n\n", username, topic)
|
||||
return showUserAccess(c, manager, username)
|
||||
}
|
||||
|
||||
@@ -175,7 +177,7 @@ func showAllAccess(c *cli.Context, manager *user.Manager) error {
|
||||
|
||||
func showUserAccess(c *cli.Context, manager *user.Manager, username string) error {
|
||||
users, err := manager.User(username)
|
||||
if err == user.ErrUserNotFound {
|
||||
if errors.Is(err, user.ErrUserNotFound) {
|
||||
return fmt.Errorf("user %s does not exist", username)
|
||||
} else if err != nil {
|
||||
return err
|
||||
@@ -193,34 +195,42 @@ func showUsers(c *cli.Context, manager *user.Manager, users []*user.User) error
|
||||
if u.Tier != nil {
|
||||
tier = u.Tier.Name
|
||||
}
|
||||
fmt.Fprintf(c.App.ErrWriter, "user %s (role: %s, tier: %s)\n", u.Name, u.Role, tier)
|
||||
provisioned := ""
|
||||
if u.Provisioned {
|
||||
provisioned = ", server config"
|
||||
}
|
||||
fmt.Fprintf(c.App.Writer, "user %s (role: %s, tier: %s%s)\n", u.Name, u.Role, tier, provisioned)
|
||||
if u.Role == user.RoleAdmin {
|
||||
fmt.Fprintf(c.App.ErrWriter, "- read-write access to all topics (admin role)\n")
|
||||
fmt.Fprintf(c.App.Writer, "- read-write access to all topics (admin role)\n")
|
||||
} else if len(grants) > 0 {
|
||||
for _, grant := range grants {
|
||||
if grant.Allow.IsReadWrite() {
|
||||
fmt.Fprintf(c.App.ErrWriter, "- read-write access to topic %s\n", grant.TopicPattern)
|
||||
} else if grant.Allow.IsRead() {
|
||||
fmt.Fprintf(c.App.ErrWriter, "- read-only access to topic %s\n", grant.TopicPattern)
|
||||
} else if grant.Allow.IsWrite() {
|
||||
fmt.Fprintf(c.App.ErrWriter, "- write-only access to topic %s\n", grant.TopicPattern)
|
||||
grantProvisioned := ""
|
||||
if grant.Provisioned {
|
||||
grantProvisioned = " (server config)"
|
||||
}
|
||||
if grant.Permission.IsReadWrite() {
|
||||
fmt.Fprintf(c.App.Writer, "- read-write access to topic %s%s\n", grant.TopicPattern, grantProvisioned)
|
||||
} else if grant.Permission.IsRead() {
|
||||
fmt.Fprintf(c.App.Writer, "- read-only access to topic %s%s\n", grant.TopicPattern, grantProvisioned)
|
||||
} else if grant.Permission.IsWrite() {
|
||||
fmt.Fprintf(c.App.Writer, "- write-only access to topic %s%s\n", grant.TopicPattern, grantProvisioned)
|
||||
} else {
|
||||
fmt.Fprintf(c.App.ErrWriter, "- no access to topic %s\n", grant.TopicPattern)
|
||||
fmt.Fprintf(c.App.Writer, "- no access to topic %s%s\n", grant.TopicPattern, grantProvisioned)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(c.App.ErrWriter, "- no topic-specific permissions\n")
|
||||
fmt.Fprintf(c.App.Writer, "- no topic-specific permissions\n")
|
||||
}
|
||||
if u.Name == user.Everyone {
|
||||
access := manager.DefaultAccess()
|
||||
if access.IsReadWrite() {
|
||||
fmt.Fprintln(c.App.ErrWriter, "- read-write access to all (other) topics (server config)")
|
||||
fmt.Fprintln(c.App.Writer, "- read-write access to all (other) topics (server config)")
|
||||
} else if access.IsRead() {
|
||||
fmt.Fprintln(c.App.ErrWriter, "- read-only access to all (other) topics (server config)")
|
||||
fmt.Fprintln(c.App.Writer, "- read-only access to all (other) topics (server config)")
|
||||
} else if access.IsWrite() {
|
||||
fmt.Fprintln(c.App.ErrWriter, "- write-only access to all (other) topics (server config)")
|
||||
fmt.Fprintln(c.App.Writer, "- write-only access to all (other) topics (server config)")
|
||||
} else {
|
||||
fmt.Fprintln(c.App.ErrWriter, "- no access to any (other) topics (server config)")
|
||||
fmt.Fprintln(c.App.Writer, "- no access to any (other) topics (server config)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v2"
|
||||
"heckel.io/ntfy/server"
|
||||
"heckel.io/ntfy/test"
|
||||
"heckel.io/ntfy/v2/server"
|
||||
"heckel.io/ntfy/v2/test"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -13,9 +13,9 @@ func TestCLI_Access_Show(t *testing.T) {
|
||||
s, conf, port := newTestServerWithAuth(t)
|
||||
defer test.StopServer(t, s, port)
|
||||
|
||||
app, _, _, stderr := newTestApp()
|
||||
app, _, stdout, _ := newTestApp()
|
||||
require.Nil(t, runAccessCommand(app, conf))
|
||||
require.Contains(t, stderr.String(), "user * (role: anonymous, tier: none)\n- no topic-specific permissions\n- no access to any (other) topics (server config)")
|
||||
require.Contains(t, stdout.String(), "user * (role: anonymous, tier: none)\n- no topic-specific permissions\n- no access to any (other) topics (server config)")
|
||||
}
|
||||
|
||||
func TestCLI_Access_Grant_And_Publish(t *testing.T) {
|
||||
@@ -30,7 +30,7 @@ func TestCLI_Access_Grant_And_Publish(t *testing.T) {
|
||||
require.Nil(t, runAccessCommand(app, conf, "ben", "sometopic", "read"))
|
||||
require.Nil(t, runAccessCommand(app, conf, "everyone", "announcements", "read"))
|
||||
|
||||
app, _, _, stderr := newTestApp()
|
||||
app, _, stdout, _ := newTestApp()
|
||||
require.Nil(t, runAccessCommand(app, conf))
|
||||
expected := `user phil (role: admin, tier: none)
|
||||
- read-write access to all topics (admin role)
|
||||
@@ -41,7 +41,7 @@ user * (role: anonymous, tier: none)
|
||||
- read-only access to topic announcements
|
||||
- no access to any (other) topics (server config)
|
||||
`
|
||||
require.Equal(t, expected, stderr.String())
|
||||
require.Equal(t, expected, stdout.String())
|
||||
|
||||
// See if access permissions match
|
||||
app, _, _, _ = newTestApp()
|
||||
|
||||
13
cmd/app.go
@@ -3,11 +3,12 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
"heckel.io/ntfy/log"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -15,6 +16,12 @@ const (
|
||||
categoryServer = "Server commands"
|
||||
)
|
||||
|
||||
// Build metadata keys for app.Metadata
|
||||
const (
|
||||
MetadataKeyCommit = "commit"
|
||||
MetadataKeyDate = "date"
|
||||
)
|
||||
|
||||
var commands = make([]*cli.Command, 0)
|
||||
|
||||
var flagsDefault = []cli.Flag{
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"github.com/urfave/cli/v2"
|
||||
"heckel.io/ntfy/client"
|
||||
"heckel.io/ntfy/log"
|
||||
"heckel.io/ntfy/v2/client"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
"gopkg.in/yaml.v2"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"os"
|
||||
)
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/urfave/cli/v2"
|
||||
"heckel.io/ntfy/client"
|
||||
"heckel.io/ntfy/log"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/client"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -31,7 +31,10 @@ var flagsPublish = append(
|
||||
&cli.StringFlag{Name: "icon", Aliases: []string{"i"}, EnvVars: []string{"NTFY_ICON"}, Usage: "URL to use as notification icon"},
|
||||
&cli.StringFlag{Name: "actions", Aliases: []string{"A"}, EnvVars: []string{"NTFY_ACTIONS"}, Usage: "actions JSON array or simple definition"},
|
||||
&cli.StringFlag{Name: "attach", Aliases: []string{"a"}, EnvVars: []string{"NTFY_ATTACH"}, Usage: "URL to send as an external attachment"},
|
||||
&cli.BoolFlag{Name: "markdown", Aliases: []string{"md"}, EnvVars: []string{"NTFY_MARKDOWN"}, Usage: "Message is formatted as Markdown"},
|
||||
&cli.StringFlag{Name: "template", Aliases: []string{"tpl"}, EnvVars: []string{"NTFY_TEMPLATE"}, Usage: "use templates to transform JSON message body"},
|
||||
&cli.StringFlag{Name: "filename", Aliases: []string{"name", "n"}, EnvVars: []string{"NTFY_FILENAME"}, Usage: "filename for the attachment"},
|
||||
&cli.StringFlag{Name: "sequence-id", Aliases: []string{"sequence_id", "sid", "S"}, EnvVars: []string{"NTFY_SEQUENCE_ID"}, Usage: "sequence ID for updating notifications"},
|
||||
&cli.StringFlag{Name: "file", Aliases: []string{"f"}, EnvVars: []string{"NTFY_FILE"}, Usage: "file to upload as an attachment"},
|
||||
&cli.StringFlag{Name: "email", Aliases: []string{"mail", "e"}, EnvVars: []string{"NTFY_EMAIL"}, Usage: "also send to e-mail address"},
|
||||
&cli.StringFlag{Name: "user", Aliases: []string{"u"}, EnvVars: []string{"NTFY_USER"}, Usage: "username[:password] used to auth against the server"},
|
||||
@@ -68,6 +71,8 @@ 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
|
||||
ntfy pub -S my-id mytopic 'Update me' # Send with sequence ID for updates
|
||||
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
|
||||
@@ -95,7 +100,10 @@ func execPublish(c *cli.Context) error {
|
||||
icon := c.String("icon")
|
||||
actions := c.String("actions")
|
||||
attach := c.String("attach")
|
||||
markdown := c.Bool("markdown")
|
||||
template := c.String("template")
|
||||
filename := c.String("filename")
|
||||
sequenceID := c.String("sequence-id")
|
||||
file := c.String("file")
|
||||
email := c.String("email")
|
||||
user := c.String("user")
|
||||
@@ -140,9 +148,18 @@ func execPublish(c *cli.Context) error {
|
||||
if attach != "" {
|
||||
options = append(options, client.WithAttach(attach))
|
||||
}
|
||||
if markdown {
|
||||
options = append(options, client.WithMarkdown())
|
||||
}
|
||||
if template != "" {
|
||||
options = append(options, client.WithTemplate(template))
|
||||
}
|
||||
if filename != "" {
|
||||
options = append(options, client.WithFilename(filename))
|
||||
}
|
||||
if sequenceID != "" {
|
||||
options = append(options, client.WithSequenceID(sequenceID))
|
||||
}
|
||||
if email != "" {
|
||||
options = append(options, client.WithEmail(email))
|
||||
}
|
||||
@@ -249,6 +266,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
|
||||
}
|
||||
|
||||
@@ -307,3 +333,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
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ package cmd
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
"heckel.io/ntfy/test"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/test"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build darwin || linux || dragonfly || freebsd || netbsd || openbsd
|
||||
// +build darwin linux dragonfly freebsd netbsd openbsd
|
||||
|
||||
package cmd
|
||||
|
||||
|
||||
512
cmd/serve.go
@@ -5,37 +5,32 @@ package cmd
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/stripe/stripe-go/v74"
|
||||
"heckel.io/ntfy/user"
|
||||
"io/fs"
|
||||
"math"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/signal"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"heckel.io/ntfy/log"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
"heckel.io/ntfy/server"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/payments"
|
||||
"heckel.io/ntfy/v2/server"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commands = append(commands, cmdServe)
|
||||
}
|
||||
|
||||
const (
|
||||
defaultServerConfigFile = "/etc/ntfy/server.yml"
|
||||
)
|
||||
|
||||
var flagsServe = append(
|
||||
append([]cli.Flag{}, flagsDefault...),
|
||||
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: defaultServerConfigFile, DefaultText: defaultServerConfigFile, Usage: "config file"},
|
||||
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: server.DefaultConfigFile, Usage: "config file"},
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "base-url", Aliases: []string{"base_url", "B"}, EnvVars: []string{"NTFY_BASE_URL"}, Usage: "externally visible base URL for this host (e.g. https://ntfy.sh)"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-http", Aliases: []string{"listen_http", "l"}, EnvVars: []string{"NTFY_LISTEN_HTTP"}, Value: server.DefaultListenHTTP, Usage: "ip:port used as HTTP listen address"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-https", Aliases: []string{"listen_https", "L"}, EnvVars: []string{"NTFY_LISTEN_HTTPS"}, Usage: "ip:port used as HTTPS listen address"}),
|
||||
@@ -45,25 +40,31 @@ var flagsServe = append(
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "cert-file", Aliases: []string{"cert_file", "E"}, EnvVars: []string{"NTFY_CERT_FILE"}, Usage: "certificate file, if listen-https is set"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "firebase-key-file", Aliases: []string{"firebase_key_file", "F"}, EnvVars: []string{"NTFY_FIREBASE_KEY_FILE"}, Usage: "Firebase credentials file; if set additionally publish to FCM topic"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-file", Aliases: []string{"cache_file", "C"}, EnvVars: []string{"NTFY_CACHE_FILE"}, Usage: "cache file used for message caching"}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "cache-duration", Aliases: []string{"cache_duration", "b"}, EnvVars: []string{"NTFY_CACHE_DURATION"}, Value: server.DefaultCacheDuration, Usage: "buffer messages for this time to allow `since` requests"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-duration", Aliases: []string{"cache_duration", "b"}, EnvVars: []string{"NTFY_CACHE_DURATION"}, Value: util.FormatDuration(server.DefaultCacheDuration), Usage: "buffer messages for this time to allow `since` requests"}),
|
||||
altsrc.NewIntFlag(&cli.IntFlag{Name: "cache-batch-size", Aliases: []string{"cache_batch_size"}, EnvVars: []string{"NTFY_BATCH_SIZE"}, Usage: "max size of messages to batch together when writing to message cache (if zero, writes are synchronous)"}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "cache-batch-timeout", Aliases: []string{"cache_batch_timeout"}, EnvVars: []string{"NTFY_CACHE_BATCH_TIMEOUT"}, Usage: "timeout for batched async writes to the message cache (if zero, writes are synchronous)"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-batch-timeout", Aliases: []string{"cache_batch_timeout"}, EnvVars: []string{"NTFY_CACHE_BATCH_TIMEOUT"}, Value: util.FormatDuration(server.DefaultCacheBatchTimeout), Usage: "timeout for batched async writes to the message cache (if zero, writes are synchronous)"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-startup-queries", Aliases: []string{"cache_startup_queries"}, EnvVars: []string{"NTFY_CACHE_STARTUP_QUERIES"}, Usage: "queries run when the cache database is initialized"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"auth_file", "H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-startup-queries", Aliases: []string{"auth_startup_queries"}, EnvVars: []string{"NTFY_AUTH_STARTUP_QUERIES"}, Usage: "queries run when the auth database is initialized"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"auth_default_access", "p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}),
|
||||
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "auth-users", Aliases: []string{"auth_users"}, EnvVars: []string{"NTFY_AUTH_USERS"}, Usage: "pre-provisioned declarative users"}),
|
||||
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "auth-access", Aliases: []string{"auth_access"}, EnvVars: []string{"NTFY_AUTH_ACCESS"}, Usage: "pre-provisioned declarative access control entries"}),
|
||||
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "auth-tokens", Aliases: []string{"auth_tokens"}, EnvVars: []string{"NTFY_AUTH_TOKENS"}, Usage: "pre-provisioned declarative access tokens"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-cache-dir", Aliases: []string{"attachment_cache_dir"}, EnvVars: []string{"NTFY_ATTACHMENT_CACHE_DIR"}, Usage: "cache directory for attached files"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-total-size-limit", Aliases: []string{"attachment_total_size_limit", "A"}, EnvVars: []string{"NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT"}, DefaultText: "5G", Usage: "limit of the on-disk attachment cache"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-file-size-limit", Aliases: []string{"attachment_file_size_limit", "Y"}, EnvVars: []string{"NTFY_ATTACHMENT_FILE_SIZE_LIMIT"}, DefaultText: "15M", Usage: "per-file attachment size limit (e.g. 300k, 2M, 100M)"}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "attachment-expiry-duration", Aliases: []string{"attachment_expiry_duration", "X"}, EnvVars: []string{"NTFY_ATTACHMENT_EXPIRY_DURATION"}, Value: server.DefaultAttachmentExpiryDuration, DefaultText: "3h", Usage: "duration after which uploaded attachments will be deleted (e.g. 3h, 20h)"}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "keepalive-interval", Aliases: []string{"keepalive_interval", "k"}, EnvVars: []string{"NTFY_KEEPALIVE_INTERVAL"}, Value: server.DefaultKeepaliveInterval, Usage: "interval of keepalive messages"}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "manager-interval", Aliases: []string{"manager_interval", "m"}, EnvVars: []string{"NTFY_MANAGER_INTERVAL"}, Value: server.DefaultManagerInterval, Usage: "interval of for message pruning and stats printing"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-total-size-limit", Aliases: []string{"attachment_total_size_limit", "A"}, EnvVars: []string{"NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentTotalSizeLimit), Usage: "limit of the on-disk attachment cache"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-file-size-limit", Aliases: []string{"attachment_file_size_limit", "Y"}, EnvVars: []string{"NTFY_ATTACHMENT_FILE_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentFileSizeLimit), Usage: "per-file attachment size limit (e.g. 300k, 2M, 100M)"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-expiry-duration", Aliases: []string{"attachment_expiry_duration", "X"}, EnvVars: []string{"NTFY_ATTACHMENT_EXPIRY_DURATION"}, Value: util.FormatDuration(server.DefaultAttachmentExpiryDuration), Usage: "duration after which uploaded attachments will be deleted (e.g. 3h, 20h)"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "template-dir", Aliases: []string{"template_dir"}, EnvVars: []string{"NTFY_TEMPLATE_DIR"}, Value: server.DefaultTemplateDir, Usage: "directory to load named message templates from"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "keepalive-interval", Aliases: []string{"keepalive_interval", "k"}, EnvVars: []string{"NTFY_KEEPALIVE_INTERVAL"}, Value: util.FormatDuration(server.DefaultKeepaliveInterval), Usage: "interval of keepalive messages"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "manager-interval", Aliases: []string{"manager_interval", "m"}, EnvVars: []string{"NTFY_MANAGER_INTERVAL"}, Value: util.FormatDuration(server.DefaultManagerInterval), Usage: "interval of for message pruning and stats printing"}),
|
||||
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "disallowed-topics", Aliases: []string{"disallowed_topics"}, EnvVars: []string{"NTFY_DISALLOWED_TOPICS"}, Usage: "topics that are not allowed to be used"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-root", Aliases: []string{"web_root"}, EnvVars: []string{"NTFY_WEB_ROOT"}, Value: "app", Usage: "sets web root to landing page (home), web app (app) or disabled (disable)"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-root", Aliases: []string{"web_root"}, EnvVars: []string{"NTFY_WEB_ROOT"}, Value: "/", Usage: "sets root of the web app (e.g. /, or /app), or disables it (disable)"}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-signup", Aliases: []string{"enable_signup"}, EnvVars: []string{"NTFY_ENABLE_SIGNUP"}, Value: false, Usage: "allows users to sign up via the web app, or API"}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-login", Aliases: []string{"enable_login"}, EnvVars: []string{"NTFY_ENABLE_LOGIN"}, Value: false, Usage: "allows users to log in via the web app, or API"}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-reservations", Aliases: []string{"enable_reservations"}, EnvVars: []string{"NTFY_ENABLE_RESERVATIONS"}, Value: false, Usage: "allows users to reserve topics (if their tier allows it)"}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "require-login", Aliases: []string{"require_login"}, EnvVars: []string{"NTFY_REQUIRE_LOGIN"}, Value: false, Usage: "all actions via the web app requires a login"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "upstream-base-url", Aliases: []string{"upstream_base_url"}, EnvVars: []string{"NTFY_UPSTREAM_BASE_URL"}, Value: "", Usage: "forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "upstream-access-token", Aliases: []string{"upstream_access_token"}, EnvVars: []string{"NTFY_UPSTREAM_ACCESS_TOKEN"}, Value: "", Usage: "access token to use for the upstream server; needed only if upstream rate limits are exceeded or upstream server requires auth"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-addr", Aliases: []string{"smtp_sender_addr"}, EnvVars: []string{"NTFY_SMTP_SENDER_ADDR"}, Usage: "SMTP server address (host:port) for outgoing emails"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-user", Aliases: []string{"smtp_sender_user"}, EnvVars: []string{"NTFY_SMTP_SENDER_USER"}, Usage: "SMTP user (if e-mail sending is enabled)"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-pass", Aliases: []string{"smtp_sender_pass"}, EnvVars: []string{"NTFY_SMTP_SENDER_PASS"}, Usage: "SMTP password (if e-mail sending is enabled)"}),
|
||||
@@ -71,23 +72,42 @@ var flagsServe = append(
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-listen", Aliases: []string{"smtp_server_listen"}, EnvVars: []string{"NTFY_SMTP_SERVER_LISTEN"}, Usage: "SMTP server address (ip:port) for incoming emails, e.g. :25"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-domain", Aliases: []string{"smtp_server_domain"}, EnvVars: []string{"NTFY_SMTP_SERVER_DOMAIN"}, Usage: "SMTP domain for incoming e-mail, e.g. ntfy.sh"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-addr-prefix", Aliases: []string{"smtp_server_addr_prefix"}, EnvVars: []string{"NTFY_SMTP_SERVER_ADDR_PREFIX"}, Usage: "SMTP email address prefix for topics to prevent spam (e.g. 'ntfy-')"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-account", Aliases: []string{"twilio_account"}, EnvVars: []string{"NTFY_TWILIO_ACCOUNT"}, Usage: "Twilio account SID, used for phone calls, e.g. AC123..."}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-auth-token", Aliases: []string{"twilio_auth_token"}, EnvVars: []string{"NTFY_TWILIO_AUTH_TOKEN"}, Usage: "Twilio auth token"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-phone-number", Aliases: []string{"twilio_phone_number"}, EnvVars: []string{"NTFY_TWILIO_PHONE_NUMBER"}, Usage: "Twilio number to use for outgoing calls"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-verify-service", Aliases: []string{"twilio_verify_service"}, EnvVars: []string{"NTFY_TWILIO_VERIFY_SERVICE"}, Usage: "Twilio Verify service ID, used for phone number verification"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-call-format", Aliases: []string{"twilio_call_format"}, EnvVars: []string{"NTFY_TWILIO_CALL_FORMAT"}, Usage: "Twilio/TwiML format string for phone calls"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "message-size-limit", Aliases: []string{"message_size_limit"}, EnvVars: []string{"NTFY_MESSAGE_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultMessageSizeLimit), Usage: "size limit for the message (see docs for limitations)"}),
|
||||
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.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: "100M", Usage: "total storage limit used for attachments 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"}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "visitor-request-limit-replenish", Aliases: []string{"visitor_request_limit_replenish"}, EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_REPLENISH"}, Value: server.DefaultVisitorRequestLimitReplenish, Usage: "interval at which burst limit is replenished (one per x)"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-request-limit-replenish", Aliases: []string{"visitor_request_limit_replenish"}, EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_REPLENISH"}, Value: util.FormatDuration(server.DefaultVisitorRequestLimitReplenish), Usage: "interval at which burst limit is replenished (one per x)"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-request-limit-exempt-hosts", Aliases: []string{"visitor_request_limit_exempt_hosts"}, EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS"}, Value: "", Usage: "hostnames and/or IP addresses of hosts that will be exempt from the visitor request limit"}),
|
||||
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-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.NewDurationFlag(&cli.DurationFlag{Name: "visitor-email-limit-replenish", Aliases: []string{"visitor_email_limit_replenish"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_REPLENISH"}, Value: server.DefaultVisitorEmailLimitReplenish, Usage: "interval at which burst limit is replenished (one per x)"}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "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.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.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)"}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-metrics", Aliases: []string{"enable_metrics"}, EnvVars: []string{"NTFY_ENABLE_METRICS"}, Value: false, Usage: "if set, Prometheus metrics are exposed via the /metrics endpoint"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "metrics-listen-http", Aliases: []string{"metrics_listen_http"}, EnvVars: []string{"NTFY_METRICS_LISTEN_HTTP"}, Usage: "ip:port used to expose the metrics endpoint (implicitly enables metrics)"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "profile-listen-http", Aliases: []string{"profile_listen_http"}, EnvVars: []string{"NTFY_PROFILE_LISTEN_HTTP"}, Usage: "ip:port used to expose the profiling endpoints (implicitly enables profiling)"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-public-key", Aliases: []string{"web_push_public_key"}, EnvVars: []string{"NTFY_WEB_PUSH_PUBLIC_KEY"}, Usage: "public key used for web push notifications"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-private-key", Aliases: []string{"web_push_private_key"}, EnvVars: []string{"NTFY_WEB_PUSH_PRIVATE_KEY"}, Usage: "private key used for web push notifications"}),
|
||||
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{
|
||||
@@ -115,7 +135,7 @@ func execServe(c *cli.Context) error {
|
||||
|
||||
// Read all the options
|
||||
config := c.String("config")
|
||||
baseURL := c.String("base-url")
|
||||
baseURL := strings.TrimSuffix(c.String("base-url"), "/")
|
||||
listenHTTP := c.String("listen-http")
|
||||
listenHTTPS := c.String("listen-https")
|
||||
listenUnix := c.String("listen-unix")
|
||||
@@ -123,26 +143,39 @@ func execServe(c *cli.Context) error {
|
||||
keyFile := c.String("key-file")
|
||||
certFile := c.String("cert-file")
|
||||
firebaseKeyFile := c.String("firebase-key-file")
|
||||
webPushPrivateKey := c.String("web-push-private-key")
|
||||
webPushPublicKey := c.String("web-push-public-key")
|
||||
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")
|
||||
cacheDuration := c.Duration("cache-duration")
|
||||
cacheDurationStr := c.String("cache-duration")
|
||||
cacheStartupQueries := c.String("cache-startup-queries")
|
||||
cacheBatchSize := c.Int("cache-batch-size")
|
||||
cacheBatchTimeout := c.Duration("cache-batch-timeout")
|
||||
cacheBatchTimeoutStr := c.String("cache-batch-timeout")
|
||||
authFile := c.String("auth-file")
|
||||
authStartupQueries := c.String("auth-startup-queries")
|
||||
authDefaultAccess := c.String("auth-default-access")
|
||||
authUsersRaw := c.StringSlice("auth-users")
|
||||
authAccessRaw := c.StringSlice("auth-access")
|
||||
authTokensRaw := c.StringSlice("auth-tokens")
|
||||
attachmentCacheDir := c.String("attachment-cache-dir")
|
||||
attachmentTotalSizeLimitStr := c.String("attachment-total-size-limit")
|
||||
attachmentFileSizeLimitStr := c.String("attachment-file-size-limit")
|
||||
attachmentExpiryDuration := c.Duration("attachment-expiry-duration")
|
||||
keepaliveInterval := c.Duration("keepalive-interval")
|
||||
managerInterval := c.Duration("manager-interval")
|
||||
attachmentExpiryDurationStr := c.String("attachment-expiry-duration")
|
||||
templateDir := c.String("template-dir")
|
||||
keepaliveIntervalStr := c.String("keepalive-interval")
|
||||
managerIntervalStr := c.String("manager-interval")
|
||||
disallowedTopics := c.StringSlice("disallowed-topics")
|
||||
webRoot := c.String("web-root")
|
||||
enableSignup := c.Bool("enable-signup")
|
||||
enableLogin := c.Bool("enable-login")
|
||||
requireLogin := c.Bool("require-login")
|
||||
enableReservations := c.Bool("enable-reservations")
|
||||
upstreamBaseURL := c.String("upstream-base-url")
|
||||
upstreamAccessToken := c.String("upstream-access-token")
|
||||
smtpSenderAddr := c.String("smtp-sender-addr")
|
||||
smtpSenderUser := c.String("smtp-sender-user")
|
||||
smtpSenderPass := c.String("smtp-sender-pass")
|
||||
@@ -150,27 +183,109 @@ func execServe(c *cli.Context) error {
|
||||
smtpServerListen := c.String("smtp-server-listen")
|
||||
smtpServerDomain := c.String("smtp-server-domain")
|
||||
smtpServerAddrPrefix := c.String("smtp-server-addr-prefix")
|
||||
twilioAccount := c.String("twilio-account")
|
||||
twilioAuthToken := c.String("twilio-auth-token")
|
||||
twilioPhoneNumber := c.String("twilio-phone-number")
|
||||
twilioVerifyService := c.String("twilio-verify-service")
|
||||
twilioCallFormat := c.String("twilio-call-format")
|
||||
messageSizeLimitStr := c.String("message-size-limit")
|
||||
messageDelayLimitStr := c.String("message-delay-limit")
|
||||
totalTopicLimit := c.Int("global-topic-limit")
|
||||
visitorSubscriptionLimit := c.Int("visitor-subscription-limit")
|
||||
visitorSubscriberRateLimiting := c.Bool("visitor-subscriber-rate-limiting")
|
||||
visitorAttachmentTotalSizeLimitStr := c.String("visitor-attachment-total-size-limit")
|
||||
visitorAttachmentDailyBandwidthLimitStr := c.String("visitor-attachment-daily-bandwidth-limit")
|
||||
visitorRequestLimitBurst := c.Int("visitor-request-limit-burst")
|
||||
visitorRequestLimitReplenish := c.Duration("visitor-request-limit-replenish")
|
||||
visitorRequestLimitReplenishStr := c.String("visitor-request-limit-replenish")
|
||||
visitorRequestLimitExemptHosts := util.SplitNoEmpty(c.String("visitor-request-limit-exempt-hosts"), ",")
|
||||
visitorMessageDailyLimit := c.Int("visitor-message-daily-limit")
|
||||
visitorEmailLimitBurst := c.Int("visitor-email-limit-burst")
|
||||
visitorEmailLimitReplenish := c.Duration("visitor-email-limit-replenish")
|
||||
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")
|
||||
metricsListenHTTP := c.String("metrics-listen-http")
|
||||
enableMetrics := c.Bool("enable-metrics") || metricsListenHTTP != ""
|
||||
profileListenHTTP := c.String("profile-listen-http")
|
||||
|
||||
// Convert durations
|
||||
cacheDuration, err := util.ParseDuration(cacheDurationStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid cache duration: %s", cacheDurationStr)
|
||||
}
|
||||
cacheBatchTimeout, err := util.ParseDuration(cacheBatchTimeoutStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid cache batch timeout: %s", cacheBatchTimeoutStr)
|
||||
}
|
||||
attachmentExpiryDuration, err := util.ParseDuration(attachmentExpiryDurationStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid attachment expiry duration: %s", attachmentExpiryDurationStr)
|
||||
}
|
||||
keepaliveInterval, err := util.ParseDuration(keepaliveIntervalStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid keepalive interval: %s", keepaliveIntervalStr)
|
||||
}
|
||||
managerInterval, err := util.ParseDuration(managerIntervalStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid manager interval: %s", managerIntervalStr)
|
||||
}
|
||||
messageDelayLimit, err := util.ParseDuration(messageDelayLimitStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid message delay limit: %s", messageDelayLimitStr)
|
||||
}
|
||||
visitorRequestLimitReplenish, err := util.ParseDuration(visitorRequestLimitReplenishStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid visitor request limit replenish: %s", visitorRequestLimitReplenishStr)
|
||||
}
|
||||
visitorEmailLimitReplenish, err := util.ParseDuration(visitorEmailLimitReplenishStr)
|
||||
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)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid message size limit: %s", messageSizeLimitStr)
|
||||
}
|
||||
attachmentTotalSizeLimit, err := util.ParseSize(attachmentTotalSizeLimitStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid attachment total size limit: %s", attachmentTotalSizeLimitStr)
|
||||
}
|
||||
attachmentFileSizeLimit, err := util.ParseSize(attachmentFileSizeLimitStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid attachment file size limit: %s", attachmentFileSizeLimitStr)
|
||||
}
|
||||
visitorAttachmentTotalSizeLimit, err := util.ParseSize(visitorAttachmentTotalSizeLimitStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid visitor attachment total size limit: %s", visitorAttachmentTotalSizeLimitStr)
|
||||
}
|
||||
visitorAttachmentDailyBandwidthLimit, err := util.ParseSize(visitorAttachmentDailyBandwidthLimitStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid visitor attachment daily bandwidth limit: %s", visitorAttachmentDailyBandwidthLimitStr)
|
||||
} else if visitorAttachmentDailyBandwidthLimit > math.MaxInt {
|
||||
return fmt.Errorf("config option visitor-attachment-daily-bandwidth-limit must be lower than %d", math.MaxInt)
|
||||
}
|
||||
|
||||
// Check values
|
||||
if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) {
|
||||
return errors.New("if set, FCM key file must exist")
|
||||
} else if firebaseKeyFile != "" && !server.FirebaseAvailable {
|
||||
return errors.New("cannot set firebase-key-file, support for Firebase is not available (nofirebase)")
|
||||
} else if webPushPublicKey != "" && (webPushPrivateKey == "" || webPushFile == "" || webPushEmailAddress == "" || baseURL == "") {
|
||||
return errors.New("if web push is enabled, web-push-private-key, web-push-public-key, web-push-file, web-push-email-address, and base-url should be set. run 'ntfy webpush keys' to generate keys")
|
||||
} else if keepaliveInterval < 5*time.Second {
|
||||
return errors.New("keepalive interval cannot be lower than five seconds")
|
||||
} else if managerInterval < 5*time.Second {
|
||||
@@ -189,12 +304,15 @@ func execServe(c *cli.Context) error {
|
||||
return errors.New("if smtp-server-listen is set, smtp-server-domain must also be set")
|
||||
} else if attachmentCacheDir != "" && baseURL == "" {
|
||||
return errors.New("if attachment-cache-dir is set, base-url must also be set")
|
||||
} else if baseURL != "" && !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") {
|
||||
return errors.New("if set, base-url must start with http:// or https://")
|
||||
} else if baseURL != "" && strings.HasSuffix(baseURL, "/") {
|
||||
return errors.New("if set, base-url must not end with a slash (/)")
|
||||
} else if !util.Contains([]string{"app", "home", "disable"}, webRoot) {
|
||||
return errors.New("if set, web-root must be 'home' or 'app'")
|
||||
} else if baseURL != "" {
|
||||
u, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("if set, base-url must be a valid URL, e.g. https://ntfy.mydomain.com: %v", err)
|
||||
} else if u.Scheme != "http" && u.Scheme != "https" {
|
||||
return errors.New("if set, base-url must be a valid URL starting with http:// or https://, e.g. https://ntfy.mydomain.com")
|
||||
} else if u.Path != "" {
|
||||
return fmt.Errorf("if set, base-url must not have a path (%s), as hosting ntfy on a sub-path is not supported, e.g. https://ntfy.mydomain.com", u.Path)
|
||||
}
|
||||
} else if upstreamBaseURL != "" && !strings.HasPrefix(upstreamBaseURL, "http://") && !strings.HasPrefix(upstreamBaseURL, "https://") {
|
||||
return errors.New("if set, upstream-base-url must start with http:// or https://")
|
||||
} else if upstreamBaseURL != "" && strings.HasSuffix(upstreamBaseURL, "/") {
|
||||
@@ -203,63 +321,95 @@ func execServe(c *cli.Context) error {
|
||||
return errors.New("if upstream-base-url is set, base-url must also be set")
|
||||
} else if upstreamBaseURL != "" && baseURL != "" && baseURL == upstreamBaseURL {
|
||||
return errors.New("base-url and upstream-base-url cannot be identical, you'll likely want to set upstream-base-url to https://ntfy.sh, see https://ntfy.sh/docs/config/#ios-instant-notifications")
|
||||
} else if authFile == "" && (enableSignup || enableLogin || enableReservations || stripeSecretKey != "") {
|
||||
return errors.New("cannot set enable-signup, enable-login, enable-reserve-topics, or stripe-secret-key if auth-file is not set")
|
||||
} else if authFile == "" && (enableSignup || enableLogin || requireLogin || enableReservations || stripeSecretKey != "") {
|
||||
return errors.New("cannot set enable-signup, enable-login, require-login, enable-reserve-topics, or stripe-secret-key if auth-file is not set")
|
||||
} else if enableSignup && !enableLogin {
|
||||
return errors.New("cannot set enable-signup without also setting enable-login")
|
||||
} else if requireLogin && !enableLogin {
|
||||
return errors.New("cannot set require-login without also setting enable-login")
|
||||
} else if !payments.Available && (stripeSecretKey != "" || stripeWebhookKey != "") {
|
||||
return errors.New("cannot set stripe-secret-key or stripe-webhook-key, support for payments is not available in this build (nopayments)")
|
||||
} else if stripeSecretKey != "" && (stripeWebhookKey == "" || baseURL == "") {
|
||||
return errors.New("if stripe-secret-key is set, stripe-webhook-key and base-url must also be set")
|
||||
} else if twilioAccount != "" && (twilioAuthToken == "" || twilioPhoneNumber == "" || twilioVerifyService == "" || baseURL == "" || authFile == "") {
|
||||
return errors.New("if twilio-account is set, twilio-auth-token, twilio-phone-number, twilio-verify-service, base-url, and auth-file must also be set")
|
||||
} else if messageSizeLimit > server.DefaultMessageSizeLimit {
|
||||
log.Warn("message-size-limit is greater than 4K, this is not recommended and largely untested, and may lead to issues with some clients")
|
||||
if messageSizeLimit > 5*1024*1024 {
|
||||
return errors.New("message-size-limit cannot be higher than 5M")
|
||||
}
|
||||
} else if !server.WebPushAvailable && (webPushPrivateKey != "" || webPushPublicKey != "" || webPushFile != "") {
|
||||
return errors.New("cannot enable WebPush, support is not available in this build (nowebpush)")
|
||||
} 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")
|
||||
} else if runtime.GOOS == "windows" && listenUnix != "" {
|
||||
return errors.New("listen-unix is not supported on Windows")
|
||||
}
|
||||
|
||||
webRootIsApp := webRoot == "app"
|
||||
enableWeb := webRoot != "disable"
|
||||
// Backwards compatibility
|
||||
if webRoot == "app" {
|
||||
webRoot = "/"
|
||||
} else if webRoot == "home" {
|
||||
webRoot = "/app"
|
||||
} else if webRoot == "disable" {
|
||||
webRoot = ""
|
||||
} else if !strings.HasPrefix(webRoot, "/") {
|
||||
webRoot = "/" + webRoot
|
||||
}
|
||||
|
||||
// Default auth permissions
|
||||
// Convert default auth permission, read provisioned users
|
||||
authDefault, err := user.ParsePermission(authDefaultAccess)
|
||||
if err != nil {
|
||||
return errors.New("if set, auth-default-access must start set to 'read-write', 'read-only', 'write-only' or 'deny-all'")
|
||||
}
|
||||
authUsers, err := parseUsers(authUsersRaw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authAccess, err := parseAccess(authUsers, authAccessRaw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authTokens, err := parseTokens(authUsers, authTokensRaw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Special case: Unset default
|
||||
if listenHTTP == "-" {
|
||||
listenHTTP = ""
|
||||
}
|
||||
|
||||
// Convert sizes to bytes
|
||||
attachmentTotalSizeLimit, err := parseSize(attachmentTotalSizeLimitStr, server.DefaultAttachmentTotalSizeLimit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
attachmentFileSizeLimit, err := parseSize(attachmentFileSizeLimitStr, server.DefaultAttachmentFileSizeLimit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
visitorAttachmentTotalSizeLimit, err := parseSize(visitorAttachmentTotalSizeLimitStr, server.DefaultVisitorAttachmentTotalSizeLimit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
visitorAttachmentDailyBandwidthLimit, err := parseSize(visitorAttachmentDailyBandwidthLimitStr, server.DefaultVisitorAttachmentDailyBandwidthLimit)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if visitorAttachmentDailyBandwidthLimit > math.MaxInt {
|
||||
return fmt.Errorf("config option visitor-attachment-daily-bandwidth-limit must be lower than %d", math.MaxInt)
|
||||
}
|
||||
|
||||
// Resolve hosts
|
||||
visitorRequestLimitExemptIPs := make([]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
|
||||
if stripeSecretKey != "" {
|
||||
stripe.EnableTelemetry = false // Whoa!
|
||||
stripe.Key = stripeSecretKey
|
||||
payments.Setup(stripeSecretKey)
|
||||
}
|
||||
|
||||
// Add default forbidden topics
|
||||
@@ -284,15 +434,20 @@ func execServe(c *cli.Context) error {
|
||||
conf.AuthFile = authFile
|
||||
conf.AuthStartupQueries = authStartupQueries
|
||||
conf.AuthDefault = authDefault
|
||||
conf.AuthUsers = authUsers
|
||||
conf.AuthAccess = authAccess
|
||||
conf.AuthTokens = authTokens
|
||||
conf.AttachmentCacheDir = attachmentCacheDir
|
||||
conf.AttachmentTotalSizeLimit = attachmentTotalSizeLimit
|
||||
conf.AttachmentFileSizeLimit = attachmentFileSizeLimit
|
||||
conf.AttachmentExpiryDuration = attachmentExpiryDuration
|
||||
conf.TemplateDir = templateDir
|
||||
conf.KeepaliveInterval = keepaliveInterval
|
||||
conf.ManagerInterval = managerInterval
|
||||
conf.DisallowedTopics = disallowedTopics
|
||||
conf.WebRootIsApp = webRootIsApp
|
||||
conf.WebRoot = webRoot
|
||||
conf.UpstreamBaseURL = upstreamBaseURL
|
||||
conf.UpstreamAccessToken = upstreamAccessToken
|
||||
conf.SMTPSenderAddr = smtpSenderAddr
|
||||
conf.SMTPSenderUser = smtpSenderUser
|
||||
conf.SMTPSenderPass = smtpSenderPass
|
||||
@@ -300,28 +455,63 @@ func execServe(c *cli.Context) error {
|
||||
conf.SMTPServerListen = smtpServerListen
|
||||
conf.SMTPServerDomain = smtpServerDomain
|
||||
conf.SMTPServerAddrPrefix = smtpServerAddrPrefix
|
||||
conf.TwilioAccount = twilioAccount
|
||||
conf.TwilioAuthToken = twilioAuthToken
|
||||
conf.TwilioPhoneNumber = twilioPhoneNumber
|
||||
conf.TwilioVerifyService = twilioVerifyService
|
||||
if twilioCallFormat != "" {
|
||||
tmpl, err := template.New("twiml").Parse(twilioCallFormat)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse twilio-call-format template: %w", err)
|
||||
}
|
||||
conf.TwilioCallFormat = tmpl
|
||||
}
|
||||
conf.MessageSizeLimit = int(messageSizeLimit)
|
||||
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
|
||||
conf.EnableWeb = enableWeb
|
||||
conf.EnableSignup = enableSignup
|
||||
conf.EnableLogin = enableLogin
|
||||
conf.RequireLogin = requireLogin
|
||||
conf.EnableReservations = enableReservations
|
||||
conf.EnableMetrics = enableMetrics
|
||||
conf.MetricsListenHTTP = metricsListenHTTP
|
||||
conf.Version = c.App.Version
|
||||
conf.ProfileListenHTTP = profileListenHTTP
|
||||
conf.WebPushPrivateKey = webPushPrivateKey
|
||||
conf.WebPushPublicKey = webPushPublicKey
|
||||
conf.WebPushFile = webPushFile
|
||||
conf.WebPushEmailAddress = webPushEmailAddress
|
||||
conf.WebPushStartupQueries = webPushStartupQueries
|
||||
conf.WebPushExpiryDuration = webPushExpiryDuration
|
||||
conf.WebPushExpiryWarningDuration = webPushExpiryWarningDuration
|
||||
conf.BuildVersion = c.App.Version
|
||||
conf.BuildDate = maybeFromMetadata(c.App.Metadata, MetadataKeyDate)
|
||||
conf.BuildCommit = maybeFromMetadata(c.App.Metadata, MetadataKeyCommit)
|
||||
|
||||
// Check if we should run as a Windows service
|
||||
if ranAsService, err := maybeRunAsService(conf); err != nil {
|
||||
log.Fatal("%s", err.Error())
|
||||
} else if ranAsService {
|
||||
log.Info("Exiting.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set up hot-reloading of config
|
||||
go sigHandlerConfigReload(config)
|
||||
@@ -329,43 +519,16 @@ 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
|
||||
}
|
||||
|
||||
func parseSize(s string, defaultValue int64) (v int64, err error) {
|
||||
if s == "" {
|
||||
return defaultValue, nil
|
||||
}
|
||||
v, err = util.ParseSize(s)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func sigHandlerConfigReload(config string) {
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGHUP)
|
||||
for range sigs {
|
||||
log.Info("Partially hot reloading configuration ...")
|
||||
inputSource, err := newYamlSourceFromFile(config, flagsServe)
|
||||
if err != nil {
|
||||
log.Warn("Hot reload failed: %s", err.Error())
|
||||
continue
|
||||
}
|
||||
if err := reloadLogLevel(inputSource); err != nil {
|
||||
log.Warn("Reloading log level failed: %s", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
@@ -389,24 +552,123 @@ func parseIPHostPrefix(host string) (prefixes []netip.Prefix, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func reloadLogLevel(inputSource altsrc.InputSourceContext) error {
|
||||
newLevelStr, err := inputSource.String("log-level")
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot load log level: %s", err.Error())
|
||||
func parseUsers(usersRaw []string) ([]*user.User, error) {
|
||||
users := make([]*user.User, 0)
|
||||
for _, userLine := range usersRaw {
|
||||
parts := strings.Split(userLine, ":")
|
||||
if len(parts) != 3 {
|
||||
return nil, fmt.Errorf("invalid auth-users: %s, expected format: 'name:hash:role'", userLine)
|
||||
}
|
||||
username := strings.TrimSpace(parts[0])
|
||||
passwordHash := strings.TrimSpace(parts[1])
|
||||
role := user.Role(strings.TrimSpace(parts[2]))
|
||||
if !user.AllowedUsername(username) {
|
||||
return nil, fmt.Errorf("invalid auth-users: %s, username invalid", userLine)
|
||||
} else if err := user.ValidPasswordHash(passwordHash, user.DefaultUserPasswordBcryptCost); err != nil {
|
||||
return nil, fmt.Errorf("invalid auth-users: %s, password hash invalid, %s", userLine, err.Error())
|
||||
} else if !user.AllowedRole(role) {
|
||||
return nil, fmt.Errorf("invalid auth-users: %s, role %s is not allowed, allowed roles are 'admin' or 'user'", userLine, role)
|
||||
}
|
||||
users = append(users, &user.User{
|
||||
Name: username,
|
||||
Hash: passwordHash,
|
||||
Role: role,
|
||||
Provisioned: true,
|
||||
})
|
||||
}
|
||||
overrides, err := inputSource.StringSlice("log-level-overrides")
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot load log level overrides (1): %s", err.Error())
|
||||
}
|
||||
log.ResetLevelOverrides()
|
||||
if err := applyLogLevelOverrides(overrides); err != nil {
|
||||
return fmt.Errorf("cannot load log level overrides (2): %s", err.Error())
|
||||
}
|
||||
log.SetLevel(log.ToLevel(newLevelStr))
|
||||
if len(overrides) > 0 {
|
||||
log.Info("Log level is %v, %d override(s) in place", strings.ToUpper(newLevelStr), len(overrides))
|
||||
} else {
|
||||
log.Info("Log level is %v", strings.ToUpper(newLevelStr))
|
||||
}
|
||||
return nil
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func parseAccess(users []*user.User, accessRaw []string) (map[string][]*user.Grant, error) {
|
||||
access := make(map[string][]*user.Grant)
|
||||
for _, accessLine := range accessRaw {
|
||||
parts := strings.Split(accessLine, ":")
|
||||
if len(parts) != 3 {
|
||||
return nil, fmt.Errorf("invalid auth-access: %s, expected format: 'user:topic:permission'", accessLine)
|
||||
}
|
||||
username := strings.TrimSpace(parts[0])
|
||||
if username == userEveryone {
|
||||
username = user.Everyone
|
||||
}
|
||||
u, exists := util.Find(users, func(u *user.User) bool {
|
||||
return u.Name == username
|
||||
})
|
||||
if username != user.Everyone {
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("invalid auth-access: %s, user %s is not provisioned", accessLine, username)
|
||||
} else if !user.AllowedUsername(username) {
|
||||
return nil, fmt.Errorf("invalid auth-access: %s, username %s invalid", accessLine, username)
|
||||
} else if u.Role != user.RoleUser {
|
||||
return nil, fmt.Errorf("invalid auth-access: %s, user %s is not a regular user, only regular users can have ACL entries", accessLine, username)
|
||||
}
|
||||
}
|
||||
topic := strings.TrimSpace(parts[1])
|
||||
if !user.AllowedTopicPattern(topic) {
|
||||
return nil, fmt.Errorf("invalid auth-access: %s, topic pattern %s invalid", accessLine, topic)
|
||||
}
|
||||
permission, err := user.ParsePermission(strings.TrimSpace(parts[2]))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid auth-access: %s, permission %s invalid, %s", accessLine, parts[2], err.Error())
|
||||
}
|
||||
if _, exists := access[username]; !exists {
|
||||
access[username] = make([]*user.Grant, 0)
|
||||
}
|
||||
access[username] = append(access[username], &user.Grant{
|
||||
TopicPattern: topic,
|
||||
Permission: permission,
|
||||
Provisioned: true,
|
||||
})
|
||||
}
|
||||
return access, nil
|
||||
}
|
||||
|
||||
func parseTokens(users []*user.User, tokensRaw []string) (map[string][]*user.Token, error) {
|
||||
tokens := make(map[string][]*user.Token)
|
||||
for _, tokenLine := range tokensRaw {
|
||||
parts := strings.Split(tokenLine, ":")
|
||||
if len(parts) < 2 || len(parts) > 3 {
|
||||
return nil, fmt.Errorf("invalid auth-tokens: %s, expected format: 'user:token[:label]'", tokenLine)
|
||||
}
|
||||
username := strings.TrimSpace(parts[0])
|
||||
_, exists := util.Find(users, func(u *user.User) bool {
|
||||
return u.Name == username
|
||||
})
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("invalid auth-tokens: %s, user %s is not provisioned", tokenLine, username)
|
||||
} else if !user.AllowedUsername(username) {
|
||||
return nil, fmt.Errorf("invalid auth-tokens: %s, username %s invalid", tokenLine, username)
|
||||
}
|
||||
token := strings.TrimSpace(parts[1])
|
||||
if !user.ValidToken(token) {
|
||||
return nil, fmt.Errorf("invalid auth-tokens: %s, token %s invalid, use 'ntfy token generate' to generate a random token", tokenLine, token)
|
||||
}
|
||||
var label string
|
||||
if len(parts) > 2 {
|
||||
label = parts[2]
|
||||
}
|
||||
if _, exists := tokens[username]; !exists {
|
||||
tokens[username] = make([]*user.Token, 0)
|
||||
}
|
||||
tokens[username] = append(tokens[username], &user.Token{
|
||||
Value: token,
|
||||
Label: label,
|
||||
Provisioned: true,
|
||||
})
|
||||
}
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
func maybeFromMetadata(m map[string]any, key string) string {
|
||||
if m == nil {
|
||||
return ""
|
||||
}
|
||||
v, exists := m[key]
|
||||
if !exists {
|
||||
return ""
|
||||
}
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -12,13 +12,461 @@ import (
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"heckel.io/ntfy/client"
|
||||
"heckel.io/ntfy/test"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/client"
|
||||
"heckel.io/ntfy/v2/test"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixMilli())
|
||||
func TestParseUsers_Success(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []string
|
||||
expected []*user.User
|
||||
}{
|
||||
{
|
||||
name: "single user",
|
||||
input: []string{"alice:$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:user"},
|
||||
expected: []*user.User{
|
||||
{
|
||||
Name: "alice",
|
||||
Hash: "$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S",
|
||||
Role: user.RoleUser,
|
||||
Provisioned: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple users with different roles",
|
||||
input: []string{
|
||||
"alice:$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:user",
|
||||
"bob:$2a$10$jIcuBWcbxd6oW1aPvoJ5iOShzu3/UJ2kSxKbTZtDypG06nBflQagq:admin",
|
||||
},
|
||||
expected: []*user.User{
|
||||
{
|
||||
Name: "alice",
|
||||
Hash: "$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S",
|
||||
Role: user.RoleUser,
|
||||
Provisioned: true,
|
||||
},
|
||||
{
|
||||
Name: "bob",
|
||||
Hash: "$2a$10$jIcuBWcbxd6oW1aPvoJ5iOShzu3/UJ2kSxKbTZtDypG06nBflQagq",
|
||||
Role: user.RoleAdmin,
|
||||
Provisioned: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty input",
|
||||
input: []string{},
|
||||
expected: []*user.User{},
|
||||
},
|
||||
{
|
||||
name: "user with special characters in name",
|
||||
input: []string{"alice.test+123@example.com:$2a$10$RYUYAsl5zOnAIp6fH7BPX.Eug0rUfEUk92r8WiVusb0VK.vGojWBe:user"},
|
||||
expected: []*user.User{
|
||||
{
|
||||
Name: "alice.test+123@example.com",
|
||||
Hash: "$2a$10$RYUYAsl5zOnAIp6fH7BPX.Eug0rUfEUk92r8WiVusb0VK.vGojWBe",
|
||||
Role: user.RoleUser,
|
||||
Provisioned: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := parseUsers(tt.input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result, len(tt.expected))
|
||||
|
||||
for i, expectedUser := range tt.expected {
|
||||
assert.Equal(t, expectedUser.Name, result[i].Name)
|
||||
assert.Equal(t, expectedUser.Hash, result[i].Hash)
|
||||
assert.Equal(t, expectedUser.Role, result[i].Role)
|
||||
assert.Equal(t, expectedUser.Provisioned, result[i].Provisioned)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseUsers_Errors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []string
|
||||
error string
|
||||
}{
|
||||
{
|
||||
name: "invalid format - too few parts",
|
||||
input: []string{"alice:hash"},
|
||||
error: "invalid auth-users: alice:hash, expected format: 'name:hash:role'",
|
||||
},
|
||||
{
|
||||
name: "invalid format - too many parts",
|
||||
input: []string{"alice:hash:role:extra"},
|
||||
error: "invalid auth-users: alice:hash:role:extra, expected format: 'name:hash:role'",
|
||||
},
|
||||
{
|
||||
name: "invalid username",
|
||||
input: []string{"alice@#$%:$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:user"},
|
||||
error: "invalid auth-users: alice@#$%:$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:user, username invalid",
|
||||
},
|
||||
{
|
||||
name: "invalid password hash - wrong prefix",
|
||||
input: []string{"alice:plaintext:user"},
|
||||
error: "invalid auth-users: alice:plaintext:user, password hash invalid, password hash must be a bcrypt hash, use 'ntfy user hash' to generate",
|
||||
},
|
||||
{
|
||||
name: "invalid role",
|
||||
input: []string{"alice:$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:invalid"},
|
||||
error: "invalid auth-users: alice:$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:invalid, role invalid is not allowed, allowed roles are 'admin' or 'user'",
|
||||
},
|
||||
{
|
||||
name: "empty username",
|
||||
input: []string{":$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:user"},
|
||||
error: "invalid auth-users: :$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:user, username invalid",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := parseUsers(tt.input)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, result)
|
||||
assert.Contains(t, err.Error(), tt.error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAccess_Success(t *testing.T) {
|
||||
users := []*user.User{
|
||||
{Name: "alice", Role: user.RoleUser},
|
||||
{Name: "bob", Role: user.RoleUser},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
users []*user.User
|
||||
input []string
|
||||
expected map[string][]*user.Grant
|
||||
}{
|
||||
{
|
||||
name: "single access entry",
|
||||
users: users,
|
||||
input: []string{"alice:mytopic:read-write"},
|
||||
expected: map[string][]*user.Grant{
|
||||
"alice": {
|
||||
{
|
||||
TopicPattern: "mytopic",
|
||||
Permission: user.PermissionReadWrite,
|
||||
Provisioned: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple access entries for same user",
|
||||
users: users,
|
||||
input: []string{
|
||||
"alice:topic1:read-only",
|
||||
"alice:topic2:write-only",
|
||||
},
|
||||
expected: map[string][]*user.Grant{
|
||||
"alice": {
|
||||
{
|
||||
TopicPattern: "topic1",
|
||||
Permission: user.PermissionRead,
|
||||
Provisioned: true,
|
||||
},
|
||||
{
|
||||
TopicPattern: "topic2",
|
||||
Permission: user.PermissionWrite,
|
||||
Provisioned: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "access for everyone",
|
||||
users: users,
|
||||
input: []string{"everyone:publictopic:read-only"},
|
||||
expected: map[string][]*user.Grant{
|
||||
user.Everyone: {
|
||||
{
|
||||
TopicPattern: "publictopic",
|
||||
Permission: user.PermissionRead,
|
||||
Provisioned: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wildcard topic pattern",
|
||||
users: users,
|
||||
input: []string{"alice:topic*:read-write"},
|
||||
expected: map[string][]*user.Grant{
|
||||
"alice": {
|
||||
{
|
||||
TopicPattern: "topic*",
|
||||
Permission: user.PermissionReadWrite,
|
||||
Provisioned: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty input",
|
||||
users: users,
|
||||
input: []string{},
|
||||
expected: map[string][]*user.Grant{},
|
||||
},
|
||||
{
|
||||
name: "deny-all permission",
|
||||
users: users,
|
||||
input: []string{"alice:secretopic:deny-all"},
|
||||
expected: map[string][]*user.Grant{
|
||||
"alice": {
|
||||
{
|
||||
TopicPattern: "secretopic",
|
||||
Permission: user.PermissionDenyAll,
|
||||
Provisioned: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := parseAccess(tt.users, tt.input)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAccess_Errors(t *testing.T) {
|
||||
users := []*user.User{
|
||||
{Name: "alice", Role: user.RoleUser},
|
||||
{Name: "admin", Role: user.RoleAdmin},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
users []*user.User
|
||||
input []string
|
||||
error string
|
||||
}{
|
||||
{
|
||||
name: "invalid format - too few parts",
|
||||
users: users,
|
||||
input: []string{"alice:topic"},
|
||||
error: "invalid auth-access: alice:topic, expected format: 'user:topic:permission'",
|
||||
},
|
||||
{
|
||||
name: "invalid format - too many parts",
|
||||
users: users,
|
||||
input: []string{"alice:topic:read:extra"},
|
||||
error: "invalid auth-access: alice:topic:read:extra, expected format: 'user:topic:permission'",
|
||||
},
|
||||
{
|
||||
name: "user not provisioned",
|
||||
users: users,
|
||||
input: []string{"charlie:topic:read"},
|
||||
error: "invalid auth-access: charlie:topic:read, user charlie is not provisioned",
|
||||
},
|
||||
{
|
||||
name: "admin user cannot have ACL entries",
|
||||
users: users,
|
||||
input: []string{"admin:topic:read"},
|
||||
error: "invalid auth-access: admin:topic:read, user admin is not a regular user, only regular users can have ACL entries",
|
||||
},
|
||||
{
|
||||
name: "invalid topic pattern",
|
||||
users: users,
|
||||
input: []string{"alice:topic-with-invalid-chars!:read"},
|
||||
error: "invalid auth-access: alice:topic-with-invalid-chars!:read, topic pattern topic-with-invalid-chars! invalid",
|
||||
},
|
||||
{
|
||||
name: "invalid permission",
|
||||
users: users,
|
||||
input: []string{"alice:topic:invalid-permission"},
|
||||
error: "invalid auth-access: alice:topic:invalid-permission, permission invalid-permission invalid",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := parseAccess(tt.users, tt.input)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, result)
|
||||
assert.Contains(t, err.Error(), tt.error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTokens_Success(t *testing.T) {
|
||||
users := []*user.User{
|
||||
{Name: "alice"},
|
||||
{Name: "bob"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
users []*user.User
|
||||
input []string
|
||||
expected map[string][]*user.Token
|
||||
}{
|
||||
{
|
||||
name: "single token without label",
|
||||
users: users,
|
||||
input: []string{"alice:tk_abcdefghijklmnopqrstuvwxyz123"},
|
||||
expected: map[string][]*user.Token{
|
||||
"alice": {
|
||||
{
|
||||
Value: "tk_abcdefghijklmnopqrstuvwxyz123",
|
||||
Label: "",
|
||||
Provisioned: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single token with label",
|
||||
users: users,
|
||||
input: []string{"alice:tk_abcdefghijklmnopqrstuvwxyz123:My Phone"},
|
||||
expected: map[string][]*user.Token{
|
||||
"alice": {
|
||||
{
|
||||
Value: "tk_abcdefghijklmnopqrstuvwxyz123",
|
||||
Label: "My Phone",
|
||||
Provisioned: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple tokens for same user",
|
||||
users: users,
|
||||
input: []string{
|
||||
"alice:tk_abcdefghijklmnopqrstuvwxyz123:Phone",
|
||||
"alice:tk_zyxwvutsrqponmlkjihgfedcba987:Laptop",
|
||||
},
|
||||
expected: map[string][]*user.Token{
|
||||
"alice": {
|
||||
{
|
||||
Value: "tk_abcdefghijklmnopqrstuvwxyz123",
|
||||
Label: "Phone",
|
||||
Provisioned: true,
|
||||
},
|
||||
{
|
||||
Value: "tk_zyxwvutsrqponmlkjihgfedcba987",
|
||||
Label: "Laptop",
|
||||
Provisioned: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tokens for multiple users",
|
||||
users: users,
|
||||
input: []string{
|
||||
"alice:tk_abcdefghijklmnopqrstuvwxyz123:Phone",
|
||||
"bob:tk_zyxwvutsrqponmlkjihgfedcba987:Tablet",
|
||||
},
|
||||
expected: map[string][]*user.Token{
|
||||
"alice": {
|
||||
{
|
||||
Value: "tk_abcdefghijklmnopqrstuvwxyz123",
|
||||
Label: "Phone",
|
||||
Provisioned: true,
|
||||
},
|
||||
},
|
||||
"bob": {
|
||||
{
|
||||
Value: "tk_zyxwvutsrqponmlkjihgfedcba987",
|
||||
Label: "Tablet",
|
||||
Provisioned: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty input",
|
||||
users: users,
|
||||
input: []string{},
|
||||
expected: map[string][]*user.Token{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := parseTokens(tt.users, tt.input)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTokens_Errors(t *testing.T) {
|
||||
users := []*user.User{
|
||||
{Name: "alice"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
users []*user.User
|
||||
input []string
|
||||
error string
|
||||
}{
|
||||
{
|
||||
name: "invalid format - too few parts",
|
||||
users: users,
|
||||
input: []string{"alice"},
|
||||
error: "invalid auth-tokens: alice, expected format: 'user:token[:label]'",
|
||||
},
|
||||
{
|
||||
name: "invalid format - too many parts",
|
||||
users: users,
|
||||
input: []string{"alice:token:label:extra:parts"},
|
||||
error: "invalid auth-tokens: alice:token:label:extra:parts, expected format: 'user:token[:label]'",
|
||||
},
|
||||
{
|
||||
name: "user not provisioned",
|
||||
users: users,
|
||||
input: []string{"charlie:tk_abcdefghijklmnopqrstuvwxyz123"},
|
||||
error: "invalid auth-tokens: charlie:tk_abcdefghijklmnopqrstuvwxyz123, user charlie is not provisioned",
|
||||
},
|
||||
{
|
||||
name: "invalid token format",
|
||||
users: users,
|
||||
input: []string{"alice:invalid-token"},
|
||||
error: "invalid auth-tokens: alice:invalid-token, token invalid-token invalid, use 'ntfy token generate' to generate a random token",
|
||||
},
|
||||
{
|
||||
name: "token too short",
|
||||
users: users,
|
||||
input: []string{"alice:tk_short"},
|
||||
error: "invalid auth-tokens: alice:tk_short, token tk_short invalid, use 'ntfy token generate' to generate a random token",
|
||||
},
|
||||
{
|
||||
name: "token without prefix",
|
||||
users: users,
|
||||
input: []string{"alice:abcdefghijklmnopqrstuvwxyz12345"},
|
||||
error: "invalid auth-tokens: alice:abcdefghijklmnopqrstuvwxyz12345, token abcdefghijklmnopqrstuvwxyz12345 invalid, use 'ntfy token generate' to generate a random token",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := parseTokens(tt.users, tt.input)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, result)
|
||||
assert.Contains(t, err.Error(), tt.error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLI_Serve_Unix_Curl(t *testing.T) {
|
||||
|
||||
55
cmd/serve_unix.go
Normal file
@@ -0,0 +1,55 @@
|
||||
//go:build linux || dragonfly || freebsd || netbsd || openbsd
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/server"
|
||||
)
|
||||
|
||||
func sigHandlerConfigReload(config string) {
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGHUP)
|
||||
for range sigs {
|
||||
log.Info("Partially hot reloading configuration ...")
|
||||
inputSource, err := newYamlSourceFromFile(config, flagsServe)
|
||||
if err != nil {
|
||||
log.Warn("Hot reload failed: %s", err.Error())
|
||||
continue
|
||||
}
|
||||
if err := reloadLogLevel(inputSource); err != nil {
|
||||
log.Warn("Reloading log level failed: %s", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func reloadLogLevel(inputSource altsrc.InputSourceContext) error {
|
||||
newLevelStr, err := inputSource.String("log-level")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
overrides, err := inputSource.StringSlice("log-level-overrides")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.ResetLevelOverrides()
|
||||
if err := applyLogLevelOverrides(overrides); err != nil {
|
||||
return err
|
||||
}
|
||||
log.SetLevel(log.ToLevel(newLevelStr))
|
||||
if len(overrides) > 0 {
|
||||
log.Info("Log level is %v, %d override(s) in place", newLevelStr, len(overrides))
|
||||
} else {
|
||||
log.Info("Log level is %v", newLevelStr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func maybeRunAsService(conf *server.Config) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
100
cmd/serve_windows.go
Normal file
@@ -0,0 +1,100 @@
|
||||
//go:build windows && !noserver
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/sys/windows/svc"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/server"
|
||||
)
|
||||
|
||||
const serviceName = "ntfy"
|
||||
|
||||
// sigHandlerConfigReload is a no-op on Windows since SIGHUP is not available.
|
||||
// Windows users can restart the service to reload configuration.
|
||||
func sigHandlerConfigReload(config string) {
|
||||
log.Debug("Config hot-reload via SIGHUP is not supported on Windows")
|
||||
}
|
||||
|
||||
// runAsWindowsService runs the ntfy server as a Windows service
|
||||
func runAsWindowsService(conf *server.Config) error {
|
||||
return svc.Run(serviceName, &windowsService{conf: conf})
|
||||
}
|
||||
|
||||
// windowsService implements the svc.Handler interface
|
||||
type windowsService struct {
|
||||
conf *server.Config
|
||||
server *server.Server
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// Execute is the main entry point for the Windows service
|
||||
func (s *windowsService) Execute(args []string, requests <-chan svc.ChangeRequest, status chan<- svc.Status) (bool, uint32) {
|
||||
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
|
||||
status <- svc.Status{State: svc.StartPending}
|
||||
|
||||
// Create and start the server
|
||||
var err error
|
||||
s.mu.Lock()
|
||||
s.server, err = server.New(s.conf)
|
||||
s.mu.Unlock()
|
||||
if err != nil {
|
||||
log.Error("Failed to create server: %s", err.Error())
|
||||
return true, 1
|
||||
}
|
||||
|
||||
// Start server in a goroutine
|
||||
serverErrChan := make(chan error, 1)
|
||||
go func() {
|
||||
serverErrChan <- s.server.Run()
|
||||
}()
|
||||
|
||||
status <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
|
||||
log.Info("Windows service started")
|
||||
|
||||
for {
|
||||
select {
|
||||
case err := <-serverErrChan:
|
||||
if err != nil {
|
||||
log.Error("Server error: %s", err.Error())
|
||||
return true, 1
|
||||
}
|
||||
return false, 0
|
||||
case req := <-requests:
|
||||
switch req.Cmd {
|
||||
case svc.Interrogate:
|
||||
status <- req.CurrentStatus
|
||||
case svc.Stop, svc.Shutdown:
|
||||
log.Info("Windows service stopping...")
|
||||
status <- svc.Status{State: svc.StopPending}
|
||||
s.mu.Lock()
|
||||
if s.server != nil {
|
||||
s.server.Stop()
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return false, 0
|
||||
default:
|
||||
log.Warn("Unexpected service control request: %d", req.Cmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// maybeRunAsService checks if the process is running as a Windows service,
|
||||
// and if so, runs the server as a service. Returns true if it ran as a service.
|
||||
func maybeRunAsService(conf *server.Config) (bool, error) {
|
||||
isService, err := svc.IsWindowsService()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to detect Windows service mode: %w", err)
|
||||
} else if !isService {
|
||||
return false, nil
|
||||
}
|
||||
log.Info("Running as Windows service")
|
||||
if err := runAsWindowsService(conf); err != nil {
|
||||
return true, fmt.Errorf("failed to run as Windows service: %w", err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
@@ -3,28 +3,21 @@ package cmd
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/urfave/cli/v2"
|
||||
"heckel.io/ntfy/client"
|
||||
"heckel.io/ntfy/log"
|
||||
"heckel.io/ntfy/util"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"heckel.io/ntfy/v2/client"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commands = append(commands, cmdSubscribe)
|
||||
}
|
||||
|
||||
const (
|
||||
clientRootConfigFileUnixAbsolute = "/etc/ntfy/client.yml"
|
||||
clientUserConfigFileUnixRelative = "ntfy/client.yml"
|
||||
clientUserConfigFileWindowsRelative = "ntfy\\client.yml"
|
||||
)
|
||||
|
||||
var flagsSubscribe = append(
|
||||
append([]cli.Flag{}, flagsDefault...),
|
||||
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "client config file"},
|
||||
@@ -72,7 +65,7 @@ ntfy subscribe TOPIC COMMAND
|
||||
$NTFY_TITLE $title, $t Message title
|
||||
$NTFY_PRIORITY $priority, $prio, $p Message priority (1=min, 5=max)
|
||||
$NTFY_TAGS $tags, $tag, $ta Message tags (comma separated list)
|
||||
$NTFY_RAW $raw Raw JSON message
|
||||
$NTFY_RAW $raw Raw JSON message
|
||||
|
||||
Examples:
|
||||
ntfy sub mytopic 'notify-send "$m"' # Execute command for incoming messages
|
||||
@@ -119,8 +112,7 @@ func execSubscribe(c *cli.Context) error {
|
||||
}
|
||||
if token != "" {
|
||||
options = append(options, client.WithBearerAuth(token))
|
||||
}
|
||||
if user != "" {
|
||||
} else if user != "" {
|
||||
var pass string
|
||||
parts := strings.SplitN(user, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
@@ -136,6 +128,10 @@ func execSubscribe(c *cli.Context) error {
|
||||
fmt.Fprintf(c.App.ErrWriter, "\r%s\r", strings.Repeat(" ", 20))
|
||||
}
|
||||
options = append(options, client.WithBasicAuth(user, pass))
|
||||
} else if conf.DefaultToken != "" {
|
||||
options = append(options, client.WithBearerAuth(conf.DefaultToken))
|
||||
} else if conf.DefaultUser != "" && conf.DefaultPassword != nil {
|
||||
options = append(options, client.WithBasicAuth(conf.DefaultUser, *conf.DefaultPassword))
|
||||
}
|
||||
if scheduled {
|
||||
options = append(options, client.WithScheduled())
|
||||
@@ -191,7 +187,10 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic,
|
||||
topicOptions = append(topicOptions, auth)
|
||||
}
|
||||
|
||||
subscriptionID := cl.Subscribe(s.Topic, topicOptions...)
|
||||
subscriptionID, err := cl.Subscribe(s.Topic, topicOptions...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.Command != "" {
|
||||
cmds[subscriptionID] = s.Command
|
||||
} else if conf.DefaultCommand != "" {
|
||||
@@ -201,7 +200,10 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic,
|
||||
}
|
||||
}
|
||||
if topic != "" {
|
||||
subscriptionID := cl.Subscribe(topic, options...)
|
||||
subscriptionID, err := cl.Subscribe(topic, options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmds[subscriptionID] = command
|
||||
}
|
||||
for m := range cl.Messages {
|
||||
@@ -216,12 +218,17 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic,
|
||||
}
|
||||
|
||||
func maybeAddAuthHeader(s client.Subscribe, conf *client.Config) client.SubscribeOption {
|
||||
// check for subscription token then subscription user:pass
|
||||
if s.Token != "" {
|
||||
return client.WithBearerAuth(s.Token)
|
||||
// if an explicit empty token or empty user:pass is given, exit without auth
|
||||
if (s.Token != nil && *s.Token == "") || (s.User != nil && *s.User == "" && s.Password != nil && *s.Password == "") {
|
||||
return client.WithEmptyAuth()
|
||||
}
|
||||
if s.User != "" && s.Password != nil {
|
||||
return client.WithBasicAuth(s.User, *s.Password)
|
||||
|
||||
// check for subscription token then subscription user:pass
|
||||
if s.Token != nil && *s.Token != "" {
|
||||
return client.WithBearerAuth(*s.Token)
|
||||
}
|
||||
if s.User != nil && *s.User != "" && s.Password != nil {
|
||||
return client.WithBasicAuth(*s.User, *s.Password)
|
||||
}
|
||||
|
||||
// if no subscription token nor subscription user:pass, check for default token then default user:pass
|
||||
@@ -296,30 +303,16 @@ func loadConfig(c *cli.Context) (*client.Config, error) {
|
||||
if filename != "" {
|
||||
return client.LoadConfig(filename)
|
||||
}
|
||||
configFile := defaultClientConfigFile()
|
||||
if s, _ := os.Stat(configFile); s != nil {
|
||||
return client.LoadConfig(configFile)
|
||||
if client.DefaultConfigFile != "" {
|
||||
if s, _ := os.Stat(client.DefaultConfigFile); s != nil {
|
||||
return client.LoadConfig(client.DefaultConfigFile)
|
||||
}
|
||||
log.Debug("Config file %s not found", client.DefaultConfigFile)
|
||||
}
|
||||
log.Debug("Loading default config")
|
||||
return client.NewConfig(), nil
|
||||
}
|
||||
|
||||
//lint:ignore U1000 Conditionally used in different builds
|
||||
func defaultClientConfigFileUnix() string {
|
||||
u, _ := user.Current()
|
||||
configFile := clientRootConfigFileUnixAbsolute
|
||||
if u.Uid != "0" {
|
||||
homeDir, _ := os.UserConfigDir()
|
||||
return filepath.Join(homeDir, clientUserConfigFileUnixRelative)
|
||||
}
|
||||
return configFile
|
||||
}
|
||||
|
||||
//lint:ignore U1000 Conditionally used in different builds
|
||||
func defaultClientConfigFileWindows() string {
|
||||
homeDir, _ := os.UserConfigDir()
|
||||
return filepath.Join(homeDir, clientUserConfigFileWindowsRelative)
|
||||
}
|
||||
|
||||
func logMessagePrefix(m *client.Message) string {
|
||||
return fmt.Sprintf("%s/%s", util.ShortTopicURL(m.TopicURL), m.ID)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build darwin
|
||||
|
||||
package cmd
|
||||
|
||||
const (
|
||||
@@ -10,7 +12,3 @@ or "~/Library/Application Support/ntfy/client.yml" for all other users.`
|
||||
var (
|
||||
scriptLauncher = []string{"sh", "-c"}
|
||||
)
|
||||
|
||||
func defaultClientConfigFile() string {
|
||||
return defaultClientConfigFileUnix()
|
||||
}
|
||||
|
||||
@@ -310,3 +310,108 @@ func TestCLI_Subscribe_Token_And_UserPass(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
require.Equal(t, "cannot set both --user and --token", err.Error())
|
||||
}
|
||||
|
||||
func TestCLI_Subscribe_Default_Token(t *testing.T) {
|
||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
||||
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(message))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||
default-host: %s
|
||||
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
||||
`, server.URL)), 0600))
|
||||
|
||||
app, _, stdout, _ := newTestApp()
|
||||
|
||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "mytopic"}))
|
||||
|
||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||
}
|
||||
|
||||
func TestCLI_Subscribe_Default_UserPass(t *testing.T) {
|
||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
||||
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(message))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||
default-host: %s
|
||||
default-user: philipp
|
||||
default-password: mypass
|
||||
`, server.URL)), 0600))
|
||||
|
||||
app, _, stdout, _ := newTestApp()
|
||||
|
||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "mytopic"}))
|
||||
|
||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||
}
|
||||
|
||||
func TestCLI_Subscribe_Override_Default_UserPass_With_Empty_UserPass(t *testing.T) {
|
||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
||||
require.Equal(t, "", r.Header.Get("Authorization"))
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(message))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||
default-host: %s
|
||||
default-user: philipp
|
||||
default-password: mypass
|
||||
subscribe:
|
||||
- topic: mytopic
|
||||
user: ""
|
||||
password: ""
|
||||
`, server.URL)), 0600))
|
||||
|
||||
app, _, stdout, _ := newTestApp()
|
||||
|
||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
||||
|
||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||
}
|
||||
|
||||
func TestCLI_Subscribe_Override_Default_Token_With_Empty_Token(t *testing.T) {
|
||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
||||
require.Equal(t, "", r.Header.Get("Authorization"))
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(message))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||
default-host: %s
|
||||
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
||||
subscribe:
|
||||
- topic: mytopic
|
||||
token: ""
|
||||
`, server.URL)), 0600))
|
||||
|
||||
app, _, stdout, _ := newTestApp()
|
||||
|
||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
||||
|
||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||
}
|
||||
|
||||
@@ -12,7 +12,3 @@ or ~/.config/ntfy/client.yml for all other users.`
|
||||
var (
|
||||
scriptLauncher = []string{"sh", "-c"}
|
||||
)
|
||||
|
||||
func defaultClientConfigFile() string {
|
||||
return defaultClientConfigFileUnix()
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build windows
|
||||
|
||||
package cmd
|
||||
|
||||
const (
|
||||
@@ -9,7 +11,3 @@ const (
|
||||
var (
|
||||
scriptLauncher = []string{"cmd.exe", "/Q", "/C"}
|
||||
)
|
||||
|
||||
func defaultClientConfigFile() string {
|
||||
return defaultClientConfigFileWindows()
|
||||
}
|
||||
|
||||
42
cmd/tier.go
@@ -6,8 +6,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/urfave/cli/v2"
|
||||
"heckel.io/ntfy/user"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -18,6 +18,7 @@ const (
|
||||
defaultMessageLimit = 5000
|
||||
defaultMessageExpiryDuration = "12h"
|
||||
defaultEmailLimit = 20
|
||||
defaultCallLimit = 0
|
||||
defaultReservationLimit = 3
|
||||
defaultAttachmentFileSizeLimit = "15M"
|
||||
defaultAttachmentTotalSizeLimit = "100M"
|
||||
@@ -48,6 +49,7 @@ var cmdTier = &cli.Command{
|
||||
&cli.Int64Flag{Name: "message-limit", Value: defaultMessageLimit, Usage: "daily message limit"},
|
||||
&cli.StringFlag{Name: "message-expiry-duration", Value: defaultMessageExpiryDuration, Usage: "duration after which messages are deleted"},
|
||||
&cli.Int64Flag{Name: "email-limit", Value: defaultEmailLimit, Usage: "daily email limit"},
|
||||
&cli.Int64Flag{Name: "call-limit", Value: defaultCallLimit, Usage: "daily phone call limit"},
|
||||
&cli.Int64Flag{Name: "reservation-limit", Value: defaultReservationLimit, Usage: "topic reservation limit"},
|
||||
&cli.StringFlag{Name: "attachment-file-size-limit", Value: defaultAttachmentFileSizeLimit, Usage: "per-attachment file size limit"},
|
||||
&cli.StringFlag{Name: "attachment-total-size-limit", Value: defaultAttachmentTotalSizeLimit, Usage: "total size limit of attachments for the user"},
|
||||
@@ -91,6 +93,7 @@ Examples:
|
||||
&cli.Int64Flag{Name: "message-limit", Usage: "daily message limit"},
|
||||
&cli.StringFlag{Name: "message-expiry-duration", Usage: "duration after which messages are deleted"},
|
||||
&cli.Int64Flag{Name: "email-limit", Usage: "daily email limit"},
|
||||
&cli.Int64Flag{Name: "call-limit", Usage: "daily phone call limit"},
|
||||
&cli.Int64Flag{Name: "reservation-limit", Usage: "topic reservation limit"},
|
||||
&cli.StringFlag{Name: "attachment-file-size-limit", Usage: "per-attachment file size limit"},
|
||||
&cli.StringFlag{Name: "attachment-total-size-limit", Usage: "total size limit of attachments for the user"},
|
||||
@@ -179,7 +182,7 @@ func execTierAdd(c *cli.Context) error {
|
||||
}
|
||||
if tier, _ := manager.Tier(code); tier != nil {
|
||||
if c.Bool("ignore-exists") {
|
||||
fmt.Fprintf(c.App.ErrWriter, "tier %s already exists (exited successfully)\n", code)
|
||||
fmt.Fprintf(c.App.Writer, "tier %s already exists (exited successfully)\n", code)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("tier %s already exists", code)
|
||||
@@ -215,6 +218,7 @@ func execTierAdd(c *cli.Context) error {
|
||||
MessageLimit: c.Int64("message-limit"),
|
||||
MessageExpiryDuration: messageExpiryDuration,
|
||||
EmailLimit: c.Int64("email-limit"),
|
||||
CallLimit: c.Int64("call-limit"),
|
||||
ReservationLimit: c.Int64("reservation-limit"),
|
||||
AttachmentFileSizeLimit: attachmentFileSizeLimit,
|
||||
AttachmentTotalSizeLimit: attachmentTotalSizeLimit,
|
||||
@@ -230,7 +234,7 @@ func execTierAdd(c *cli.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(c.App.ErrWriter, "tier added\n\n")
|
||||
fmt.Fprintf(c.App.Writer, "tier added\n\n")
|
||||
printTier(c, tier)
|
||||
return nil
|
||||
}
|
||||
@@ -267,6 +271,9 @@ func execTierChange(c *cli.Context) error {
|
||||
if c.IsSet("email-limit") {
|
||||
tier.EmailLimit = c.Int64("email-limit")
|
||||
}
|
||||
if c.IsSet("call-limit") {
|
||||
tier.CallLimit = c.Int64("call-limit")
|
||||
}
|
||||
if c.IsSet("reservation-limit") {
|
||||
tier.ReservationLimit = c.Int64("reservation-limit")
|
||||
}
|
||||
@@ -308,7 +315,7 @@ func execTierChange(c *cli.Context) error {
|
||||
if err := manager.UpdateTier(tier); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(c.App.ErrWriter, "tier updated\n\n")
|
||||
fmt.Fprintf(c.App.Writer, "tier updated\n\n")
|
||||
printTier(c, tier)
|
||||
return nil
|
||||
}
|
||||
@@ -328,7 +335,7 @@ func execTierDel(c *cli.Context) error {
|
||||
if err := manager.RemoveTier(code); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(c.App.ErrWriter, "tier %s removed\n", code)
|
||||
fmt.Fprintf(c.App.Writer, "tier %s removed\n", code)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -352,15 +359,16 @@ func printTier(c *cli.Context, tier *user.Tier) {
|
||||
if tier.StripeMonthlyPriceID != "" && tier.StripeYearlyPriceID != "" {
|
||||
prices = fmt.Sprintf("%s / %s", tier.StripeMonthlyPriceID, tier.StripeYearlyPriceID)
|
||||
}
|
||||
fmt.Fprintf(c.App.ErrWriter, "tier %s (id: %s)\n", tier.Code, tier.ID)
|
||||
fmt.Fprintf(c.App.ErrWriter, "- Name: %s\n", tier.Name)
|
||||
fmt.Fprintf(c.App.ErrWriter, "- Message limit: %d\n", tier.MessageLimit)
|
||||
fmt.Fprintf(c.App.ErrWriter, "- Message expiry duration: %s (%d seconds)\n", tier.MessageExpiryDuration.String(), int64(tier.MessageExpiryDuration.Seconds()))
|
||||
fmt.Fprintf(c.App.ErrWriter, "- Email limit: %d\n", tier.EmailLimit)
|
||||
fmt.Fprintf(c.App.ErrWriter, "- Reservation limit: %d\n", tier.ReservationLimit)
|
||||
fmt.Fprintf(c.App.ErrWriter, "- Attachment file size limit: %s\n", util.FormatSize(tier.AttachmentFileSizeLimit))
|
||||
fmt.Fprintf(c.App.ErrWriter, "- Attachment total size limit: %s\n", util.FormatSize(tier.AttachmentTotalSizeLimit))
|
||||
fmt.Fprintf(c.App.ErrWriter, "- Attachment expiry duration: %s (%d seconds)\n", tier.AttachmentExpiryDuration.String(), int64(tier.AttachmentExpiryDuration.Seconds()))
|
||||
fmt.Fprintf(c.App.ErrWriter, "- Attachment daily bandwidth limit: %s\n", util.FormatSize(tier.AttachmentBandwidthLimit))
|
||||
fmt.Fprintf(c.App.ErrWriter, "- Stripe prices (monthly/yearly): %s\n", prices)
|
||||
fmt.Fprintf(c.App.Writer, "tier %s (id: %s)\n", tier.Code, tier.ID)
|
||||
fmt.Fprintf(c.App.Writer, "- Name: %s\n", tier.Name)
|
||||
fmt.Fprintf(c.App.Writer, "- Message limit: %d\n", tier.MessageLimit)
|
||||
fmt.Fprintf(c.App.Writer, "- Message expiry duration: %s (%d seconds)\n", tier.MessageExpiryDuration.String(), int64(tier.MessageExpiryDuration.Seconds()))
|
||||
fmt.Fprintf(c.App.Writer, "- Email limit: %d\n", tier.EmailLimit)
|
||||
fmt.Fprintf(c.App.Writer, "- Phone call limit: %d\n", tier.CallLimit)
|
||||
fmt.Fprintf(c.App.Writer, "- Reservation limit: %d\n", tier.ReservationLimit)
|
||||
fmt.Fprintf(c.App.Writer, "- Attachment file size limit: %s\n", util.FormatSizeHuman(tier.AttachmentFileSizeLimit))
|
||||
fmt.Fprintf(c.App.Writer, "- Attachment total size limit: %s\n", util.FormatSizeHuman(tier.AttachmentTotalSizeLimit))
|
||||
fmt.Fprintf(c.App.Writer, "- Attachment expiry duration: %s (%d seconds)\n", tier.AttachmentExpiryDuration.String(), int64(tier.AttachmentExpiryDuration.Seconds()))
|
||||
fmt.Fprintf(c.App.Writer, "- Attachment daily bandwidth limit: %s\n", util.FormatSizeHuman(tier.AttachmentBandwidthLimit))
|
||||
fmt.Fprintf(c.App.Writer, "- Stripe prices (monthly/yearly): %s\n", prices)
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ package cmd
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v2"
|
||||
"heckel.io/ntfy/server"
|
||||
"heckel.io/ntfy/test"
|
||||
"heckel.io/ntfy/v2/server"
|
||||
"heckel.io/ntfy/v2/test"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -12,21 +12,21 @@ func TestCLI_Tier_AddListChangeDelete(t *testing.T) {
|
||||
s, conf, port := newTestServerWithAuth(t)
|
||||
defer test.StopServer(t, s, port)
|
||||
|
||||
app, _, _, stderr := newTestApp()
|
||||
app, _, stdout, _ := newTestApp()
|
||||
require.Nil(t, runTierCommand(app, conf, "add", "--name", "Pro", "--message-limit", "1234", "pro"))
|
||||
require.Contains(t, stderr.String(), "tier added\n\ntier pro (id: ti_")
|
||||
require.Contains(t, stdout.String(), "tier added\n\ntier pro (id: ti_")
|
||||
|
||||
err := runTierCommand(app, conf, "add", "pro")
|
||||
require.NotNil(t, err)
|
||||
require.Equal(t, "tier pro already exists", err.Error())
|
||||
|
||||
app, _, _, stderr = newTestApp()
|
||||
app, _, stdout, _ = newTestApp()
|
||||
require.Nil(t, runTierCommand(app, conf, "list"))
|
||||
require.Contains(t, stderr.String(), "tier pro (id: ti_")
|
||||
require.Contains(t, stderr.String(), "- Name: Pro")
|
||||
require.Contains(t, stderr.String(), "- Message limit: 1234")
|
||||
require.Contains(t, stdout.String(), "tier pro (id: ti_")
|
||||
require.Contains(t, stdout.String(), "- Name: Pro")
|
||||
require.Contains(t, stdout.String(), "- Message limit: 1234")
|
||||
|
||||
app, _, _, stderr = newTestApp()
|
||||
app, _, stdout, _ = newTestApp()
|
||||
require.Nil(t, runTierCommand(app, conf, "change",
|
||||
"--message-limit=999",
|
||||
"--message-expiry-duration=2d",
|
||||
@@ -40,18 +40,18 @@ func TestCLI_Tier_AddListChangeDelete(t *testing.T) {
|
||||
"--stripe-yearly-price-id=price_992",
|
||||
"pro",
|
||||
))
|
||||
require.Contains(t, stderr.String(), "- Message limit: 999")
|
||||
require.Contains(t, stderr.String(), "- Message expiry duration: 48h")
|
||||
require.Contains(t, stderr.String(), "- Email limit: 91")
|
||||
require.Contains(t, stderr.String(), "- Reservation limit: 98")
|
||||
require.Contains(t, stderr.String(), "- Attachment file size limit: 100.0 MB")
|
||||
require.Contains(t, stderr.String(), "- Attachment expiry duration: 24h")
|
||||
require.Contains(t, stderr.String(), "- Attachment total size limit: 10.0 GB")
|
||||
require.Contains(t, stderr.String(), "- Stripe prices (monthly/yearly): price_991 / price_992")
|
||||
require.Contains(t, stdout.String(), "- Message limit: 999")
|
||||
require.Contains(t, stdout.String(), "- Message expiry duration: 48h")
|
||||
require.Contains(t, stdout.String(), "- Email limit: 91")
|
||||
require.Contains(t, stdout.String(), "- Reservation limit: 98")
|
||||
require.Contains(t, stdout.String(), "- Attachment file size limit: 100.0 MB")
|
||||
require.Contains(t, stdout.String(), "- Attachment expiry duration: 24h")
|
||||
require.Contains(t, stdout.String(), "- Attachment total size limit: 10.0 GB")
|
||||
require.Contains(t, stdout.String(), "- Stripe prices (monthly/yearly): price_991 / price_992")
|
||||
|
||||
app, _, _, stderr = newTestApp()
|
||||
app, _, stdout, _ = newTestApp()
|
||||
require.Nil(t, runTierCommand(app, conf, "remove", "pro"))
|
||||
require.Contains(t, stderr.String(), "tier pro removed")
|
||||
require.Contains(t, stdout.String(), "tier pro removed")
|
||||
}
|
||||
|
||||
func runTierCommand(app *cli.App, conf *server.Config, args ...string) error {
|
||||
|
||||
45
cmd/token.go
@@ -6,8 +6,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/urfave/cli/v2"
|
||||
"heckel.io/ntfy/user"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"net/netip"
|
||||
"time"
|
||||
)
|
||||
@@ -72,6 +72,15 @@ Example:
|
||||
This is a server-only command. It directly reads from user.db as defined in the server config
|
||||
file server.yml. The command only works if 'auth-file' is properly defined.`,
|
||||
},
|
||||
{
|
||||
Name: "generate",
|
||||
Usage: "Generates a random token",
|
||||
Action: execTokenGenerate,
|
||||
Description: `Randomly generate a token to be used in provisioned tokens.
|
||||
|
||||
This command only generates the token value, but does not persist it anywhere.
|
||||
The output can be used in the 'auth-tokens' config option.`,
|
||||
},
|
||||
},
|
||||
Description: `Manage access tokens for individual users.
|
||||
|
||||
@@ -112,19 +121,19 @@ func execTokenAdd(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
u, err := manager.User(username)
|
||||
if err == user.ErrUserNotFound {
|
||||
if errors.Is(err, user.ErrUserNotFound) {
|
||||
return fmt.Errorf("user %s does not exist", username)
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
token, err := manager.CreateToken(u.ID, label, expires, netip.IPv4Unspecified())
|
||||
token, err := manager.CreateToken(u.ID, label, expires, netip.IPv4Unspecified(), false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if expires.Unix() == 0 {
|
||||
fmt.Fprintf(c.App.ErrWriter, "token %s created for user %s, never expires\n", token.Value, u.Name)
|
||||
fmt.Fprintf(c.App.Writer, "token %s created for user %s, never expires\n", token.Value, u.Name)
|
||||
} else {
|
||||
fmt.Fprintf(c.App.ErrWriter, "token %s created for user %s, expires %v\n", token.Value, u.Name, expires.Format(time.UnixDate))
|
||||
fmt.Fprintf(c.App.Writer, "token %s created for user %s, expires %v\n", token.Value, u.Name, expires.Format(time.UnixDate))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -141,7 +150,7 @@ func execTokenDel(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
u, err := manager.User(username)
|
||||
if err == user.ErrUserNotFound {
|
||||
if errors.Is(err, user.ErrUserNotFound) {
|
||||
return fmt.Errorf("user %s does not exist", username)
|
||||
} else if err != nil {
|
||||
return err
|
||||
@@ -149,7 +158,7 @@ func execTokenDel(c *cli.Context) error {
|
||||
if err := manager.RemoveToken(u.ID, token); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(c.App.ErrWriter, "token %s for user %s removed\n", token, username)
|
||||
fmt.Fprintf(c.App.Writer, "token %s for user %s removed\n", token, username)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -165,7 +174,7 @@ func execTokenList(c *cli.Context) error {
|
||||
var users []*user.User
|
||||
if username != "" {
|
||||
u, err := manager.User(username)
|
||||
if err == user.ErrUserNotFound {
|
||||
if errors.Is(err, user.ErrUserNotFound) {
|
||||
return fmt.Errorf("user %s does not exist", username)
|
||||
} else if err != nil {
|
||||
return err
|
||||
@@ -183,15 +192,15 @@ func execTokenList(c *cli.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
} else if len(tokens) == 0 && username != "" {
|
||||
fmt.Fprintf(c.App.ErrWriter, "user %s has no access tokens\n", username)
|
||||
fmt.Fprintf(c.App.Writer, "user %s has no access tokens\n", username)
|
||||
return nil
|
||||
} else if len(tokens) == 0 {
|
||||
continue
|
||||
}
|
||||
usersWithTokens++
|
||||
fmt.Fprintf(c.App.ErrWriter, "user %s\n", u.Name)
|
||||
fmt.Fprintf(c.App.Writer, "user %s\n", u.Name)
|
||||
for _, t := range tokens {
|
||||
var label, expires string
|
||||
var label, expires, provisioned string
|
||||
if t.Label != "" {
|
||||
label = fmt.Sprintf(" (%s)", t.Label)
|
||||
}
|
||||
@@ -200,11 +209,19 @@ func execTokenList(c *cli.Context) error {
|
||||
} else {
|
||||
expires = fmt.Sprintf("expires %s", t.Expires.Format(time.RFC822))
|
||||
}
|
||||
fmt.Fprintf(c.App.ErrWriter, "- %s%s, %s, accessed from %s at %s\n", t.Value, label, expires, t.LastOrigin.String(), t.LastAccess.Format(time.RFC822))
|
||||
if t.Provisioned {
|
||||
provisioned = " (server config)"
|
||||
}
|
||||
fmt.Fprintf(c.App.Writer, "- %s%s, %s, accessed from %s at %s%s\n", t.Value, label, expires, t.LastOrigin.String(), t.LastAccess.Format(time.RFC822), provisioned)
|
||||
}
|
||||
}
|
||||
if usersWithTokens == 0 {
|
||||
fmt.Fprintf(c.App.ErrWriter, "no users with tokens\n")
|
||||
fmt.Fprintf(c.App.Writer, "no users with tokens\n")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func execTokenGenerate(c *cli.Context) error {
|
||||
fmt.Fprintln(c.App.Writer, user.GenerateToken())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v2"
|
||||
"heckel.io/ntfy/server"
|
||||
"heckel.io/ntfy/test"
|
||||
"heckel.io/ntfy/v2/server"
|
||||
"heckel.io/ntfy/v2/test"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
@@ -14,28 +14,28 @@ func TestCLI_Token_AddListRemove(t *testing.T) {
|
||||
s, conf, port := newTestServerWithAuth(t)
|
||||
defer test.StopServer(t, s, port)
|
||||
|
||||
app, stdin, _, stderr := newTestApp()
|
||||
app, stdin, stdout, _ := newTestApp()
|
||||
stdin.WriteString("mypass\nmypass")
|
||||
require.Nil(t, runUserCommand(app, conf, "add", "phil"))
|
||||
require.Contains(t, stderr.String(), "user phil added with role user")
|
||||
require.Contains(t, stdout.String(), "user phil added with role user")
|
||||
|
||||
app, _, _, stderr = newTestApp()
|
||||
app, _, stdout, _ = newTestApp()
|
||||
require.Nil(t, runTokenCommand(app, conf, "add", "phil"))
|
||||
require.Regexp(t, `token tk_.+ created for user phil, never expires`, stderr.String())
|
||||
require.Regexp(t, `token tk_.+ created for user phil, never expires`, stdout.String())
|
||||
|
||||
app, _, _, stderr = newTestApp()
|
||||
app, _, stdout, _ = newTestApp()
|
||||
require.Nil(t, runTokenCommand(app, conf, "list", "phil"))
|
||||
require.Regexp(t, `user phil\n- tk_.+, never expires, accessed from 0.0.0.0 at .+`, stderr.String())
|
||||
require.Regexp(t, `user phil\n- tk_.+, never expires, accessed from 0.0.0.0 at .+`, stdout.String())
|
||||
re := regexp.MustCompile(`tk_\w+`)
|
||||
token := re.FindString(stderr.String())
|
||||
token := re.FindString(stdout.String())
|
||||
|
||||
app, _, _, stderr = newTestApp()
|
||||
app, _, stdout, _ = newTestApp()
|
||||
require.Nil(t, runTokenCommand(app, conf, "remove", "phil", token))
|
||||
require.Regexp(t, fmt.Sprintf("token %s for user phil removed", token), stderr.String())
|
||||
require.Regexp(t, fmt.Sprintf("token %s for user phil removed", token), stdout.String())
|
||||
|
||||
app, _, _, stderr = newTestApp()
|
||||
app, _, stdout, _ = newTestApp()
|
||||
require.Nil(t, runTokenCommand(app, conf, "list"))
|
||||
require.Equal(t, "no users with tokens\n", stderr.String())
|
||||
require.Equal(t, "no users with tokens\n", stdout.String())
|
||||
}
|
||||
|
||||
func runTokenCommand(app *cli.App, conf *server.Config, args ...string) error {
|
||||
|
||||
109
cmd/user.go
@@ -6,13 +6,14 @@ import (
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"fmt"
|
||||
"heckel.io/ntfy/user"
|
||||
"heckel.io/ntfy/v2/server"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -25,7 +26,7 @@ func init() {
|
||||
|
||||
var flagsUser = append(
|
||||
append([]cli.Flag{}, flagsDefault...),
|
||||
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: defaultServerConfigFile, DefaultText: defaultServerConfigFile, Usage: "config file"},
|
||||
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: server.DefaultConfigFile, DefaultText: server.DefaultConfigFile, Usage: "config file"},
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"auth_file", "H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"auth_default_access", "p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}),
|
||||
)
|
||||
@@ -42,7 +43,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 +56,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 +81,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,10 +91,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.
|
||||
`,
|
||||
},
|
||||
{
|
||||
@@ -131,6 +133,22 @@ as messages per day, attachment file sizes, etc.
|
||||
Example:
|
||||
ntfy user change-tier phil pro # Change tier to "pro" for user "phil"
|
||||
ntfy user change-tier phil - # Remove tier from user "phil" entirely
|
||||
`,
|
||||
},
|
||||
{
|
||||
Name: "hash",
|
||||
Usage: "Create password hash for a predefined user",
|
||||
UsageText: "ntfy user hash",
|
||||
Action: execUserHash,
|
||||
Description: `Asks for a password and creates a bcrypt password hash.
|
||||
|
||||
This command is useful to create a password hash for a user, which can then be used
|
||||
for predefined users in the server config file, in auth-users.
|
||||
|
||||
Example:
|
||||
$ ntfy user hash
|
||||
(asks for password and confirmation)
|
||||
$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C
|
||||
`,
|
||||
},
|
||||
{
|
||||
@@ -174,7 +192,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 {
|
||||
@@ -188,7 +211,7 @@ func execUserAdd(c *cli.Context) error {
|
||||
}
|
||||
if user, _ := manager.User(username); user != nil {
|
||||
if c.Bool("ignore-exists") {
|
||||
fmt.Fprintf(c.App.ErrWriter, "user %s already exists (exited successfully)\n", username)
|
||||
fmt.Fprintf(c.App.Writer, "user %s already exists (exited successfully)\n", username)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("user %s already exists", username)
|
||||
@@ -198,13 +221,12 @@ func execUserAdd(c *cli.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
fmt.Fprintf(c.App.Writer, "user %s added with role %s\n", username, role)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -219,19 +241,23 @@ func execUserDel(c *cli.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := manager.User(username); err == user.ErrUserNotFound {
|
||||
if _, err := manager.User(username); errors.Is(err, user.ErrUserNotFound) {
|
||||
return fmt.Errorf("user %s does not exist", username)
|
||||
}
|
||||
if err := manager.RemoveUser(username); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(c.App.ErrWriter, "user %s removed\n", username)
|
||||
fmt.Fprintf(c.App.Writer, "user %s removed\n", username)
|
||||
return nil
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -241,7 +267,7 @@ func execUserChangePass(c *cli.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := manager.User(username); err == user.ErrUserNotFound {
|
||||
if _, err := manager.User(username); errors.Is(err, user.ErrUserNotFound) {
|
||||
return fmt.Errorf("user %s does not exist", username)
|
||||
}
|
||||
if password == "" {
|
||||
@@ -250,10 +276,10 @@ 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)
|
||||
fmt.Fprintf(c.App.Writer, "changed password for user %s\n", username)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -269,13 +295,26 @@ func execUserChangeRole(c *cli.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := manager.User(username); err == user.ErrUserNotFound {
|
||||
if _, err := manager.User(username); errors.Is(err, user.ErrUserNotFound) {
|
||||
return fmt.Errorf("user %s does not exist", username)
|
||||
}
|
||||
if err := manager.ChangeRole(username, role); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(c.App.ErrWriter, "changed role for user %s to %s\n", username, role)
|
||||
fmt.Fprintf(c.App.Writer, "changed role for user %s to %s\n", username, role)
|
||||
return nil
|
||||
}
|
||||
|
||||
func execUserHash(c *cli.Context) error {
|
||||
password, err := readPasswordAndConfirm(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hash, err := user.HashPassword(password)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to hash password: %w", err)
|
||||
}
|
||||
fmt.Fprintln(c.App.Writer, hash)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -293,19 +332,19 @@ func execUserChangeTier(c *cli.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := manager.User(username); err == user.ErrUserNotFound {
|
||||
if _, err := manager.User(username); errors.Is(err, user.ErrUserNotFound) {
|
||||
return fmt.Errorf("user %s does not exist", username)
|
||||
}
|
||||
if tier == tierReset {
|
||||
if err := manager.ResetTier(username); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(c.App.ErrWriter, "removed tier from user %s\n", username)
|
||||
fmt.Fprintf(c.App.Writer, "removed tier from user %s\n", username)
|
||||
} else {
|
||||
if err := manager.ChangeTier(username, tier); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(c.App.ErrWriter, "changed tier for user %s to %s\n", username, tier)
|
||||
fmt.Fprintf(c.App.Writer, "changed tier for user %s to %s\n", username, tier)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -335,7 +374,15 @@ func createUserManager(c *cli.Context) (*user.Manager, error) {
|
||||
if err != nil {
|
||||
return nil, errors.New("if set, auth-default-access must start set to 'read-write', 'read-only', 'write-only' or 'deny-all'")
|
||||
}
|
||||
return user.NewManager(authFile, authStartupQueries, authDefault, user.DefaultUserPasswordBcryptCost, user.DefaultUserStatsQueueWriterInterval)
|
||||
authConfig := &user.Config{
|
||||
Filename: authFile,
|
||||
StartupQueries: authStartupQueries,
|
||||
DefaultAccess: authDefault,
|
||||
ProvisionEnabled: false, // Hack: Do not re-provision users on manager initialization
|
||||
BcryptCost: user.DefaultUserPasswordBcryptCost,
|
||||
QueueWriterInterval: user.DefaultUserStatsQueueWriterInterval,
|
||||
}
|
||||
return user.NewManager(authConfig)
|
||||
}
|
||||
|
||||
func readPasswordAndConfirm(c *cli.Context) (string, error) {
|
||||
@@ -343,6 +390,8 @@ func readPasswordAndConfirm(c *cli.Context) (string, error) {
|
||||
password, err := util.ReadPassword(c.App.Reader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if len(password) == 0 {
|
||||
return "", errors.New("password cannot be empty")
|
||||
}
|
||||
fmt.Fprintf(c.App.ErrWriter, "\r%s\rconfirm: ", strings.Repeat(" ", 25))
|
||||
confirm, err := util.ReadPassword(c.App.Reader)
|
||||
|
||||
@@ -3,9 +3,9 @@ package cmd
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v2"
|
||||
"heckel.io/ntfy/server"
|
||||
"heckel.io/ntfy/test"
|
||||
"heckel.io/ntfy/user"
|
||||
"heckel.io/ntfy/v2/server"
|
||||
"heckel.io/ntfy/v2/test"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -15,20 +15,20 @@ func TestCLI_User_Add(t *testing.T) {
|
||||
s, conf, port := newTestServerWithAuth(t)
|
||||
defer test.StopServer(t, s, port)
|
||||
|
||||
app, stdin, _, stderr := newTestApp()
|
||||
app, stdin, stdout, _ := newTestApp()
|
||||
stdin.WriteString("mypass\nmypass")
|
||||
require.Nil(t, runUserCommand(app, conf, "add", "phil"))
|
||||
require.Contains(t, stderr.String(), "user phil added with role user")
|
||||
require.Contains(t, stdout.String(), "user phil added with role user")
|
||||
}
|
||||
|
||||
func TestCLI_User_Add_Exists(t *testing.T) {
|
||||
s, conf, port := newTestServerWithAuth(t)
|
||||
defer test.StopServer(t, s, port)
|
||||
|
||||
app, stdin, _, stderr := newTestApp()
|
||||
app, stdin, stdout, _ := newTestApp()
|
||||
stdin.WriteString("mypass\nmypass")
|
||||
require.Nil(t, runUserCommand(app, conf, "add", "phil"))
|
||||
require.Contains(t, stderr.String(), "user phil added with role user")
|
||||
require.Contains(t, stdout.String(), "user phil added with role user")
|
||||
|
||||
app, stdin, _, _ = newTestApp()
|
||||
stdin.WriteString("mypass\nmypass")
|
||||
@@ -41,10 +41,10 @@ func TestCLI_User_Add_Admin(t *testing.T) {
|
||||
s, conf, port := newTestServerWithAuth(t)
|
||||
defer test.StopServer(t, s, port)
|
||||
|
||||
app, stdin, _, stderr := newTestApp()
|
||||
app, stdin, stdout, _ := newTestApp()
|
||||
stdin.WriteString("mypass\nmypass")
|
||||
require.Nil(t, runUserCommand(app, conf, "add", "--role=admin", "phil"))
|
||||
require.Contains(t, stderr.String(), "user phil added with role admin")
|
||||
require.Contains(t, stdout.String(), "user phil added with role admin")
|
||||
}
|
||||
|
||||
func TestCLI_User_Add_Password_Mismatch(t *testing.T) {
|
||||
@@ -60,19 +60,27 @@ func TestCLI_User_Add_Password_Mismatch(t *testing.T) {
|
||||
|
||||
func TestCLI_User_ChangePass(t *testing.T) {
|
||||
s, conf, port := newTestServerWithAuth(t)
|
||||
conf.AuthUsers = []*user.User{
|
||||
{Name: "philuser", Hash: "$2a$10$U4WSIYY6evyGmZaraavM2e2JeVG6EMGUKN1uUwufUeeRd4Jpg6cGC", Role: user.RoleUser}, // philuser:philpass
|
||||
}
|
||||
defer test.StopServer(t, s, port)
|
||||
|
||||
// Add user
|
||||
app, stdin, _, stderr := newTestApp()
|
||||
app, stdin, stdout, _ := newTestApp()
|
||||
stdin.WriteString("mypass\nmypass")
|
||||
require.Nil(t, runUserCommand(app, conf, "add", "phil"))
|
||||
require.Contains(t, stderr.String(), "user phil added with role user")
|
||||
require.Contains(t, stdout.String(), "user phil added with role user")
|
||||
|
||||
// Change pass
|
||||
app, stdin, _, stderr = newTestApp()
|
||||
app, stdin, stdout, _ = newTestApp()
|
||||
stdin.WriteString("newpass\nnewpass")
|
||||
require.Nil(t, runUserCommand(app, conf, "change-pass", "phil"))
|
||||
require.Contains(t, stderr.String(), "changed password for user phil")
|
||||
require.Contains(t, stdout.String(), "changed password for user phil")
|
||||
|
||||
// Cannot change provisioned user's pass
|
||||
app, stdin, _, _ = newTestApp()
|
||||
stdin.WriteString("newpass\nnewpass")
|
||||
require.Error(t, runUserCommand(app, conf, "change-pass", "philuser"))
|
||||
}
|
||||
|
||||
func TestCLI_User_ChangeRole(t *testing.T) {
|
||||
@@ -80,15 +88,15 @@ func TestCLI_User_ChangeRole(t *testing.T) {
|
||||
defer test.StopServer(t, s, port)
|
||||
|
||||
// Add user
|
||||
app, stdin, _, stderr := newTestApp()
|
||||
app, stdin, stdout, _ := newTestApp()
|
||||
stdin.WriteString("mypass\nmypass")
|
||||
require.Nil(t, runUserCommand(app, conf, "add", "phil"))
|
||||
require.Contains(t, stderr.String(), "user phil added with role user")
|
||||
require.Contains(t, stdout.String(), "user phil added with role user")
|
||||
|
||||
// Change role
|
||||
app, _, _, stderr = newTestApp()
|
||||
app, _, stdout, _ = newTestApp()
|
||||
require.Nil(t, runUserCommand(app, conf, "change-role", "phil", "admin"))
|
||||
require.Contains(t, stderr.String(), "changed role for user phil to admin")
|
||||
require.Contains(t, stdout.String(), "changed role for user phil to admin")
|
||||
}
|
||||
|
||||
func TestCLI_User_Delete(t *testing.T) {
|
||||
@@ -96,15 +104,15 @@ func TestCLI_User_Delete(t *testing.T) {
|
||||
defer test.StopServer(t, s, port)
|
||||
|
||||
// Add user
|
||||
app, stdin, _, stderr := newTestApp()
|
||||
app, stdin, stdout, _ := newTestApp()
|
||||
stdin.WriteString("mypass\nmypass")
|
||||
require.Nil(t, runUserCommand(app, conf, "add", "phil"))
|
||||
require.Contains(t, stderr.String(), "user phil added with role user")
|
||||
require.Contains(t, stdout.String(), "user phil added with role user")
|
||||
|
||||
// Delete user
|
||||
app, _, _, stderr = newTestApp()
|
||||
app, _, stdout, _ = newTestApp()
|
||||
require.Nil(t, runUserCommand(app, conf, "del", "phil"))
|
||||
require.Contains(t, stderr.String(), "user phil removed")
|
||||
require.Contains(t, stdout.String(), "user phil removed")
|
||||
|
||||
// Delete user again (does not exist)
|
||||
app, _, _, _ = newTestApp()
|
||||
|
||||
69
cmd/webpush.go
Normal file
@@ -0,0 +1,69 @@
|
||||
//go:build !noserver && !nowebpush
|
||||
|
||||
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() {
|
||||
commands = append(commands, cmdWebPush)
|
||||
}
|
||||
|
||||
var cmdWebPush = &cli.Command{
|
||||
Name: "webpush",
|
||||
Usage: "Generate keys, in the future manage web push subscriptions",
|
||||
UsageText: "ntfy webpush [keys]",
|
||||
Category: categoryServer,
|
||||
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Action: generateWebPushKeys,
|
||||
Name: "keys",
|
||||
Usage: "Generate VAPID keys to enable browser background push notifications",
|
||||
UsageText: "ntfy webpush keys",
|
||||
Category: categoryServer,
|
||||
Flags: flagsWebPush,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func generateWebPushKeys(c *cli.Context) error {
|
||||
privateKey, publicKey, err := webpush.GenerateVAPIDKeys()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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.Writer, "Web Push keys written to %s.\n", outputFile)
|
||||
} else {
|
||||
_, err = fmt.Fprintf(c.App.Writer, `Web Push keys generated. Add the following lines to your config file:
|
||||
|
||||
web-push-public-key: %s
|
||||
web-push-private-key: %s
|
||||
web-push-file: /var/cache/ntfy/webpush.db # or similar
|
||||
web-push-email-address: <email address>
|
||||
|
||||
See https://ntfy.sh/docs/config/#web-push for details.
|
||||
`, publicKey, privateKey)
|
||||
}
|
||||
return err
|
||||
}
|
||||
34
cmd/webpush_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v2"
|
||||
"heckel.io/ntfy/v2/server"
|
||||
)
|
||||
|
||||
func TestCLI_WebPush_GenerateKeys(t *testing.T) {
|
||||
app, _, stdout, _ := newTestApp()
|
||||
require.Nil(t, runWebPushCommand(app, server.NewConfig(), "keys"))
|
||||
require.Contains(t, stdout.String(), "Web Push keys generated.")
|
||||
}
|
||||
|
||||
func TestCLI_WebPush_WriteKeysToFile(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
t.Chdir(tempDir)
|
||||
app, _, stdout, _ := newTestApp()
|
||||
require.Nil(t, runWebPushCommand(app, server.NewConfig(), "keys", "--output-file=key-file.yaml"))
|
||||
require.Contains(t, stdout.String(), "Web Push keys written to key-file.yaml")
|
||||
require.FileExists(t, filepath.Join(tempDir, "key-file.yaml"))
|
||||
}
|
||||
|
||||
func runWebPushCommand(app *cli.App, conf *server.Config, args ...string) error {
|
||||
webPushArgs := []string{
|
||||
"ntfy",
|
||||
"--log-level=ERROR",
|
||||
"webpush",
|
||||
}
|
||||
return app.Run(append(webPushArgs, args...))
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
version: "2.1"
|
||||
services:
|
||||
ntfy:
|
||||
image: binwiederhier/ntfy
|
||||
@@ -14,4 +13,3 @@ services:
|
||||
ports:
|
||||
- 80:80
|
||||
restart: unless-stopped
|
||||
|
||||
|
||||
@@ -32,11 +32,11 @@
|
||||
<path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41Z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
If you like ntfy, please consider sponsoring it via <a target="_blank" href="https://github.com/sponsors/binwiederhier"><strong>GitHub Sponsors</strong></a>
|
||||
If you like ntfy, please consider sponsoring me via <a target="_blank" href="https://github.com/sponsors/binwiederhier"><strong>GitHub Sponsors</strong></a>
|
||||
or <a target="_blank" href="https://en.liberapay.com/ntfy/"><strong>Liberapay</strong></a>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" role="img" viewBox="0 0 36 36" class="twemoji md-footer-custom-text">
|
||||
<path fill="#DD2E44" d="M35.885 11.833c0-5.45-4.418-9.868-9.867-9.868-3.308 0-6.227 1.633-8.018 4.129-1.791-2.496-4.71-4.129-8.017-4.129-5.45 0-9.868 4.417-9.868 9.868 0 .772.098 1.52.266 2.241C1.751 22.587 11.216 31.568 18 34.034c6.783-2.466 16.249-11.447 17.617-19.959.17-.721.268-1.469.268-2.242z"/>
|
||||
</svg>
|
||||
</svg>, or subscribing to <a target="_blank" href="https://ntfy.sh/app"><strong>ntfy Pro</strong></a>.
|
||||
<script>
|
||||
announceBarKey = 'announce-bar-closed-sponsor';
|
||||
document.getElementById('announce-bar-close').addEventListener('click', (e) => {
|
||||
|
||||
785
docs/config.md
46
docs/contact.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Contact
|
||||
|
||||
This service is run by [Philipp C. Heckel](https://heckel.io). There are several ways to get in touch with me and the
|
||||
ntfy community. Please choose the appropriate channel based on your needs.
|
||||
|
||||
## Support
|
||||
|
||||
### Community support
|
||||
|
||||
For general questions, feature discussions, and community help, please use one of these public channels:
|
||||
|
||||
| Channel | Link | Description |
|
||||
|-------------------|--------------------------------------------------------------------------------------|------------------------------------------------------------|
|
||||
| **Discord** | [discord.gg/cT7ECsZj9w](https://discord.gg/cT7ECsZj9w) | Real-time chat with the community (I'm `binwiederhier`) |
|
||||
| **Matrix** | [#ntfy:matrix.org](https://matrix.to/#/#ntfy:matrix.org) | Bridged from Discord, same community (I'm `binwiederhier`) |
|
||||
| **Matrix Space** | [#ntfy-space:matrix.org](https://matrix.to/#/#ntfy-space:matrix.org) | Matrix space with all ntfy rooms |
|
||||
| **GitHub Issues** | [github.com/binwiederhier/ntfy/issues](https://github.com/binwiederhier/ntfy/issues) | Bug reports and feature requests |
|
||||
|
||||
!!! info "Why public channels?"
|
||||
Answering questions in public channels benefits the entire community. Other users can learn from the
|
||||
discussion, and answers can be referenced later. This is much more scalable than 1-on-1 support.
|
||||
|
||||
### Paid support
|
||||
|
||||
If you are subscribed to a [ntfy Pro](https://ntfy.sh/#pricing) plan, you are entitled to priority support
|
||||
via the following channels:
|
||||
|
||||
| Channel | Contact | Description |
|
||||
|-----------------------|-----------------------------------------------------|------------------------------------------|
|
||||
| **General Support** | [support@mail.ntfy.sh](mailto:support@mail.ntfy.sh) | Direct email support for Pro subscribers |
|
||||
| **Billing Inquiries** | [billing@mail.ntfy.sh](mailto:support@mail.ntfy.sh) | Inquire about billing issues |
|
||||
| **Discord/Matrix** | Mention your Pro status | Priority responses in community channels |
|
||||
|
||||
Please include your ntfy.sh username when contacting support so we can verify your subscription status.
|
||||
|
||||
## Security issues
|
||||
|
||||
If you discover a security vulnerability, please report it responsibly via [security@mail.ntfy.sh](mailto:security@mail.ntfy.sh). See also: [SECURITY.md](https://github.com/binwiederhier/ntfy/blob/main/SECURITY.md).
|
||||
|
||||
## Other inquiries
|
||||
|
||||
For questions about our [privacy policy](privacy.md), data handling, or to exercise your data rights
|
||||
(access, deletion, etc.), please email [privacy@mail.ntfy.sh](mailto:privacy@mail.ntfy.sh).
|
||||
|
||||
For business inquiries, partnerships, press, or other general questions that don't fit the categories above, please
|
||||
use [contact@mail.ntfy.sh](mailto:contact@mail.ntfy.sh).
|
||||
43
docs/contributing.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Contributing
|
||||
|
||||
Thank you for your interest in contributing to ntfy! There are many ways to help, whether you're a developer,
|
||||
translator, or just an enthusiastic user.
|
||||
|
||||
## Code contributions
|
||||
|
||||
If you'd like to contribute code to ntfy:
|
||||
|
||||
1. Check out the [development guide](develop.md) to set up your environment
|
||||
2. Look at [open issues](https://github.com/binwiederhier/ntfy/issues) for ideas, or propose your own
|
||||
3. For larger features or architectural changes, please reach out on [Discord/Matrix](contact.md) first to discuss
|
||||
before investing significant time
|
||||
4. Submit a pull request on GitHub
|
||||
|
||||
All contributions are welcome, from small bug fixes to major features.
|
||||
|
||||
## Translations
|
||||
|
||||
Help make ntfy accessible to users around the world! We use Hosted Weblate for translations:
|
||||
|
||||
- **Weblate**: [hosted.weblate.org/projects/ntfy](https://hosted.weblate.org/projects/ntfy/)
|
||||
|
||||
You can start translating immediately without any coding knowledge.
|
||||
|
||||
## Documentation
|
||||
|
||||
Found a typo? Want to improve the docs? Documentation contributions are very welcome:
|
||||
|
||||
- Edit any page directly on GitHub using the edit button
|
||||
- Submit a pull request with your improvements
|
||||
|
||||
## Bug reports and feature requests
|
||||
|
||||
- **GitHub Issues**: [github.com/binwiederhier/ntfy/issues](https://github.com/binwiederhier/ntfy/issues)
|
||||
|
||||
Please search existing issues before creating a new one to avoid duplicates.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Please be respectful and constructive in all interactions. See the
|
||||
[Code of Conduct](https://github.com/binwiederhier/ntfy/blob/main/CODE_OF_CONDUCT.md) for details.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Deprecation notices
|
||||
# Deprecations and breaking changes
|
||||
This page is used to list deprecation notices for ntfy. Deprecated commands and options will be
|
||||
**removed after 1-3 months** from the time they were deprecated. How long the feature is deprecated
|
||||
before the behavior is changed depends on the severity of the change, and how prominent the feature is.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Hurray 🥳 🎉, you are interested in writing code for ntfy! **That's awesome.** 😎
|
||||
|
||||
I tried my very best to write up detailed instructions, but if at any point in time you run into issues, don't
|
||||
hesitate to **contact me on [Discord](https://discord.gg/cT7ECsZj9w) or [Matrix](https://matrix.to/#/#ntfy:matrix.org)**.
|
||||
hesitate to reach out via one of the channels listed on the [contact page](contact.md).
|
||||
|
||||
## ntfy server
|
||||
The ntfy server source code is available [on GitHub](https://github.com/binwiederhier/ntfy). The codebase for the
|
||||
@@ -16,7 +16,7 @@ server consists of three components:
|
||||
* **The documentation** is generated by [MkDocs](https://www.mkdocs.org/) and [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/),
|
||||
which is written in [Python](https://www.python.org/). You'll need Python and MkDocs (via `pip`) only if you want to
|
||||
build the docs.
|
||||
* **The web app** is written in [React](https://reactjs.org/), using [MUI](https://mui.com/). It uses [Create React App](https://create-react-app.dev/)
|
||||
* **The web app** is written in [React](https://reactjs.org/), using [MUI](https://mui.com/). It uses [Vite](https://vitejs.dev/)
|
||||
to build the production build. If you want to modify the web app, you need [nodejs](https://nodejs.org/en/) (for `npm`)
|
||||
and install all the 100,000 dependencies (*sigh*).
|
||||
|
||||
@@ -163,6 +163,15 @@ $ make release-snapshot
|
||||
|
||||
During development, you may want to be more picky and build only certain things. Here are a few examples.
|
||||
|
||||
### Build a Docker image only for Linux
|
||||
|
||||
This is useful to test the final build with web app, docs, and server without any dependencies locally
|
||||
|
||||
``` shell
|
||||
$ make docker-dev
|
||||
$ docker run --rm -p 80:80 binwiederhier/ntfy:dev serve
|
||||
```
|
||||
|
||||
### Build the ntfy binary
|
||||
To build only the `ntfy` binary **without the web app or documentation**, use the `make cli-...` targets:
|
||||
|
||||
@@ -232,6 +241,41 @@ $ cd web
|
||||
$ npm start
|
||||
```
|
||||
|
||||
### Testing Web Push locally
|
||||
|
||||
Reference: <https://stackoverflow.com/questions/34160509/options-for-testing-service-workers-via-http>
|
||||
|
||||
#### With the dev servers
|
||||
|
||||
1. Get web push keys `go run main.go webpush keys`
|
||||
|
||||
2. Run the server with web push enabled
|
||||
|
||||
```sh
|
||||
go run main.go \
|
||||
--log-level debug \
|
||||
serve \
|
||||
--web-push-public-key KEY \
|
||||
--web-push-private-key KEY \
|
||||
--web-push-email-address <email> \
|
||||
--web-push-file=/tmp/webpush.db
|
||||
```
|
||||
|
||||
3. In `web/public/config.js`:
|
||||
|
||||
- Set `base_url` to `http://localhost`, This is required as web push can only be used with the server matching the `base_url`.
|
||||
|
||||
- Set the `web_push_public_key` correctly.
|
||||
|
||||
4. Run `npm run start`
|
||||
|
||||
#### With a built package
|
||||
|
||||
1. Run `make web-build`
|
||||
|
||||
2. Run the server (step 2 above)
|
||||
|
||||
3. Open <http://localhost/>
|
||||
### Build the docs
|
||||
The sources for the docs live in `docs/`. Similarly to the web app, you can simply run `make docs` to build the
|
||||
documentation. As long as you have `mkdocs` installed (see above), this should work fine:
|
||||
@@ -319,7 +363,7 @@ To build your own version with Firebase, you must:
|
||||
* And change `app_base_url` in [values.xml](https://github.com/binwiederhier/ntfy-android/blob/main/app/src/main/res/values/values.xml)
|
||||
* Then run:
|
||||
```
|
||||
# To build an unsigned .apk (app/build/outputs/apk/play/*.apk)
|
||||
# To build an unsigned .apk (app/build/outputs/apk/play/release/*.apk)
|
||||
./gradlew assemblePlayRelease
|
||||
|
||||
# To build a bundle .aab (app/play/release/*.aab)
|
||||
@@ -340,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)
|
||||
@@ -385,7 +429,7 @@ steps:
|
||||
|
||||
### XCode setup
|
||||
|
||||
1. Follow step 4 of [https://firebase.google.com/docs/ios/setup](Add Firebase to your Apple project) to install the
|
||||
1. Follow step 4 of [Add Firebase to your Apple project](https://firebase.google.com/docs/ios/setup) to install the
|
||||
`firebase-ios-sdk` in XCode, if it's not already present - you can select any packages in addition to Firebase Core / Firebase Messaging
|
||||
1. Similarly, install the SQLite.swift package dependency in XCode
|
||||
1. When running the debug build, ensure XCode is pointed to the connected iOS device - registering for push notifications does not work in the iOS simulators
|
||||
@@ -397,6 +441,6 @@ To have instant notifications/better notification delivery when using firebase,
|
||||
1. In XCode, find the NTFY app target. **Not** the NSE app target.
|
||||
1. Find the Asset/ folder in the project navigator
|
||||
1. Drag the `GoogleService-Info.plist` file into the Asset/ folder that you get from the firebase console. It can be
|
||||
found in the "Project settings" > "General" > "Your apps" with a button labled "GoogleService-Info.plist"
|
||||
found in the "Project settings" > "General" > "Your apps" with a button labeled "GoogleService-Info.plist"
|
||||
|
||||
After that, you should be all set!
|
||||
|
||||
3640
docs/emojis.md
113
docs/examples.md
@@ -16,7 +16,7 @@ I started adding notifications pretty much all of my scripts. Typically, I just
|
||||
directly to the command I'm running. The following example will either send <i>Laptop backup succeeded</i>
|
||||
or ⚠️ <i>Laptop backup failed</i> directly to my phone:
|
||||
|
||||
```
|
||||
``` bash
|
||||
rsync -a root@laptop /backups/laptop \
|
||||
&& zfs snapshot ... \
|
||||
&& curl -H prio:low -d "Laptop backup succeeded" ntfy.sh/backups \
|
||||
@@ -26,11 +26,17 @@ rsync -a root@laptop /backups/laptop \
|
||||
Here's one for the history books. I desperately want the `github.com/ntfy` organization, but all my tickets with
|
||||
GitHub have been hopeless. In case it ever becomes available, I want to know immediately.
|
||||
|
||||
``` cron
|
||||
```
|
||||
# Check github/ntfy user
|
||||
*/6 * * * * if curl -s https://api.github.com/users/ntfy | grep "Not Found"; then curl -d "github.com/ntfy is available" -H "Tags: tada" -H "Prio: high" ntfy.sh/my-alerts; fi
|
||||
```
|
||||
|
||||
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
|
||||
@@ -135,28 +141,64 @@ You can send a message during a workflow run with curl. Here is an example sendi
|
||||
${{ secrets.NTFY_URL }}
|
||||
```
|
||||
|
||||
## Changedetection.io
|
||||
ntfy is an excellent choice for getting notifications when a website has a change sent to your mobile (or desktop),
|
||||
[changedetection.io](https://changedetection.io) or on GitHub ([dgtlmoon/changedetection.io](https://github.com/dgtlmoon/changedetection.io))
|
||||
uses [apprise](https://github.com/caronc/apprise) library for notification integrations.
|
||||
|
||||
To add any ntfy(s) notification to a website change simply add the [ntfy style URL](https://github.com/caronc/apprise/wiki/Notify_ntfy)
|
||||
to the notification list.
|
||||
|
||||
For example `ntfy://{topic}` or `ntfy://{user}:{password}@{host}:{port}/{topics}`
|
||||
|
||||
In your changedetection.io installation, click `Edit` > `Notifications` on a single website watch (or group) then add
|
||||
the special ntfy Apprise Notification URL to the Notification List.
|
||||
|
||||

|
||||
|
||||
## Watchtower (shoutrrr)
|
||||
You can use [shoutrrr](https://github.com/containrrr/shoutrrr) generic webhook support to send
|
||||
You can use [shoutrrr](https://containrrr.dev/shoutrrr/latest/services/ntfy/) to send
|
||||
[Watchtower](https://github.com/containrrr/watchtower/) notifications to your ntfy topic.
|
||||
|
||||
Example docker-compose.yml:
|
||||
|
||||
``` yaml
|
||||
services:
|
||||
watchtower:
|
||||
image: containrrr/watchtower
|
||||
environment:
|
||||
- WATCHTOWER_NOTIFICATIONS=shoutrrr
|
||||
- WATCHTOWER_NOTIFICATION_URL=generic+https://ntfy.sh/my_watchtower_topic?title=WatchtowerUpdates
|
||||
- WATCHTOWER_NOTIFICATION_SKIP_TITLE=True
|
||||
- WATCHTOWER_NOTIFICATION_URL=ntfy://ntfy.sh/my_watchtower_topic?title=WatchtowerUpdates
|
||||
```
|
||||
|
||||
The environment variable `WATCHTOWER_NOTIFICATION_SKIP_TITLE` is required to prevent Watchtower from [replacing the `title` query parameter](https://containrrr.dev/watchtower/notifications/#settings). If omitted, the provided notification title will not be used.
|
||||
|
||||
Or, if you only want to send notifications using shoutrrr:
|
||||
```
|
||||
shoutrrr send -u "generic+https://ntfy.sh/my_watchtower_topic?title=WatchtowerUpdates" -m "testMessage"
|
||||
shoutrrr send -u "ntfy://ntfy.sh/my_watchtower_topic?title=WatchtowerUpdates" -m "testMessage"
|
||||
```
|
||||
|
||||
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`
|
||||
```
|
||||
|
||||
## Sonarr, Radarr, Lidarr, Readarr, Prowlarr, SABnzbd
|
||||
It's possible to use custom scripts for all the *arr services, plus SABnzbd. Notifications for downloads, warnings, grabs etc.
|
||||
Some simple bash scripts to achieve this are kindly provided in [nickexyz's repository](https://github.com/nickexyz/ntfy-shellscripts).
|
||||
|
||||
<!-- Sonarr v4 is in beta as of May 2023, should be updated to remove v3 reference when stable -->
|
||||
|
||||
Radarr, Prowlarr, and Sonarr v4 support ntfy natively under Settings > Connect.
|
||||
|
||||
Sonarr v3, Readarr, and SABnzbd support custom scripts for downloads, warnings, grabs, etc.
|
||||
Some simple bash scripts to achieve this are kindly provided in [nickexyz's ntfy-shellscripts repository](https://github.com/nickexyz/ntfy-shellscripts).
|
||||
|
||||
## Node-RED
|
||||
You can use the HTTP request node to send messages with [Node-RED](https://nodered.org), some examples:
|
||||
@@ -577,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>
|
||||
@@ -596,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
|
||||
```
|
||||

|
||||
|
||||
**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
|
||||
```
|
||||
28
docs/faq.md
@@ -43,9 +43,9 @@ of the app and [self-host your own ntfy server](install.md).
|
||||
## How much battery does the Android app use?
|
||||
If you use the ntfy.sh server, and you don't use the [instant delivery](subscribe/phone.md#instant-delivery) feature,
|
||||
the Android/iOS app uses no additional battery, since Firebase Cloud Messaging (FCM) is used. If you use your own server,
|
||||
or you use *instant delivery* (Android only), the app has to maintain a constant connection to the server, which consumes
|
||||
about 0-1% of battery in 17h of use (on my phone). There has been a ton of testing and improvement around this. I think it's pretty
|
||||
decent now.
|
||||
or you use *instant delivery* (Android only), or install from F-droid ([which does not support FCM](https://f-droid.org/docs/Inclusion_Policy/)),
|
||||
the app has to maintain a constant connection to the server, which consumes about 0-1% of battery in 17h of use (on my phone).
|
||||
There has been a ton of testing and improvement around this. I think it's pretty decent now.
|
||||
|
||||
## Paid plans? I thought it was open source?
|
||||
All of ntfy will remain open source, with a free software license (Apache 2.0 and GPLv2). If you'd like to self-host, you
|
||||
@@ -76,7 +76,29 @@ However, if you still want to disable it, you can do so with the `web-root: disa
|
||||
Think of the ntfy web app like an Android/iOS app. It is freely available and accessible to anyone, yet useless without
|
||||
a proper backend. So as long as you secure your backend with ACLs, exposing the ntfy web app to the Internet is harmless.
|
||||
|
||||
## If topic names are public, could I not just brute force them?
|
||||
If you don't have [ACLs set up](config.md#access-control), the topic name is your password, it says so everywhere. If you
|
||||
choose a easy-to-guess/dumb topic name, people will be able to guess it. If you choose a randomly generated topic name,
|
||||
the topic is as good as a good password.
|
||||
|
||||
As for brute forcing: It's not possible to brute force a ntfy server for very long, as you'll get quickly rate limited.
|
||||
In the default configuration, you'll be able to do 60 requests as a burst, and then 1 request per 10 seconds. Assuming you
|
||||
choose a random 10 digit topic name using only A-Z, a-z, 0-9, _ and -, there are 64^10 possible topic names. Even if you
|
||||
could do hundreds of requests per seconds (which you cannot), it would take many years to brute force a topic name.
|
||||
|
||||
For ntfy.sh, there's even a fail2ban in place which will ban your IP pretty quickly.
|
||||
|
||||
## Where can I donate?
|
||||
I have just very recently started accepting donations via [GitHub Sponsors](https://github.com/sponsors/binwiederhier).
|
||||
I would be humbled if you helped me carry the server and developer account costs. Even small donations are very much
|
||||
appreciated.
|
||||
|
||||
## Can I email you? Can I DM you on Discord/Matrix?
|
||||
For community support, please use the public channels listed on the [contact page](contact.md). I generally
|
||||
**do not respond to direct messages** about ntfy, unless you are paying for a [ntfy Pro](https://ntfy.sh/#pricing)
|
||||
plan (see [paid support](contact.md#paid-support)), or you are inquiring about business
|
||||
opportunities (see [other inquiries](contact.md#other-inquiries)).
|
||||
|
||||
I am sorry, but answering individual questions about ntfy on a 1-on-1 basis is not scalable. Answering your questions
|
||||
in public forums benefits others, since I can link to the discussion at a later point in time, or other users
|
||||
may be able to help out. I hope you understand.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
import shutil
|
||||
|
||||
def copy_fonts(config, **kwargs):
|
||||
site_dir = config['site_dir']
|
||||
shutil.copytree('docs/static/fonts', os.path.join(site_dir, 'get'))
|
||||
|
||||
def on_post_build(config, **kwargs):
|
||||
site_dir = config["site_dir"]
|
||||
shutil.copytree("docs/static/fonts", os.path.join(site_dir, "get"))
|
||||
|
||||
@@ -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.**
|
||||
|
||||
139
docs/install.md
@@ -14,62 +14,72 @@ We support amd64, armv7 and arm64.
|
||||
|
||||
1. Install ntfy using one of the methods described below
|
||||
2. Then (optionally) edit `/etc/ntfy/server.yml` for the server (Linux only, see [configuration](config.md) or [sample server.yml](https://github.com/binwiederhier/ntfy/blob/main/server/server.yml))
|
||||
3. Or (optionally) create/edit `~/.config/ntfy/client.yml` (for the non-root user) or `/etc/ntfy/client.yml` (for the root user), see [sample client.yml](https://github.com/binwiederhier/ntfy/blob/main/client/client.yml))
|
||||
3. Or (optionally) create/edit `~/.config/ntfy/client.yml` (for the non-root user), `~/Library/Application Support/ntfy/client.yml` (for the macOS non-root user), or `/etc/ntfy/client.yml` (for the root user), see [sample client.yml](https://github.com/binwiederhier/ntfy/blob/main/client/client.yml))
|
||||
|
||||
To run the ntfy server, then just run `ntfy serve` (or `systemctl start ntfy` when using the deb/rpm).
|
||||
To send messages, use `ntfy publish`. To subscribe to topics, use `ntfy subscribe` (see [subscribing via CLI](subscribe/cli.md)
|
||||
for details).
|
||||
|
||||
If you like tutorials, check out :simple-youtube: [Kris Occhipinti's ntfy install guide](https://www.youtube.com/watch?v=bZzqrX05mNU) on YouTube, or
|
||||
[Alex's Docker-based setup guide](https://blog.alexsguardian.net/posts/2023/09/12/selfhosting-ntfy/). Both are great
|
||||
resources to get started. _I am not affiliated with Kris or Alex, I just liked their video/post._
|
||||
|
||||
## Linux binaries
|
||||
Please check out the [releases page](https://github.com/binwiederhier/ntfy/releases) for binaries and
|
||||
deb/rpm packages.
|
||||
|
||||
=== "x86_64/amd64"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.2.0/ntfy_2.2.0_linux_x86_64.tar.gz
|
||||
tar zxvf ntfy_2.2.0_linux_x86_64.tar.gz
|
||||
sudo cp -a ntfy_2.2.0_linux_x86_64/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.2.0_linux_x86_64/{client,server}/*.yml /etc/ntfy
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_amd64.tar.gz
|
||||
tar zxvf ntfy_2.15.0_linux_amd64.tar.gz
|
||||
sudo cp -a ntfy_2.15.0_linux_amd64/ntfy /usr/local/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.15.0_linux_amd64/{client,server}/*.yml /etc/ntfy
|
||||
sudo ntfy serve
|
||||
```
|
||||
|
||||
=== "armv6"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.2.0/ntfy_2.2.0_linux_armv6.tar.gz
|
||||
tar zxvf ntfy_2.2.0_linux_armv6.tar.gz
|
||||
sudo cp -a ntfy_2.2.0_linux_armv6/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.2.0_linux_armv6/{client,server}/*.yml /etc/ntfy
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_armv6.tar.gz
|
||||
tar zxvf ntfy_2.15.0_linux_armv6.tar.gz
|
||||
sudo cp -a ntfy_2.15.0_linux_armv6/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.15.0_linux_armv6/{client,server}/*.yml /etc/ntfy
|
||||
sudo ntfy serve
|
||||
```
|
||||
|
||||
=== "armv7/armhf"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.2.0/ntfy_2.2.0_linux_armv7.tar.gz
|
||||
tar zxvf ntfy_2.2.0_linux_armv7.tar.gz
|
||||
sudo cp -a ntfy_2.2.0_linux_armv7/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.2.0_linux_armv7/{client,server}/*.yml /etc/ntfy
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_armv7.tar.gz
|
||||
tar zxvf ntfy_2.15.0_linux_armv7.tar.gz
|
||||
sudo cp -a ntfy_2.15.0_linux_armv7/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.15.0_linux_armv7/{client,server}/*.yml /etc/ntfy
|
||||
sudo ntfy serve
|
||||
```
|
||||
|
||||
=== "arm64"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.2.0/ntfy_2.2.0_linux_arm64.tar.gz
|
||||
tar zxvf ntfy_2.2.0_linux_arm64.tar.gz
|
||||
sudo cp -a ntfy_2.2.0_linux_arm64/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.2.0_linux_arm64/{client,server}/*.yml /etc/ntfy
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_arm64.tar.gz
|
||||
tar zxvf ntfy_2.15.0_linux_arm64.tar.gz
|
||||
sudo cp -a ntfy_2.15.0_linux_arm64/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.15.0_linux_arm64/{client,server}/*.yml /etc/ntfy
|
||||
sudo ntfy serve
|
||||
```
|
||||
|
||||
## Debian/Ubuntu repository
|
||||
Installation via Debian repository:
|
||||
|
||||
!!! info
|
||||
As of September 2025, **the official ntfy.sh Debian/Ubuntu repository has moved to [archive.ntfy.sh](https://archive.ntfy.sh/apt)**.
|
||||
The old repository [archive.heckel.io](https://archive.heckel.io/apt) is still available for now, but will likely
|
||||
go away soon. I suspect I will phase it out some time in early 2026.
|
||||
|
||||
Installation via Debian/Ubuntu repository (fingerprint `55BA 774A 6F5E E674 31E4 6B7C CFDB 962D 4F1E C4AF`):
|
||||
|
||||
=== "x86_64/amd64"
|
||||
```bash
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
curl -fsSL https://archive.heckel.io/apt/pubkey.txt | sudo gpg --dearmor -o /etc/apt/keyrings/archive.heckel.io.gpg
|
||||
sudo curl -L -o /etc/apt/keyrings/ntfy.gpg https://archive.ntfy.sh/apt/keyring.gpg
|
||||
sudo apt install apt-transport-https
|
||||
sudo sh -c "echo 'deb [arch=amd64 signed-by=/etc/apt/keyrings/archive.heckel.io.gpg] https://archive.heckel.io/apt debian main' \
|
||||
> /etc/apt/sources.list.d/archive.heckel.io.list"
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/ntfy.gpg] https://archive.ntfy.sh/apt stable main" \
|
||||
| sudo tee /etc/apt/sources.list.d/ntfy.list
|
||||
sudo apt update
|
||||
sudo apt install ntfy
|
||||
sudo systemctl enable ntfy
|
||||
@@ -79,10 +89,10 @@ Installation via Debian repository:
|
||||
=== "armv7/armhf"
|
||||
```bash
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
curl -fsSL https://archive.heckel.io/apt/pubkey.txt | sudo gpg --dearmor -o /etc/apt/keyrings/archive.heckel.io.gpg
|
||||
sudo curl -L -o /etc/apt/keyrings/ntfy.gpg https://archive.ntfy.sh/apt/keyring.gpg
|
||||
sudo apt install apt-transport-https
|
||||
sudo sh -c "echo 'deb [arch=armhf signed-by=/etc/apt/keyrings/archive.heckel.io.gpg] https://archive.heckel.io/apt debian main' \
|
||||
> /etc/apt/sources.list.d/archive.heckel.io.list"
|
||||
echo "deb [arch=armhf signed-by=/etc/apt/keyrings/ntfy.gpg] https://archive.ntfy.sh/apt stable main" \
|
||||
| sudo tee /etc/apt/sources.list.d/ntfy.list
|
||||
sudo apt update
|
||||
sudo apt install ntfy
|
||||
sudo systemctl enable ntfy
|
||||
@@ -92,10 +102,10 @@ Installation via Debian repository:
|
||||
=== "arm64"
|
||||
```bash
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
curl -fsSL https://archive.heckel.io/apt/pubkey.txt | sudo gpg --dearmor -o /etc/apt/keyrings/archive.heckel.io.gpg
|
||||
sudo curl -L -o /etc/apt/keyrings/ntfy.gpg https://archive.ntfy.sh/apt/keyring.gpg
|
||||
sudo apt install apt-transport-https
|
||||
sudo sh -c "echo 'deb [arch=arm64 signed-by=/etc/apt/keyrings/archive.heckel.io.gpg] https://archive.heckel.io/apt debian main' \
|
||||
> /etc/apt/sources.list.d/archive.heckel.io.list"
|
||||
echo "deb [arch=arm64 signed-by=/etc/apt/keyrings/ntfy.gpg] https://archive.ntfy.sh/apt stable main" \
|
||||
| sudo tee /etc/apt/sources.list.d/ntfy.list
|
||||
sudo apt update
|
||||
sudo apt install ntfy
|
||||
sudo systemctl enable ntfy
|
||||
@@ -106,7 +116,7 @@ Manually installing the .deb file:
|
||||
|
||||
=== "x86_64/amd64"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.2.0/ntfy_2.2.0_linux_amd64.deb
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_amd64.deb
|
||||
sudo dpkg -i ntfy_*.deb
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
@@ -114,7 +124,7 @@ Manually installing the .deb file:
|
||||
|
||||
=== "armv6"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.2.0/ntfy_2.2.0_linux_armv6.deb
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_armv6.deb
|
||||
sudo dpkg -i ntfy_*.deb
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
@@ -122,7 +132,7 @@ Manually installing the .deb file:
|
||||
|
||||
=== "armv7/armhf"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.2.0/ntfy_2.2.0_linux_armv7.deb
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_armv7.deb
|
||||
sudo dpkg -i ntfy_*.deb
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
@@ -130,7 +140,7 @@ Manually installing the .deb file:
|
||||
|
||||
=== "arm64"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.2.0/ntfy_2.2.0_linux_arm64.deb
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_arm64.deb
|
||||
sudo dpkg -i ntfy_*.deb
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
@@ -140,34 +150,36 @@ Manually installing the .deb file:
|
||||
|
||||
=== "x86_64/amd64"
|
||||
```bash
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.2.0/ntfy_2.2.0_linux_amd64.rpm
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.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.2.0/ntfy_2.2.0_linux_armv6.rpm
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.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.2.0/ntfy_2.2.0_linux_armv7.rpm
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.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.2.0/ntfy_2.2.0_linux_arm64.rpm
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_arm64.rpm
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
```
|
||||
|
||||
## Arch Linux
|
||||
ntfy can be installed using an [AUR package](https://aur.archlinux.org/packages/ntfysh-bin/). You can use an [AUR helper](https://wiki.archlinux.org/title/AUR_helpers) like `paru`, `yay` or others to download, build and install ntfy and keep it up to date.
|
||||
ntfy can be installed using an [AUR package](https://aur.archlinux.org/packages/ntfysh-bin/).
|
||||
You can use an [AUR helper](https://wiki.archlinux.org/title/AUR_helpers) like `paru`, `yay` or others to download,
|
||||
build and install ntfy and keep it up to date.
|
||||
```
|
||||
paru -S ntfysh-bin
|
||||
```
|
||||
@@ -189,41 +201,56 @@ 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.2.0/ntfy_2.2.0_macOS_all.tar.gz),
|
||||
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.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.2.0/ntfy_2.2.0_macOS_all.tar.gz > ntfy_2.2.0_macOS_all.tar.gz
|
||||
tar zxvf ntfy_2.2.0_macOS_all.tar.gz
|
||||
sudo cp -a ntfy_2.2.0_macOS_all/ntfy /usr/local/bin/ntfy
|
||||
curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_darwin_all.tar.gz > ntfy_2.15.0_darwin_all.tar.gz
|
||||
tar zxvf ntfy_2.15.0_darwin_all.tar.gz
|
||||
sudo cp -a ntfy_2.15.0_darwin_all/ntfy /usr/local/bin/ntfy
|
||||
mkdir ~/Library/Application\ Support/ntfy
|
||||
cp ntfy_2.2.0_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
|
||||
cp ntfy_2.15.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
|
||||
ntfy --help
|
||||
```
|
||||
|
||||
!!! info
|
||||
There is a [GitHub issue](https://github.com/binwiederhier/ntfy/issues/286) about making ntfy installable via
|
||||
[Homebrew](https://brew.sh/). I'll eventually get to that, but I'd also love if somebody else stepped up to do it.
|
||||
Also, you can build and run the ntfy server on macOS as well, though I don't officially support that.
|
||||
Check out the [build instructions](develop.md) for details.
|
||||
Only the ntfy CLI is supported on macOS. ntfy server is currently not supported, but you can build and run it for
|
||||
development as well. Check out the [build instructions](develop.md) for details.
|
||||
|
||||
## Homebrew
|
||||
To install the [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) via Homebrew (Linux and macOS),
|
||||
simply run:
|
||||
```
|
||||
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.2.0/ntfy_2.2.0_windows_x86_64.zip),
|
||||
The ntfy server and CLI are fully supported on Windows. You can run the ntfy server directly or as a Windows service.
|
||||
To install, you can either
|
||||
|
||||
* [Download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_windows_amd64.zip),
|
||||
extract it and place the `ntfy.exe` binary somewhere in your `%Path%`.
|
||||
* Or install ntfy from the [Scoop](https://scoop.sh) main repository via `scoop install ntfy`
|
||||
|
||||
The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file).
|
||||
Once installed, you can run the ntfy CLI commands like so:
|
||||
|
||||
Also available in [Scoop's](https://scoop.sh) Main repository:
|
||||
```
|
||||
ntfy.exe -h
|
||||
```
|
||||
|
||||
`scoop install ntfy`
|
||||
The default configuration file location on Windows is `%ProgramData%\ntfy\server.yml` (e.g., `C:\ProgramData\ntfy\server.yml`)
|
||||
for the server, and `%AppData%\ntfy\client.yml` for the client. You may need to create the directory and config file manually.
|
||||
|
||||
!!! info
|
||||
There is currently no installer for Windows, and the binary is not signed. If this is desired, please create a
|
||||
[GitHub issue](https://github.com/binwiederhier/ntfy/issues) to let me know.
|
||||
To install the ntfy server as a Windows service, you can use the built-in `sc` command. For example, run this in an
|
||||
elevated command prompt (adjust the path to `ntfy.exe` accordingly):
|
||||
|
||||
```
|
||||
sc create ntfy binPath="C:\path\to\ntfy.exe serve" start=auto
|
||||
sc start ntfy
|
||||
```
|
||||
|
||||
## Docker
|
||||
The [ntfy image](https://hub.docker.com/r/binwiederhier/ntfy) is available for amd64, armv6, armv7 and arm64. It should
|
||||
@@ -268,8 +295,6 @@ docker run \
|
||||
|
||||
Using docker-compose with non-root user and healthchecks enabled:
|
||||
```yaml
|
||||
version: "2.1"
|
||||
|
||||
services:
|
||||
ntfy:
|
||||
image: binwiederhier/ntfy
|
||||
@@ -291,6 +316,7 @@ services:
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
restart: unless-stopped
|
||||
init: true # needed, if healthcheck is used. Prevents zombie processes
|
||||
```
|
||||
|
||||
If using a non-root user when running the docker version, be sure to chown the server.yml, user.db, and cache.db files and attachments directory to the same uid/gid.
|
||||
@@ -309,7 +335,6 @@ The setup for Kubernetes is very similar to that for Docker, and requires a fair
|
||||
are a few options to mix and match, including a deployment without a cache file, a stateful set with a persistent cache, and a standalone
|
||||
unmanned pod.
|
||||
|
||||
|
||||
=== "deployment"
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
@@ -528,7 +553,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
|
||||
|
||||
@@ -4,25 +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.
|
||||
|
||||
## Public ntfy servers
|
||||
## Table of Contents
|
||||
|
||||
Here's a list of public ntfy servers. As of right now, there is only one official server. The others are provided by the
|
||||
ntfy community. Thanks to everyone running a public server. **You guys rock!**
|
||||
|
||||
| URL | Country |
|
||||
|---------------------------------------------------|--------------------|
|
||||
| [ntfy.sh](https://ntfy.sh/) (*Official*) | 🇺🇸 United States |
|
||||
| [ntfy.tedomum.net](https://ntfy.tedomum.net/) | 🇫🇷 France |
|
||||
| [ntfy.jae.fi](https://ntfy.jae.fi/) | 🇫🇮 Finland |
|
||||
| [ntfy.adminforge.de](https://ntfy.adminforge.de/) | 🇩🇪 Germany |
|
||||
| [ntfy.envs.net](https://ntfy.envs.net) | 🇩🇪 Germany |
|
||||
| [ntfy.mzte.de](https://ntfy.mzte.de/) | 🇩🇪 Germany |
|
||||
|
||||
Please be aware that **server operators can log your messages**. The project also cannot guarantee the reliability
|
||||
and uptime of third party servers, so use of each server is **at your own discretion**.
|
||||
- [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
|
||||
@@ -33,11 +29,31 @@ and uptime of third party servers, so use of each server is **at your own discre
|
||||
- [Gatus](https://gatus.io/) ⭐ - Automated service health dashboard
|
||||
- [Automatisch](https://automatisch.io/) ⭐ - Open source Zapier alternative / workflow automation tool
|
||||
- [FlexGet](https://flexget.com/Plugins/Notifiers/ntfysh) ⭐ - Multipurpose automation tool for all of your media
|
||||
- [Shoutrrr](https://containrrr.dev/shoutrrr/v0.7/services/ntfy/) ⭐ - Notification library for gophers and their furry friends.
|
||||
- [Shoutrrr](https://containrrr.dev/shoutrrr/v0.8/services/ntfy/) ⭐ - Notification library for gophers and their furry friends.
|
||||
- [Netdata](https://learn.netdata.cloud/docs/alerts-and-notifications/notifications/agent-alert-notifications/ntfy) ⭐ - Real-time performance monitoring
|
||||
- [Deployer](https://github.com/deployphp/deployer) ⭐ - PHP deployment tool
|
||||
- [Scrt.link](https://scrt.link/) - Share a secret
|
||||
- [Platypush](https://docs.platypush.tech/platypush/plugins/ntfy.html) - Automation platform aimed to run on any device that can run Python
|
||||
- [diun](https://crazymax.dev/diun/) - Docker Image Update Notifier
|
||||
- [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
|
||||
- [Simple Observability](https://simpleobservability.com/docs/alerts/ntfy) - Server monitoring and observability 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#))
|
||||
- [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 +77,27 @@ and uptime of third party servers, so use of each server is **at your own discre
|
||||
- [ntfy](https://github.com/jonocarroll/ntfy) - Wraps the ntfy API with pipe-friendly tooling (R)
|
||||
- [ntfy-for-delphi](https://github.com/hazzelnuts/ntfy-for-delphi) - A friendly library to push instant notifications ntfy (Delphi)
|
||||
- [ntfy](https://github.com/ffflorian/ntfy) - Send notifications over ntfy (JS)
|
||||
- [ntfy_dart](https://github.com/jr1221/ntfy_dart) - Dart wrapper around the ntfy API (Dart)
|
||||
- [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
|
||||
- [ntfysh-windows](https://github.com/mshafer1/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
|
||||
- [Ntfy Desktop](https://github.com/emmaexe/ntfyDesktop) - Fully featured desktop client for Linux, built with Qt and C++.
|
||||
|
||||
## Projects + scripts
|
||||
|
||||
@@ -79,13 +106,14 @@ and uptime of third party servers, so use of each server is **at your own discre
|
||||
- [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)
|
||||
- [grav-plugin-whistleblower](https://github.com/Himmlisch-Studios/grav-plugin-whistleblower) - Grav CMS plugin to get notifications via ntfy (PHP)
|
||||
- [ntfy-server-status](https://github.com/filip2cz/ntfy-server-status) - Checking if server is online and reporting through ntfy (C)
|
||||
- [borg-based backup](https://github.com/davidhi7/backup) - Simple borg-based backup script with notifications based on ntfy.sh or Discord webhooks (Python/Shell)
|
||||
- [ntfy.sh *arr script](https://github.com/agent-squirrel/nfty-arr-script) - Quick and hacky script to get sonarr/radarr to notify the ntfy.sh service (Shell)
|
||||
- [website-watcher](https://github.com/muety/website-watcher) - A small tool to watch websites for changes (with XPath support) (Python)
|
||||
- [siteeagle](https://github.com/tpanum/siteeagle) - A small Python script to monitor websites and notify changes (Python)
|
||||
- [send_to_phone](https://github.com/whipped-cream/send_to_phone) - Scripts to upload a file to Transfer.sh and ping ntfy with the download link (Python)
|
||||
- [ntfy Discord bot](https://github.com/R0dn3yS/ntfy-bot) - WIP ntfy discord bot (TypeScript)
|
||||
@@ -109,6 +137,7 @@ and uptime of third party servers, so use of each server is **at your own discre
|
||||
- [ntfy_on_a_chip](https://github.com/gergepalfi/ntfy_on_a_chip) - ESP8266 and ESP32 client code to communicate with ntfy
|
||||
- [ntfy-sdk](https://github.com/yukibtc/ntfy-sdk) - ntfy client library to send notifications (Rust)
|
||||
- [ntfy_ynh](https://github.com/YunoHost-Apps/ntfy_ynh) - ntfy app for YunoHost
|
||||
- [woodpecker-ntfy](https://codeberg.org/l-x/woodpecker-ntfy)- Woodpecker CI plugin for sending ntfy notfication from a pipeline (Go)
|
||||
- [drone-ntfy](https://github.com/Clortox/drone-ntfy) - Drone.io plugin for sending ntfy notifications from a pipeline (Shell)
|
||||
- [ignition-ntfy-module](https://github.com/Kyvis-Labs/ignition-ntfy-module) - Adds support for sending notifications via a ntfy server to Ignition (Java)
|
||||
- [maubot-ntfy](https://gitlab.com/999eagle/maubot-ntfy) - Matrix bot to subscribe to ntfy topics and send messages to Matrix (Python)
|
||||
@@ -116,9 +145,96 @@ and uptime of third party servers, so use of each server is **at your own discre
|
||||
- [nodebb-plugin-ntfy](https://github.com/NodeBB/nodebb-plugin-ntfy) - Push notifications for NodeBB forums
|
||||
- [n8n-ntfy](https://github.com/raghavanand98/n8n-ntfy.sh) - n8n community node that lets you use ntfy in your workflows
|
||||
- [nlog-ntfy](https://github.com/MichelMichels/nlog-ntfy) - Send NLog messages over ntfy (C# / .NET / NLog)
|
||||
- [helm-charts](https://github.com/sarab97/helm-charts) - Helm charts of some of the selfhosted services, incl. ntfy
|
||||
- [ntfy_ansible_role](https://github.com/stevenengland/ntfy_ansible_role) (on [Ansible Galaxy](https://galaxy.ansible.com/stevenengland/ntfy)) - Ansible role to install ntfy
|
||||
- [easy2ntfy](https://github.com/chromoxdor/easy2ntfy) - Gateway for ESPeasy to receive commands through ntfy and using easyfetch (HTML/JS)
|
||||
- [ntfy_lite](https://github.com/MPI-IS/ntfy_lite) - Minimalist python API for pushing ntfy notifications (Python)
|
||||
- [notify](https://github.com/guanguans/notify) - 推送通知 (PHP)
|
||||
- [zpool-events](https://github.com/maglar0/zpool-events) - Notify on ZFS pool events (Python)
|
||||
- [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, 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
|
||||
- [NtfyMe-Blender](https://github.com/NotNanook/NtfyMe-Blender) - Blender addon to send notifications to NtfyMe (Python)
|
||||
- [ntfy-ios-url-share](https://www.icloud.com/shortcuts/be8a7f49530c45f79733cfe3e41887e6) - An iOS shortcut that lets you share URLs easily and quickly.
|
||||
- [ntfy-ios-filesharing](https://www.icloud.com/shortcuts/fe948d151b2e4ae08fb2f9d6b27d680b) - An iOS shortcut that lets you share files from your share feed to a topic of your choice.
|
||||
- [systemd-ntfy](https://hackage.haskell.org/package/systemd-ntfy) - monitor a set of systemd services an send a notification to ntfy.sh whenever their status changes
|
||||
- [RouterOS Scripts](https://git.eworm.de/cgit/routeros-scripts/about/) - a collection of scripts for MikroTik RouterOS
|
||||
- [ntfy-android-builder](https://github.com/TheBlusky/ntfy-android-builder) - Script for building ntfy-android with custom Firebase configuration (Docker/Shell)
|
||||
- [jetspotter](https://github.com/vvanouytsel/jetspotter) - send notifications when planes are spotted near you (Go)
|
||||
- [monitoring_ntfy](https://www.drupal.org/project/monitoring_ntfy) - Drupal monitoring Ntfy.sh integration (PHP/Drupal)
|
||||
- [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) - Overseerr and Maintainerr webhook notification to ntfy helper service (C#)
|
||||
- [ntfy for Sandstorm](https://apps.sandstorm.io/app/c6rk81r4qk6dm3k04x1kxmyccqewhh4npuxeyg1xrpfypn2ddy0h) - ntfy app for the Sandstorm platform
|
||||
- [ntfy-heartbeat-monitor](https://codeberg.org/RockWolf/ntfy-heartbeat-monitor) - Application for implementing heartbeat monitoring/alerting by utilizing ntfy
|
||||
- [ntfy-bridge](https://github.com/AlexGaudon/ntfy-bridge) - An application to bridge Discord messages (or webhooks) to ntfy.
|
||||
- [ntailfy](https://github.com/leukosaima/ntailfy) - ntfy notifications when Tailscale devices connect/disconnect (Go)
|
||||
- [BRun](https://github.com/cbrake/brun) - Native Linux automation platform connecting triggers to actions without containers (Go)
|
||||
|
||||
## 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
|
||||
- [ZFS and SMART Warnings via Ntfy](https://rair.dev/zfs-smart-ntfy/) - rair.dev - 2/2024
|
||||
- [Automating Security Camera Notifications With Home Assistant and Ntfy](https://runtimeterror.dev/automating-camera-notifications-home-assistant-ntfy/) ⭐ - runtimeterror.dev - 2/2024
|
||||
- [Ntfy: self-hosted notification service](https://medium.com/@williamdonze/ntfy-self-hosted-notification-service-0f3eada6e657) ⭐ - williamdonze.medium.com - 1/2024
|
||||
- [Let’s Supercharge Snowflake Alerts with Cool ntfy Open-source Notifications!](https://sarathi-data-ml-cloud.medium.com/lets-supercharge-snowflake-alerts-with-cool-ntfy-open-source-notifications-296da442c331) - sarathi-data-ml-cloud.medium.com - 1/2024
|
||||
- [Setting up NTFY with Ngnix-Proxy-Manager, authentication and Ansible notifications](https://random-it-blog.de/rocky-linux/setting-up-ntfy-with-ngnix-proxy-manager-authentication-and-ansible-notifications/) - random-it-blog.de - 12/2023
|
||||
- [Introducing the Monitoring Ntfy.sh Integration Module: Real-time Notifications for Drupal Monitoring](https://cyberschorsch.dev/drupal/introducing-monitoring-ntfysh-integration-module-real-time-notifications-drupal-monitoring) - cyberschorsch.dev - 11/2023
|
||||
- [How to install Ntfy.sh on CasaOS using BigBearCasaOS](https://www.youtube.com/watch?v=wSWhtSNwTd8) - youtube.com - 10/2023
|
||||
- [Podman Update Notifications via Ntfy](https://rair.dev/podman-update-notifications-ntfy/) - rair.dev - 9/2023
|
||||
- [Easy Push Notifications With ntfy.sh](https://runtimeterror.dev/easy-push-notifications-with-ntfy/) ⭐ - runtimeterror.dev - 9/2023
|
||||
- [Ntfy: Your Ultimate Push Notification Powerhouse!](https://kkamalesh117.medium.com/ntfy-your-ultimate-push-notification-powerhouse-1968c070f1d1) - kkamalesh117.medium.com - 9/2023
|
||||
- [Installing Self Host NTFY On Linux Using Docker Container](https://www.pinoylinux.org/topicsplus/containers/installing-self-host-ntfy-on-linux-using-docker-container/) - pinoylinux.org - 9/2023
|
||||
- [Homelab Notifications with ntfy](https://blog.alexsguardian.net/posts/2023/09/12/selfhosting-ntfy/) ⭐ - alexsguardian.net - 9/2023
|
||||
- [Why NTFY is the Ultimate Push Notification Tool for Your Needs](https://osintph.medium.com/why-ntfy-is-the-ultimate-push-notification-tool-for-your-needs-e767421c84c5) - osintph.medium.com - 9/2023
|
||||
- [Supercharge Your Alerts: Ntfy — The Ultimate Push Notification Solution](https://medium.com/spring-boot/supercharge-your-alerts-ntfy-the-ultimate-push-notification-solution-a3dda79651fe) - spring-boot.medium.com - 9/2023
|
||||
- [Deploy Ntfy using Docker](https://www.linkedin.com/pulse/deploy-ntfy-mohamed-sharfy/) - linkedin.com - 9/2023
|
||||
- [Send Notifications With Ntfy for New WordPress Posts](https://www.activepieces.com/blog/ntfy-notifications-for-wordpress-new-posts) - activepieces.com - 9/2023
|
||||
- [Get Ntfy Notifications About New Zendesk Ticket](https://www.activepieces.com/blog/ntfy-notifications-about-new-zendesk-tickets) - activepieces.com - 9/2023
|
||||
- [Set reminder for recurring events using ntfy & Cron](https://www.youtube.com/watch?v=J3O4aQ-EcYk) - youtube.com - 9/2023
|
||||
- [ntfy - Installation and full configuration setup](https://www.youtube.com/watch?v=QMy14rGmpFI) - youtube.com - 9/2023
|
||||
- [How to install Ntfy.sh on Portainer / Docker Compose](https://www.youtube.com/watch?v=utD9GNbAwyg) - youtube.com - 9/2023
|
||||
- [ntfy - Push-Benachrichtigungen // Push Notifications](https://www.youtube.com/watch?v=LE3vRPPqZOU) - youtube.com - 9/2023
|
||||
- [Podman Update Notifications via Ntfy](https://rair.dev/podman-upadte-notifications-ntfy/) - rair.dev - 9/2023
|
||||
- [How to Send Alerts From Raspberry Pi Pico W to a Phone or Tablet](https://www.tomshardware.com/how-to/send-alerts-raspberry-pi-pico-w-to-mobile-device) - tomshardware.com - 8/2023
|
||||
- [NetworkChunk - how did I NOT know about this?](https://www.youtube.com/watch?v=poDIT2ruQ9M) ⭐ - youtube.com - 8/2023
|
||||
- [NTFY - Command-Line Notifications](https://academy.networkchuck.com/blog/ntfy/) - academy.networkchuck.com - 8/2023
|
||||
- [Open Source Push Notifications! Get notified of any event you can imagine. Triggers abound!](https://www.youtube.com/watch?v=WJgwWXt79pE) ⭐ - youtube.com - 8/2023
|
||||
- [How to install and self host an Ntfy server on Linux](https://linuxconfig.org/how-to-install-and-self-host-an-ntfy-server-on-linux) - linuxconfig.org - 7/2023
|
||||
- [Basic website monitoring using cronjobs and ntfy.sh](https://burkhardt.dev/2023/website-monitoring-cron-ntfy/) - burkhardt.dev - 6/2023
|
||||
- [Pingdom alternative in one line of curl through ntfy.sh](https://piqoni.bearblog.dev/uptime-monitoring-in-one-line-of-curl/) - bearblog.dev - 6/2023
|
||||
- [#OpenSourceDiscovery 78: ntfy.sh](https://opensourcedisc.substack.com/p/opensourcediscovery-78-ntfysh) - opensourcedisc.substack.com - 6/2023
|
||||
- [ntfy: des notifications instantanées](https://blogmotion.fr/diy/ntfy-notification-push-domotique-20708) - blogmotion.fr - 5/2023
|
||||
- [桌面通知:ntfy](https://www.cnblogs.com/xueweihan/archive/2023/05/04/17370060.html) - cnblogs.com - 5/2023
|
||||
- [ntfy.sh - Open source push notifications via PUT/POST](https://lobste.rs/s/5drapz/ntfy_sh_open_source_push_notifications) - lobste.rs - 5/2023
|
||||
- [Install ntfy Inside Docker Container in Linux](https://lindevs.com/install-ntfy-inside-docker-container-in-linux) - lindevs.com - 4/2023
|
||||
- [ntfy.sh](https://neo-sahara.com/wp/2023/03/25/ntfy-sh/) - neo-sahara.com - 3/2023
|
||||
- [Using Ntfy to send and receive push notifications - Samuel Rosa de Oliveria - Delphicon 2023](https://www.youtube.com/watch?v=feu0skpI9QI) - youtube.com - 3/2023
|
||||
- [ntfy: własny darmowy system powiadomień](https://sprawdzone.it/ntfy-wlasny-darmowy-system-powiadomien/) - sprawdzone.it - 3/2023
|
||||
- [Deploying ntfy on railway](https://www.youtube.com/watch?v=auJICXtxoNA) - youtube.com - 3/2023
|
||||
- [Start-Job,Variables, and ntfy.sh](https://klingele.dev/2023/03/01/start-jobvariables-and-ntfy-sh/) - klingele.dev - 3/2023
|
||||
- [enviar notificaciones automáticas usando ntfy.sh](https://osiux.com/2023-02-15-send-automatic-notifications-using-ntfy.html) - osiux.com - 2/2023
|
||||
- [Carnet IP动态解析以及通过ntfy推送IP信息](https://blog.wslll.cn/index.php/archives/201/) - blog.wslll.cn - 2/2023
|
||||
@@ -131,10 +247,13 @@ and uptime of third party servers, so use of each server is **at your own discre
|
||||
- [UnifiedPush: a decentralized, open-source push notification protocol](https://f-droid.org/en/2022/12/18/unifiedpush.html) ⭐ - 12/2022
|
||||
- [ntfy setup instructions](https://docs.benjamin-altpeter.de/network/vms/1001029-ntfy/) - benjamin-altpeter.de - 12/2022
|
||||
- [Ntfy Self-Hosted Push Notifications](https://lachlanlife.net/posts/2022-12-ntfy/) - lachlanlife.net - 12/2022
|
||||
- [NTFY - système de notification hyper simple et complet](https://www.youtube.com/watch?v=UieZYWVVgA4) - youtube.com - 12/2022
|
||||
- [ntfy.sh](https://paramdeo.com/til/ntfy-sh) - paramdeo.com - 11/2022
|
||||
- [Using ntfy to warn me when my computer is discharging](https://ulysseszh.github.io/programming/2022/11/28/ntfy-warn-discharge.html) - ulysseszh.github.io - 11/2022
|
||||
- [Enabling SSH Login Notifications using Ntfy](https://paramdeo.com/blog/enabling-ssh-login-notifications-using-ntfy) - paramdeo.com - 11/2022
|
||||
- [ntfy - Push Notification Service](https://dizzytech.de/posts/ntfy/) - dizzytech.de - 11/2022
|
||||
- [Console #132](https://console.substack.com/p/console-132) ⭐ - console.substack.com - 11/2022
|
||||
- [How to make my phone buzz*](https://evbogue.com/howtomakemyphonebuzz) - evbogue.com - 11/2022
|
||||
- [MeshCentral - Ntfy Push Notifications ](https://www.youtube.com/watch?v=wyE4rtUd4Bg) - youtube.com - 11/2022
|
||||
- [Changelog | Tracking layoffs, tech worker demand still high, ntfy, ...](https://changelog.com/news/tracking-layoffs-tech-worker-demand-still-high-ntfy-devenv-markdoc-mike-bifulco-Y1jW) ⭐ - changelog.com - 11/2022
|
||||
- [Pointer | Issue #367](https://www.pointer.io/archives/a9495a2a6f/) - pointer.io - 11/2022
|
||||
@@ -172,3 +291,24 @@ and uptime of third party servers, so use of each server is **at your own discre
|
||||
- [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
|
||||
|
||||
Here's a list of public ntfy servers. As of right now, there is only one official server. The others are provided by the
|
||||
ntfy community. Thanks to everyone running a public server. **You guys rock!**
|
||||
|
||||
| URL | Country |
|
||||
|---------------------------------------------------|--------------------|
|
||||
| [ntfy.sh](https://ntfy.sh/) (*Official*) | 🇺🇸 United States |
|
||||
| [ntfy.tedomum.net](https://ntfy.tedomum.net/) | 🇫🇷 France |
|
||||
| [ntfy.jae.fi](https://ntfy.jae.fi/) | 🇫🇮 Finland |
|
||||
| [ntfy.adminforge.de](https://ntfy.adminforge.de/) | 🇩🇪 Germany |
|
||||
| [ntfy.envs.net](https://ntfy.envs.net) | 🇩🇪 Germany |
|
||||
| [ntfy.mzte.de](https://ntfy.mzte.de/) | 🇩🇪 Germany |
|
||||
| [ntfy.hostux.net](https://ntfy.hostux.net/) | 🇫🇷 France |
|
||||
| [ntfy.fossman.de](https://ntfy.fossman.de/) | 🇩🇪 Germany |
|
||||
|
||||
Please be aware that **server operators can log your messages**. The project also cannot guarantee the reliability
|
||||
and uptime of third party servers, so use of each server is **at your own discretion**.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Known issues
|
||||
This is an incomplete list of known issues with the ntfy server, Android app, and iOS app. You can find a complete
|
||||
This is an incomplete list of known issues with the ntfy server, web app, Android app, and iOS app. You can find a complete
|
||||
list [on GitHub](https://github.com/binwiederhier/ntfy/labels/%F0%9F%AA%B2%20bug), but I thought it may be helpful
|
||||
to have the prominent ones here to link to.
|
||||
|
||||
@@ -8,7 +8,7 @@ For some (many?) users, the iOS app is not refreshing the view when new notifica
|
||||
swipe down, you do not see the newly arrived messages, even though the popup appeared before.
|
||||
|
||||
This is caused by some weirdness between the Notification Service Extension (NSE), SwiftUI and Core Data. I am entirely
|
||||
clueless on how to fix it, sadly, as it is ephemeral and now clear to me what is causing it.
|
||||
clueless on how to fix it, sadly, as it is ephemeral and not clear to me what is causing it.
|
||||
|
||||
Please send experienced iOS developers my way to help me figure this out.
|
||||
|
||||
@@ -26,3 +26,18 @@ Be sure that in your selfhosted server:
|
||||
|
||||
* Set `upstream-base-url: "https://ntfy.sh"` (**not your own hostname!**)
|
||||
* Ensure that the URL you set in `base-url` **matches exactly** what you set the Default Server in iOS to
|
||||
|
||||
## iOS app seeing "New message", but not real message content
|
||||
If you see `New message` notifications on iOS, your iPhone can likely not talk to your self-hosted server. Be sure that
|
||||
your iOS device and your ntfy server are either on the same network, or that your phone can actually reach the server.
|
||||
|
||||
Turn on tracing/debugging on the server (via `log-level: trace` or `log-level: debug`, see [troubleshooting](troubleshooting.md)),
|
||||
and read docs on [iOS instant notifications](https://docs.ntfy.sh/config/#ios-instant-notifications).
|
||||
|
||||
## Safari does not play sounds for web push notifications
|
||||
Safari does not support playing sounds for web push notifications, and treats them all as silent. This will be fixed with
|
||||
iOS 17 / Safari 17, which will be released later in 2023.
|
||||
|
||||
## PWA on iOS sometimes crashes with an IndexedDB error (see [#787](https://github.com/binwiederhier/ntfy/issues/787))
|
||||
When resuming the installed PWA from the background, it sometimes crashes with an error from IndexedDB/Dexie, due to a
|
||||
[WebKit bug]( https://bugs.webkit.org/show_bug.cgi?id=197050). A reload will fix it until a permanent fix is found.
|
||||
|
||||
198
docs/privacy.md
@@ -1,12 +1,194 @@
|
||||
# Privacy policy
|
||||
|
||||
I love free software, and I'm doing this because it's fun. I have no bad intentions, and **I will
|
||||
never monetize or sell your information, and this service and software will always stay free and open.**
|
||||
**Last updated:** January 2, 2026
|
||||
|
||||
Neither the server nor the app record any personal information, or share any of the messages and topics with
|
||||
any outside service. All data is exclusively used to make the service function properly. The only external service
|
||||
I use is Firebase Cloud Messaging (FCM) service, which is required to provide instant Android notifications (see
|
||||
[FAQ](faq.md) for details). To avoid FCM altogether, download the F-Droid version.
|
||||
This privacy policy describes how ntfy ("we", "us", or "our") collects, uses, and handles your information
|
||||
when you use the ntfy.sh service, web app, and mobile applications (Android and iOS).
|
||||
|
||||
For debugging purposes, the ntfy server may temporarily log request paths, remote IP addresses or even topics
|
||||
or messages, though typically this is turned off.
|
||||
## Our commitment to privacy
|
||||
|
||||
We love free software, and we're doing this because it's fun. We have no bad intentions, and **we will
|
||||
never monetize or sell your information**. The ntfy service and software will always stay free and open source.
|
||||
If you don't trust us or your messages are sensitive, you can [self-host your own ntfy server](install.md).
|
||||
|
||||
## Information we collect
|
||||
|
||||
### Account information (optional)
|
||||
|
||||
If you create an account on ntfy.sh, we collect:
|
||||
|
||||
- **Username** - A unique identifier you choose
|
||||
- **Password** - Stored as a secure bcrypt hash (we never store your plaintext password)
|
||||
- **Email address** - Only if you subscribe to a paid plan (for billing purposes)
|
||||
- **Phone number** - Only if you enable the phone call notification feature (verified via SMS/call)
|
||||
|
||||
You can use ntfy without creating an account. Anonymous usage is fully supported.
|
||||
|
||||
### Messages and notifications
|
||||
|
||||
- **Message content** - Messages you publish are temporarily cached on our servers (default: 12 hours) to support
|
||||
message polling and to overcome client network disruptions. Messages are deleted after the cache duration expires.
|
||||
- **Attachments** - File attachments are temporarily stored (default: 3 hours) and then automatically deleted.
|
||||
- **Topic names** - The topic names you publish to or subscribe to are processed by our servers.
|
||||
|
||||
### Technical information
|
||||
|
||||
- **IP addresses** - Used for rate limiting to prevent abuse. May be temporarily logged for debugging purposes,
|
||||
though this is typically turned off.
|
||||
- **Access tokens** - If you create access tokens, we store the token value, an optional label, last access time,
|
||||
and the IP address of the last access.
|
||||
- **Web push subscriptions** - If you enable browser notifications, we store your browser's push subscription
|
||||
endpoint to deliver notifications.
|
||||
|
||||
### Billing information (paid plans only)
|
||||
|
||||
If you subscribe to a paid plan, payment processing is handled by Stripe. We store:
|
||||
|
||||
- Stripe customer ID
|
||||
- Subscription status and billing period
|
||||
|
||||
We do not store your credit card numbers or payment details directly. These are handled entirely by Stripe.
|
||||
|
||||
## Third-party services
|
||||
|
||||
To provide the ntfy.sh service, we use the following third-party services:
|
||||
|
||||
### Firebase Cloud Messaging (FCM)
|
||||
|
||||
We use Google's Firebase Cloud Messaging to deliver push notifications to Android and iOS devices. When you
|
||||
receive a notification through the mobile apps (Google Play or App Store versions):
|
||||
|
||||
- Message metadata and content may be transmitted through Google's FCM infrastructure
|
||||
- Google's [privacy policy](https://policies.google.com/privacy) applies to their handling of this data
|
||||
|
||||
**To avoid FCM entirely:** Download the [F-Droid version](https://f-droid.org/en/packages/io.heckel.ntfy/) of
|
||||
the Android app and use a self-hosted server, or use the instant delivery feature with your own server.
|
||||
|
||||
### Twilio (phone calls)
|
||||
|
||||
If you use the phone call notification feature (`X-Call` header), we use Twilio to:
|
||||
|
||||
- Make voice calls to your verified phone number
|
||||
- Send SMS or voice calls for phone number verification
|
||||
|
||||
Your phone number is shared with Twilio to deliver these services. Twilio's
|
||||
[privacy policy](https://www.twilio.com/legal/privacy) applies.
|
||||
|
||||
### Amazon SES (email delivery)
|
||||
|
||||
If you use the email notification feature (`X-Email` header), we use Amazon Simple Email Service (SES) to
|
||||
deliver emails. The recipient email address and message content are transmitted through Amazon's infrastructure.
|
||||
Amazon's [privacy policy](https://aws.amazon.com/privacy/) applies.
|
||||
|
||||
### Stripe (payments)
|
||||
|
||||
If you subscribe to a paid plan, payments are processed by Stripe. Your payment information is handled directly
|
||||
by Stripe and is subject to Stripe's [privacy policy](https://stripe.com/privacy).
|
||||
|
||||
Note: We have explicitly disabled Stripe's telemetry features in our integration.
|
||||
|
||||
### Web push providers
|
||||
|
||||
If you enable browser notifications in the ntfy web app, push messages are delivered through your browser
|
||||
vendor's push service:
|
||||
|
||||
- Google (Chrome)
|
||||
- Mozilla (Firefox)
|
||||
- Apple (Safari)
|
||||
- Microsoft (Edge)
|
||||
|
||||
Your browser's push subscription endpoint is shared with these providers to deliver notifications.
|
||||
|
||||
## Mobile applications
|
||||
|
||||
### Android app
|
||||
|
||||
The Android app is available from two sources:
|
||||
|
||||
- **Google Play Store** - Uses Firebase Cloud Messaging for push notifications. Firebase Analytics is
|
||||
**explicitly disabled** in our app.
|
||||
- **F-Droid** - Does not include any Google services or Firebase. Uses a foreground service to maintain
|
||||
a direct connection to the server.
|
||||
|
||||
The Android app stores the following data locally on your device:
|
||||
|
||||
- Subscribed topics and their settings
|
||||
- Cached notifications
|
||||
- User credentials (if you add a server with authentication)
|
||||
- Application logs (for debugging, stored locally only)
|
||||
|
||||
### iOS app
|
||||
|
||||
The iOS app uses Firebase Cloud Messaging (via Apple Push Notification service) to deliver notifications.
|
||||
The app stores the following data locally on your device:
|
||||
|
||||
- Subscribed topics
|
||||
- Cached notifications
|
||||
- User credentials (if configured)
|
||||
|
||||
## Web application
|
||||
|
||||
The ntfy web app is a static website that stores all data locally in your browser:
|
||||
|
||||
- **IndexedDB** - Stores your subscriptions and cached notifications
|
||||
- **Local Storage** - Stores your preferences and session information
|
||||
|
||||
No cookies are used for tracking. The web app does not have a backend beyond the ntfy API.
|
||||
|
||||
## Data retention
|
||||
|
||||
| Data type | Retention period |
|
||||
|------------------------|---------------------------------------------------|
|
||||
| Messages | 12 hours (configurable by server operators) |
|
||||
| Attachments | 3 hours (configurable by server operators) |
|
||||
| User accounts | Until you delete your account |
|
||||
| Access tokens | Until you revoke them or delete your account |
|
||||
| Phone numbers | Until you remove them or delete your account |
|
||||
| Web push subscriptions | 60 days of inactivity, then automatically removed |
|
||||
| Server logs | Varies; debugging logs are typically temporary |
|
||||
|
||||
## Self-hosting
|
||||
|
||||
If you prefer complete control over your data, you can [self-host your own ntfy server](install.md).
|
||||
When self-hosting:
|
||||
|
||||
- You control all data storage and retention
|
||||
- You can choose whether to use Firebase, Twilio, email delivery, or any other integrations
|
||||
- No data is shared with ntfy.sh or any third party (unless you configure those integrations)
|
||||
|
||||
The server and all apps are fully open source:
|
||||
|
||||
- Server: [github.com/binwiederhier/ntfy](https://github.com/binwiederhier/ntfy)
|
||||
- Android app: [github.com/binwiederhier/ntfy-android](https://github.com/binwiederhier/ntfy-android)
|
||||
- iOS app: [github.com/binwiederhier/ntfy-ios](https://github.com/binwiederhier/ntfy-ios)
|
||||
|
||||
## Data security
|
||||
|
||||
- All connections to ntfy.sh are encrypted using TLS/HTTPS
|
||||
- Passwords are hashed using bcrypt before storage
|
||||
- Access tokens are generated using cryptographically secure random values
|
||||
- The server does not log message content by default
|
||||
|
||||
## Your rights
|
||||
|
||||
You have the right to:
|
||||
|
||||
- **Access** - View your account information and data
|
||||
- **Delete** - Delete your account and associated data via the web app
|
||||
- **Export** - Your messages are available via the API while cached
|
||||
|
||||
To delete your account, use the account settings in the web app or contact us.
|
||||
|
||||
## Changes to this policy
|
||||
|
||||
We may update this privacy policy from time to time. Changes will be posted on this page with an updated
|
||||
"Last updated" date. You may also review all changes in the [Git history](https://github.com/binwiederhier/ntfy/commits/main/docs/privacy.md).
|
||||
|
||||
For significant changes, we may provide additional notice on Discord/Matrix or through the
|
||||
[announcements](https://ntfy.sh/announcements) ntfy topic.
|
||||
|
||||
## Contact
|
||||
|
||||
For privacy-related inquiries, please email [privacy@mail.ntfy.sh](mailto:privacy@mail.ntfy.sh).
|
||||
|
||||
For all other contact options, see the [contact page](contact.md).
|
||||
|
||||
1624
docs/publish.md
1507
docs/publish/template-functions.md
Normal file
514
docs/releases.md
@@ -2,6 +2,484 @@
|
||||
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).
|
||||
|
||||
## Current stable releases
|
||||
|
||||
| Component | Version | Release date |
|
||||
|------------------|---------|--------------|
|
||||
| ntfy server | v2.15.0 | Nov 16, 2025 |
|
||||
| ntfy Android app | v1.21.1 | Jan 6, 2025 |
|
||||
| ntfy iOS app | v1.3 | Nov 26, 2023 |
|
||||
|
||||
Please check out the release notes for [upcoming releases](#not-released-yet) below.
|
||||
|
||||
## ntfy Android app v1.21.1
|
||||
Released January 6, 2026
|
||||
|
||||
This is the first feature release in a long time. After all the SDK updates, fixes to comply with the Google Play policies
|
||||
and the framework updates, this release ships a lot of highly requested features: Sending messages through the app (WhatsApp-style),
|
||||
support for passing headers to your proxy, an in-app language switcher, and more.
|
||||
|
||||
If you are waiting for a feature, please 👍 the corresponding [GitHub issue](https://github.com/binwiederhier/ntfy/issues?q=is%3Aissue%20state%3Aopen%20sort%3Areactions-%2B1-desc).
|
||||
If you like ntfy, please consider purchasing [ntfy Pro](https://ntfy.sh/app) to support us.
|
||||
|
||||
**Features:**
|
||||
|
||||
* Allow publishing messages through the message bar and publish dialog ([#98](https://github.com/binwiederhier/ntfy/issues/98), [ntfy-android#144](https://github.com/binwiederhier/ntfy-android/pull/144))
|
||||
* Define custom HTTP headers to support authenticated proxies, tunnels and SSO ([ntfy-android#116](https://github.com/binwiederhier/ntfy-android/issues/116), [#1018](https://github.com/binwiederhier/ntfy/issues/1018), [ntfy-android#132](https://github.com/binwiederhier/ntfy-android/pull/132), [ntfy-android#146](https://github.com/binwiederhier/ntfy-android/pull/146), thanks to [@CrazyWolf13](https://github.com/CrazyWolf13))
|
||||
* Implement UnifiedPush "raise to foreground" requirement ([ntfy-android#98](https://github.com/binwiederhier/ntfy-android/pull/98), [ntfy-android#148](https://github.com/binwiederhier/ntfy-android/pull/148), thanks to [@p1gp1g](https://github.com/p1gp1g))
|
||||
* Language selector to allow overriding the system language ([#1508](https://github.com/binwiederhier/ntfy/issues/1508), [ntfy-android#145](https://github.com/binwiederhier/ntfy-android/pull/145), thanks to [@hudsonm62](https://github.com/hudsonm62) for reporting)
|
||||
* Highlight phone numbers and email addresses in notifications ([#957](https://github.com/binwiederhier/ntfy/issues/957), [ntfy-android#71](https://github.com/binwiederhier/ntfy-android/pull/71), thanks to [@brennenputh](https://github.com/brennenputh), and [@XylenSky](https://github.com/XylenSky) for reporting)
|
||||
* Support for port and display name in [ntfy://](subscribe/phone.md#ntfy-links) links ([ntfy-android#130](https://github.com/binwiederhier/ntfy-android/pull/130), thanks to [@godovski](https://github.com/godovski))
|
||||
|
||||
**Bug fixes + maintenance:**
|
||||
|
||||
* Add support for (technically incorrect) 'image/jpg' MIME type ([ntfy-android#142](https://github.com/binwiederhier/ntfy-android/pull/142), thanks to [@Murilobeluco](https://github.com/Murilobeluco))
|
||||
* Unify "copy to clipboard" notifications, use Android 13 style ([ntfy-android#61](https://github.com/binwiederhier/ntfy-android/pull/61), thanks to [@thgoebel](https://github.com/thgoebel))
|
||||
* Fix crash in user add dialog (onAddUser)
|
||||
* Fix ForegroundServiceDidNotStartInTimeException (attempt 2, see [#1520](https://github.com/binwiederhier/ntfy/issues/1520))
|
||||
* Hide "Exact alarms" setting if battery optimization exemption has been granted ([#1456](https://github.com/binwiederhier/ntfy/issues/1456), thanks for reporting [@HappyLer](https://github.com/HappyLer))
|
||||
|
||||
## ntfy Android app v1.20.0
|
||||
Released December 28, 2025
|
||||
|
||||
This is the last pure maintenance release for now. It'll bring all dependencies and library version to the latest version,
|
||||
and fixes some crashes. I had to drop support for about 4,000 devices (only ~200 installations), because the libraries
|
||||
themselves do not support SDK 21 anymore, which was the previous minimum SDK version (Android 5, 2014). Now the minimum
|
||||
SDK version is 26 (Android 8, 2017).
|
||||
|
||||
**Bug fixes + maintenance:**
|
||||
|
||||
* Updated dependencies, minimum SDK version to 26, clean up legacy code, upgrade Gradle ([ntfy-android#140](https://github.com/binwiederhier/ntfy-android/pull/140),
|
||||
thanks to [@cyb3rko](https://github.com/cyb3rko) for the implementation)
|
||||
* Updated target SDK version to 36 (Android 8, 2017)
|
||||
* Fixed ForegroundServiceDidNotStartInTimeException ([#1520](https://github.com/binwiederhier/ntfy/issues/1520))
|
||||
* Fixed crashes with redrawing the list when temporarily muted topics expire
|
||||
|
||||
## ntfy Android app v1.19.4
|
||||
Released December 21, 2025
|
||||
|
||||
This release upgrades the Android app to use [Material 3](https://m3.material.io/) design components and adds the
|
||||
ability to use [dynamic colors](https://developer.android.com/develop/ui/views/theming/dynamic-colors).
|
||||
**This was a lot of work** and I want to thank [@Bnyro](https://github.com/Bnyro) and [@cyb3rko](https://github.com/cyb3rko) for implementing this. You guys rock!
|
||||
|
||||
**Features:**
|
||||
|
||||
* Moved the user interface to Material 3 and added dynamic color support ([#580](https://github.com/binwiederhier/ntfy/issues/580),
|
||||
[ntfy-android#56](https://github.com/binwiederhier/ntfy-android/pull/56), [ntfy-android#126](https://github.com/binwiederhier/ntfy-android/pull/126),
|
||||
[ntfy-android#135](https://github.com/binwiederhier/ntfy-android/pull/135), thanks to [@Bnyro](https://github.com/Bnyro)
|
||||
and [@cyb3rko](https://github.com/cyb3rko) for the implementation, and to [@RokeJulianLockhart](https://github.com/RokeJulianLockhart) for reporting)
|
||||
|
||||
## ntfy Android app v1.18.0
|
||||
Released December 4, 2025
|
||||
|
||||
**Features:**
|
||||
|
||||
* Added GIF support for preview images ([ntfy-android#76](https://github.com/binwiederhier/ntfy-android/pull/76)/[#532](https://github.com/binwiederhier/ntfy/issues/532), thanks to [@MichaelArkh](https://github.com/MichaelArkh) and [@dimatx](https://github.com/dimatx) for reporting)
|
||||
* Added WebP support for preview images ([ntfy-android#81](https://github.com/binwiederhier/ntfy-android/pull/81)/[ntfy-android#80](https://github.com/binwiederhier/ntfy-android/issues/80), thanks to [@jokakilla](https://github.com/jokakilla))
|
||||
* Added UnifiedPush distributor selection support ([#137](https://github.com/binwiederhier/ntfy-android/pull/137), thanks to [@p1gp1g](https://github.com/p1gp1g))
|
||||
|
||||
**Bug fixes + maintenance:**
|
||||
|
||||
* Remove REQUEST_INSTALL_PACKAGES permission ([#684](https://github.com/binwiederhier/ntfy/issues/684))
|
||||
* Request to ignore battery optimizations before receiving subscription ([ntfy-android#97](https://github.com/binwiederhier/ntfy-android/pull/97), thanks to [@p1gp1g](https://github.com/p1gp1g))
|
||||
|
||||
## ntfy server v2.15.0
|
||||
Released Nov 16, 2025
|
||||
|
||||
This release adds a `require-login` flag to topics, which forces users to log in before they can
|
||||
use the web app. This is useful for self-hosters and will obviously not be enabled on ntfy.sh.
|
||||
|
||||
**Features:**
|
||||
|
||||
* Add `require-login` flag to redirect to login page if not logged in ([#1434](https://github.com/binwiederhier/ntfy/pull/1434)/[#238](https://github.com/binwiederhier/ntfy/issues/238)/[#1329](https://github.com/binwiederhier/ntfy/pull/1329), thanks to [@theatischbein](https://github.com/theatischbein) for implementing most of this)
|
||||
|
||||
**Bug fixes + maintenance:**
|
||||
|
||||
* The official ntfy.sh Debian/Ubuntu repository has moved to [archive.ntfy.sh](https://archive.ntfy.sh) ([#1357](https://github.com/binwiederhier/ntfy/issues/1357)/[#1401](https://github.com/binwiederhier/ntfy/issues/1401), thanks to [@skibbipl](https://github.com/skibbipl) and [@lduesing](https://github.com/lduesing) for reporting)
|
||||
* Add mutex around message cache writes to avoid `database locked` errors ([#1397](https://github.com/binwiederhier/ntfy/pull/1397), [#1391](https://github.com/binwiederhier/ntfy/issues/1391), thanks to [@timofej673](https://github.com/timofej673))
|
||||
* Add build tags `nopayments`, `nofirebase` and `nowebpush` to allow excluding external dependencies, useful for
|
||||
packaging in Debian ([#1420](https://github.com/binwiederhier/ntfy/pull/1420), discussion in [#1258](https://github.com/binwiederhier/ntfy/issues/1258), thanks to [@thekhalifa](https://github.com/thekhalifa) for packaging ntfy for Debian/Ubuntu)
|
||||
* Make copying tokens, phone numbers, etc. possible on HTTP ([#1432](https://github.com/binwiederhier/ntfy/pull/1432)/[#1408](https://github.com/binwiederhier/ntfy/issues/1408)/[#1295](https://github.com/binwiederhier/ntfy/issues/1295), thanks to [@EdwinKM](https://github.com/EdwinKM), [@xxl6097](https://github.com/xxl6097) for reporting)
|
||||
|
||||
## ntfy Android app v1.17.13
|
||||
Released October 21, 2025
|
||||
|
||||
This release makes changes to comply with the Google Play policies. See [#1463](https://github.com/binwiederhier/ntfy/issues/1463)
|
||||
or [ef57cd1](https://github.com/binwiederhier/ntfy-android/commit/ef57cd1374118b3e4d7a7ab496afe337e714fff7) for details.
|
||||
|
||||
The policies do not allow directly or indirectly linking to paid plans or donation links that do not go through Google Play.
|
||||
|
||||
**Changes:**
|
||||
|
||||
* Remove the "Donate" button from menu (all variants)
|
||||
* Change default display name from "ntfy.sh/mytopic" to "mytopic" (all variants)
|
||||
* Remove links to ntfy docs and issue tracker (Play variant only)
|
||||
* Remove how-to links to ntfy.sh in a few places (Play variant only)
|
||||
* Remove "Copy topic address" from subscription menu (Play variant only)
|
||||
|
||||
## ntfy Android app v1.17.8
|
||||
Released September 23, 2025
|
||||
|
||||
This is largely a maintenance update to ensure the SDK is up-to-date.
|
||||
|
||||
**Features:**
|
||||
|
||||
* Markdown is now rendered if "Markdown: yes" was passed ([#310](https://github.com/binwiederhier/ntfy/issues/310), thanks to [@NiNiyas](https://github.com/NiNiyas) for reporting)
|
||||
* You can now disable UnifiedPush so ntfy does not act as a UnifiedPush distributor ([#646](https://github.com/binwiederhier/ntfy/issues/646), thanks to [@ollien](https://github.com/ollien) for reporting and to [@wunter8](https://github.com/wunter8) for implementing)
|
||||
|
||||
**Bug fixes + maintenance:**
|
||||
|
||||
* UnifiedPush subscriptions now include the `Rate-Topics` header to facilitate subscriber-based billing ([#652](https://github.com/binwiederhier/ntfy/issues/652), thanks to [@wunter8](https://github.com/wunter8))
|
||||
* Subscriptions without icons no longer appear to use another subscription's icon ([#634](https://github.com/binwiederhier/ntfy/issues/634), thanks to [@topcaser](https://github.com/topcaser) for reporting and to [@wunter8](https://github.com/wunter8) for fixing)
|
||||
* Bumped all dependencies to the latest versions (no ticket)
|
||||
|
||||
## ntfy server v2.14.0
|
||||
Released August 5, 2025
|
||||
|
||||
This release adds support for [declarative users](config.md#users-via-the-config), [declarative ACL entries](config.md#acl-entries-via-the-config) and [declarative tokens](config.md#tokens-via-the-config). This allows you to define users, ACL entries and tokens in the config file, which is useful for static deployments or deployments that use a configuration management system.
|
||||
|
||||
It also adds support for [pre-defined templates](publish.md#pre-defined-templates) and [custom templates](publish.md#custom-templates) for enhanced JSON webhook support, as well as advanced [template functions](publish.md#template-functions) based on the [Sprig](https://github.com/Masterminds/sprig) functions.
|
||||
|
||||
❤️ If you like ntfy, **please consider sponsoring me** via [GitHub Sponsors](https://github.com/sponsors/binwiederhier), [Liberapay](https://en.liberapay.com/ntfy/), Bitcoin (`1626wjrw3uWk9adyjCfYwafw4sQWujyjn8`), or by buying a [paid plan via the web app](https://ntfy.sh/app). ntfy
|
||||
will always remain open source.
|
||||
|
||||
**Features:**
|
||||
|
||||
* [Declarative users](config.md#users-via-the-config), [declarative ACL entries](config.md#acl-entries-via-the-config) and [declarative tokens](config.md#tokens-via-the-config) ([#464](https://github.com/binwiederhier/ntfy/issues/464), [#1384](https://github.com/binwiederhier/ntfy/pull/1384), [#1413](https://github.com/binwiederhier/ntfy/pull/1413), thanks to [pinpox](https://github.com/pinpox) for reporting, to [@wunter8](https://github.com/wunter8) for reviewing and implementing parts of it)
|
||||
* [Pre-defined templates](publish.md#pre-defined-templates) and [custom templates](publish.md#custom-templates) for enhanced JSON webhook support ([#1390](https://github.com/binwiederhier/ntfy/pull/1390))
|
||||
* Support of advanced [template functions](publish.md#template-functions) based on the [Sprig](https://github.com/Masterminds/sprig) library ([#1121](https://github.com/binwiederhier/ntfy/issues/1121), thanks to [@davidatkinsondoyle](https://github.com/davidatkinsondoyle) for reporting, to [@wunter8](https://github.com/wunter8) for implementing, and to the Sprig team for their work)
|
||||
|
||||
## ntfy server v2.13.0
|
||||
Released July 10, 2025
|
||||
|
||||
This is a relatively small release, mainly to support IPv6 and to add more sophisticated
|
||||
proxy header support. 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.
|
||||
|
||||
**Features:**
|
||||
|
||||
* 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 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 😬.
|
||||
|
||||
!!! 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.
|
||||
|
||||
**Features:**
|
||||
|
||||
* Support for larger message delays with `message-delay-limit` (see [message limits](config.md#message-limits), [#1050](https://github.com/binwiederhier/ntfy/pull/1050)/[#1019](https://github.com/binwiederhier/ntfy/issues/1019), thanks to [@MrChadMWood](https://github.com/MrChadMWood) for reporting)
|
||||
* Support for larger message body sizes with `message-size-limit` (use at your own risk, see [message limits](config.md#message-limits), [#836](https://github.com/binwiederhier/ntfy/pull/836)/[#1050](https://github.com/binwiederhier/ntfy/pull/1050), thanks to [@zhzy0077](https://github.com/zhzy0077) for implementing this, and to [@nkjshlsqja7331](https://github.com/nkjshlsqja7331) for reporting)
|
||||
* Web app: You can now paste images into the message bar or publish dialog ([#963](https://github.com/binwiederhier/ntfy/pull/963)/[#572](https://github.com/binwiederhier/ntfy/issues/572), thanks to [@cmj2002](https://github.com/cmj2002) for implementing, and [@rounakdatta](https://github.com/rounakdatta) for reporting)
|
||||
|
||||
**Bug fixes + maintenance:**
|
||||
|
||||
* ⚠️ Remove `Rate-Topics` header due to DoS security issue if `visitor-subscriber-rate-limiting: true` ([#1048](https://github.com/binwiederhier/ntfy/issues/1048))
|
||||
|
||||
**Documentation:**
|
||||
|
||||
* Remove `mkdocs-simple-hooks` ([#1016](https://github.com/binwiederhier/ntfy/pull/1016), thanks to [@Tom-Hubrecht](https://github.com/Tom-Hubrecht))
|
||||
* Update Watchtower example ([#1014](https://github.com/binwiederhier/ntfy/pull/1014), thanks to [@lennart-m](https://github.com/lennart-m))
|
||||
* Fix dead links ([#1022](https://github.com/binwiederhier/ntfy/pull/1022), thanks to [@DerRockWolf](https://github.com/DerRockWolf))
|
||||
* PowerShell file upload example ([#1004](https://github.com/binwiederhier/ntfy/pull/1004), thanks to [@YMan84](https://github.com/YMan84))
|
||||
|
||||
## 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.
|
||||
|
||||
Many thanks to [@tcaputi](https://github.com/tcaputi) for fixing the issues, and to the anonymous donor for sponsoring these fixes.
|
||||
|
||||
**Bug fixes:**
|
||||
|
||||
* UI not updating properly ([#267](https://github.com/binwiederhier/ntfy/issues/267)/[#402](https://github.com/binwiederhier/ntfy/issues/402), thanks to [@tcaputi](https://github.com/tcaputi))
|
||||
|
||||
## 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
|
||||
|
||||
**Bug fixes + maintenance:**
|
||||
|
||||
* Support for HTML-only emails ([#690](https://github.com/binwiederhier/ntfy/issues/690)/[#693](https://github.com/binwiederhier/ntfy/pull/693), thanks to [@teastrainer](https://github.com/teastrainer) and [@CrazyWolf13](https://github.com/CrazyWolf13) for reporting)
|
||||
* Fix ACL issue with topic patterns containing underscores ([#840](https://github.com/binwiederhier/ntfy/issues/840), thanks to [@Joe-0237](https://github.com/Joe-0237) for reporting)
|
||||
* Fix ACL issue with order of read/write rules ([#914](https://github.com/binwiederhier/ntfy/issues/914)/[#917](https://github.com/binwiederhier/ntfy/pull/917), thanks to [@sandman7920](https://github.com/sandman7920))
|
||||
* Re-add `tzdata` to Docker images for amd64 image ([#894](https://github.com/binwiederhier/ntfy/issues/894), [#307](https://github.com/binwiederhier/ntfy/pull/307))
|
||||
* Add special logic to ignore `Priority` header if it resembles an RFC 9218 value ([#851](https://github.com/binwiederhier/ntfy/pull/851)/[#895](https://github.com/binwiederhier/ntfy/pull/895), thanks to [@gusdleon](https://github.com/gusdleon), see also [#351](https://github.com/binwiederhier/ntfy/issues/351), [#353](https://github.com/binwiederhier/ntfy/issues/353), [#461](https://github.com/binwiederhier/ntfy/issues/461))
|
||||
* PWA: hide install prompt on macOS 14 Safari ([#899](https://github.com/binwiederhier/ntfy/pull/899), thanks to [@nihalgonsalves](https://github.com/nihalgonsalves))
|
||||
* Fix web app crash in Edge for languages with underline in locale ([#922](https://github.com/binwiederhier/ntfy/pull/922)/[#912](https://github.com/binwiederhier/ntfy/issues/912)/[#852](https://github.com/binwiederhier/ntfy/issues/852), thanks to [@imkero](https://github.com/imkero))
|
||||
|
||||
**Additional languages:**
|
||||
|
||||
* Finnish (thanks to [@Seppo](https://hosted.weblate.org/user/Seppo/))
|
||||
|
||||
## ntfy server v2.7.0
|
||||
Released August 17, 2023
|
||||
|
||||
This release ships Markdown support for the web app (not in the Android app yet), and adds support for
|
||||
right-to-left languages (RTL) in the web app. It also fixes a few issues around date/time formatting,
|
||||
internationalization support, a CLI auth bug.
|
||||
|
||||
Furthermore, it fixes a security issue around access tokens getting erroneously deleted for other users
|
||||
in a specific scenario. This was a denial-of-service-type security issue, since it **effectively allowed a
|
||||
single user to deny access to all other users of a ntfy instance**. Please note that while tokens were
|
||||
erroneously deleted, **nobody but the token owner ever had access to it.** Please refer to [the ticket](https://github.com/binwiederhier/ntfy/issues/838)
|
||||
for details. **Please upgrade your ntfy instance if you run a multi-user system.**
|
||||
|
||||
**Features:**
|
||||
|
||||
* Add support for [Markdown formatting](publish.md#markdown-formatting) in web app ([#310](https://github.com/binwiederhier/ntfy/issues/310), thanks to [@nihalgonsalves](https://github.com/nihalgonsalves))
|
||||
* Add support for right-to-left languages (RTL) in the web app ([#663](https://github.com/binwiederhier/ntfy/issues/663), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
||||
|
||||
**Security:** ⚠️
|
||||
|
||||
* Fixes issue with access tokens getting deleted ([#838](https://github.com/binwiederhier/ntfy/issues/838))
|
||||
|
||||
**Bug fixes + maintenance:**
|
||||
|
||||
* Fix issues with date/time with different locales ([#700](https://github.com/binwiederhier/ntfy/issues/700), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
||||
* Re-init i18n on each service worker message to avoid missing translations ([#817](https://github.com/binwiederhier/ntfy/pull/817), thanks to [@nihalgonsalves](https://github.com/nihalgonsalves))
|
||||
* You can now unset the default user:pass/token in `client.yml` for an individual subscription to remove the Authorization header ([#829](https://github.com/binwiederhier/ntfy/issues/829), thanks to [@tomeon](https://github.com/tomeon) for reporting and to [@wunter8](https://github.com/wunter8) for fixing)
|
||||
|
||||
**Documentation:**
|
||||
|
||||
* Update docs for Apache config ([#819](https://github.com/binwiederhier/ntfy/pull/819), thanks to [@nisbet-hubbard](https://github.com/nisbet-hubbard))
|
||||
|
||||
## ntfy server v2.6.2
|
||||
Released June 30, 2023
|
||||
|
||||
With this release, the ntfy web app now contains a **[progressive web app](subscribe/pwa.md) (PWA)
|
||||
with Web Push support**, which means you'll be able to **install the ntfy web app on your desktop or phone** similar
|
||||
to a native app (__even on iOS!__ 🥳). Installing the PWA gives ntfy web its own launcher, a standalone window,
|
||||
push notifications, and an app badge with the unread notification count. Note that for self-hosted servers,
|
||||
[Web Push](config.md#web-push) must be configured.
|
||||
|
||||
On top of that, this release also brings **dark mode** 🧛🌙 to the web app.
|
||||
|
||||
🙏 A huge thanks for this release goes to [@nimbleghost](https://github.com/nimbleghost), for basically implementing the
|
||||
Web Push / PWA and dark mode feature by himself. I'm really grateful for your contributions.
|
||||
|
||||
❤️ 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) (20% off
|
||||
if you use promo code `MYTOPIC`). ntfy will always remain open source.
|
||||
|
||||
**Features:**
|
||||
|
||||
* The web app now supports Web Push, and is installable as a [progressive web app (PWA)](https://docs.ntfy.sh/subscribe/pwa/) on Chrome, Edge, Android, and iOS ([#751](https://github.com/binwiederhier/ntfy/pull/751), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
||||
* Support for dark mode in the web app ([#206](https://github.com/binwiederhier/ntfy/issues/206), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
||||
|
||||
**Bug fixes:**
|
||||
|
||||
* Support encoding any header as RFC 2047 ([#737](https://github.com/binwiederhier/ntfy/issues/737), thanks to [@cfouche3005](https://github.com/cfouche3005) for reporting)
|
||||
* Do not forward poll requests for UnifiedPush messages (no ticket, thanks to NoName for reporting)
|
||||
* Fix `ntfy pub %` segfaulting ([#760](https://github.com/binwiederhier/ntfy/issues/760), thanks to [@clesmian](https://github.com/clesmian) for reporting)
|
||||
* Newly created access tokens are now lowercase only to fully support `<topic>+<token>@<domain>` email syntax ([#773](https://github.com/binwiederhier/ntfy/issues/773), thanks to gingervitiz for reporting)
|
||||
* The .1 release fixes a few visual issues with dark mode, and other web app updates ([#791](https://github.com/binwiederhier/ntfy/pull/791), [#793](https://github.com/binwiederhier/ntfy/pull/793), [#792](https://github.com/binwiederhier/ntfy/pull/792), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
||||
* The .2 release fixes issues with the service worker in Firefox and adds automatic service worker updates ([#795](https://github.com/binwiederhier/ntfy/pull/795), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
||||
|
||||
**Maintenance:**
|
||||
|
||||
* Improved GitHub Actions flow ([#745](https://github.com/binwiederhier/ntfy/pull/745), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
||||
* Web: Add JS formatter "prettier" ([#746](https://github.com/binwiederhier/ntfy/pull/746), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
||||
* Web: Add eslint with eslint-config-airbnb ([#748](https://github.com/binwiederhier/ntfy/pull/748), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
||||
* Web: Switch to Vite ([#749](https://github.com/binwiederhier/ntfy/pull/749), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
||||
|
||||
**Changes in tarball/zip naming:**
|
||||
Due to a [change in GoReleaser](https://goreleaser.com/deprecations/#archivesreplacements), some of the binary release
|
||||
archives now have slightly different names. My apologies if this causes issues in the downstream projects that use ntfy:
|
||||
|
||||
- `ntfy_v${VERSION}_windows_x86_64.zip` -> `ntfy_v${VERSION}_windows_amd64.zip`
|
||||
- `ntfy_v${VERSION}_linux_x86_64.tar.gz` -> `ntfy_v${VERSION}_linux_amd64.tar.gz`
|
||||
- `ntfy_v${VERSION}_macOS_all.tar.gz` -> `ntfy_v${VERSION}_darwin_all.tar.gz`
|
||||
|
||||
## ntfy server v2.5.0
|
||||
Released May 18, 2023
|
||||
|
||||
This release brings a number of new features, including support for text-to-speech style [phone calls](publish.md#phone-calls),
|
||||
an admin API to manage users and ACL (currently in beta, and hence undocumented), and support for authorized access to
|
||||
upstream servers via the `upstream-access-token` config option.
|
||||
|
||||
❤️ If you like ntfy, **please consider sponsoring me** via [GitHub Sponsors](https://github.com/sponsors/binwiederhier)
|
||||
and [Liberapay](https://en.liberapay.com/ntfy/), or by buying a [paid plan via the web app](https://ntfy.sh/app) (20% off
|
||||
if you use promo code `MYTOPIC`). ntfy will always remain open source.
|
||||
|
||||
**Features:**
|
||||
|
||||
* Support for text-to-speech style [phone calls](publish.md#phone-calls) using the `X-Call` header (no ticket)
|
||||
* Admin API to manage users and ACL, `v1/users` + `v1/users/access` (intentionally undocumented as of now, [#722](https://github.com/binwiederhier/ntfy/issues/722), thanks to [@CreativeWarlock](https://github.com/CreativeWarlock) for sponsoring this ticket)
|
||||
* Added `upstream-access-token` config option to allow authorized access to upstream servers (no ticket)
|
||||
|
||||
**Bug fixes + maintenance:**
|
||||
|
||||
* Removed old ntfy website from ntfy entirely (no ticket)
|
||||
* Make emoji lookup for emails more efficient ([#725](https://github.com/binwiederhier/ntfy/pull/725), thanks to [@adamantike](https://github.com/adamantike))
|
||||
* Fix potential subscriber ID clash ([#712](https://github.com/binwiederhier/ntfy/issues/712), thanks to [@peterbourgon](https://github.com/peterbourgon) for reporting, and [@dropdevrahul](https://github.com/dropdevrahul) for fixing)
|
||||
* Support for `quoted-printable` in incoming emails ([#719](https://github.com/binwiederhier/ntfy/pull/719), thanks to [@Aerion](https://github.com/Aerion))
|
||||
* Attachments with filenames that are downloaded using a browser will now download with the proper filename ([#726](https://github.com/binwiederhier/ntfy/issues/726), thanks to [@un99known99](https://github.com/un99known99) for reporting, and [@wunter8](https://github.com/wunter8) for fixing)
|
||||
* Fix web app i18n issue in account preferences ([#730](https://github.com/binwiederhier/ntfy/issues/730), thanks to [@codebude](https://github.com/codebude) for reporting)
|
||||
|
||||
## ntfy server v2.4.0
|
||||
Released Apr 26, 2023
|
||||
|
||||
This release adds a tiny `v1/stats` endpoint to expose how many messages have been published, and adds support to encode the `X-Title`,
|
||||
`X-Message` and `X-Tags` header as RFC 2047. It's a pretty small release, and mainly enables the release of the new ntfy.sh website.
|
||||
|
||||
❤️ If you like ntfy, **please consider sponsoring me** via [GitHub Sponsors](https://github.com/sponsors/binwiederhier)
|
||||
and [Liberapay](https://en.liberapay.com/ntfy/), or by buying a [paid plan via the web app](https://ntfy.sh/app). ntfy
|
||||
will always remain open source.
|
||||
|
||||
**Features:**
|
||||
|
||||
* [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) can now be installed via Homebrew (thanks to [@Moulick](https://github.com/Moulick))
|
||||
* Added `v1/stats` endpoint to expose messages stats (no ticket)
|
||||
* Support [RFC 2047](https://datatracker.ietf.org/doc/html/rfc2047#section-2) encoded headers (no ticket, honorable mention to [mqttwarn](https://github.com/jpmens/mqttwarn/pull/638) and [@amotl](https://github.com/amotl))
|
||||
|
||||
**Bug fixes + maintenance:**
|
||||
|
||||
* Hide country flags on Windows ([#606](https://github.com/binwiederhier/ntfy/issues/606), thanks to [@cmeis](https://github.com/cmeis) for reporting, and to [@pokej6](https://github.com/pokej6) for fixing it)
|
||||
* `ntfy sub` now uses default auth credentials as defined in `client.yml` ([#698](https://github.com/binwiederhier/ntfy/issues/698), thanks to [@CrimsonFez](https://github.com/CrimsonFez) for reporting, and to [@wunter8](https://github.com/wunter8) for fixing it)
|
||||
|
||||
**Documentation:**
|
||||
|
||||
* Updated PowerShell examples ([#697](https://github.com/binwiederhier/ntfy/pull/697), thanks to [@Natfan](https://github.com/Natfan))
|
||||
|
||||
**Additional languages:**
|
||||
|
||||
* Swedish (thanks to [@hellbown](https://hosted.weblate.org/user/Shjosan/))
|
||||
|
||||
## ntfy server v2.3.1
|
||||
Released March 30, 2023
|
||||
|
||||
This release disables server-initiated polling of iOS devices entirely, thereby eliminating the thundering herd problem
|
||||
on ntfy.sh that we observe every 20 minutes. The polling was never strictly necessary, and has actually caused duplicate
|
||||
delivery issues as well, so disabling it should not have any negative effects. iOS users, please reach out via Discord
|
||||
or Matrix if there are issues.
|
||||
|
||||
**Bug fixes + maintenance:**
|
||||
|
||||
* Disable iOS polling entirely ([#677](https://github.com/binwiederhier/ntfy/issues/677)/[#509](https://github.com/binwiederhier/ntfy/issues/509))
|
||||
|
||||
## ntfy server v2.3.0
|
||||
Released March 29, 2023
|
||||
|
||||
This release primarily fixes an issue with delayed messages, and it adds support for Go's profiler (if enabled), which
|
||||
will allow investigating usage spikes in more detail. There will likely be a follow-up release this week to fix the
|
||||
actual spikes [caused by iOS devices](https://github.com/binwiederhier/ntfy/issues/677).
|
||||
|
||||
**Features:**
|
||||
|
||||
* ntfy now supports Go's `pprof` profiler, if enabled (relates to [#677](https://github.com/binwiederhier/ntfy/issues/677))
|
||||
|
||||
**Bug fixes + maintenance:**
|
||||
|
||||
* Fix delayed message sending from authenticated users ([#679](https://github.com/binwiederhier/ntfy/issues/679))
|
||||
* Fixed plural for Polish and other translations ([#678](https://github.com/binwiederhier/ntfy/pull/678), thanks to [@bmoczulski](https://github.com/bmoczulski))
|
||||
|
||||
## ntfy server v2.2.0
|
||||
Released March 17, 2023
|
||||
|
||||
@@ -437,7 +915,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))
|
||||
@@ -1111,7 +1589,7 @@ Released Dec 28, 2021
|
||||
|
||||
**Features & bug fixes:**
|
||||
|
||||
* [Publish messages via e-mail](ntfy.sh/docs/publish/#e-mail-publishing) #66
|
||||
* [Publish messages via e-mail](publish.md#e-mail-publishing) #66
|
||||
* Server-side work to support [unifiedpush.org](https://unifiedpush.org) #64
|
||||
* Fixing the Santa bug #65
|
||||
|
||||
@@ -1121,17 +1599,33 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
|
||||
|
||||
## Not released yet
|
||||
|
||||
### ntfy Android app v1.16.1 (UNRELEASED)
|
||||
### ntfy server v2.16.x (UNRELEASED)
|
||||
|
||||
**Features:**
|
||||
|
||||
* You can now disable UnifiedPush so ntfy does not act as a UnifiedPush distributor ([#646](https://github.com/binwiederhier/ntfy/issues/646), thanks to [@ollien](https://github.com/ollien) for reporting and to [@wunter8](https://github.com/wunter8) for implementing)
|
||||
* Support for [updating and deleting notifications](publish.md#updating-deleting-notifications) ([#303](https://github.com/binwiederhier/ntfy/issues/303), [#1536](https://github.com/binwiederhier/ntfy/pull/1536),
|
||||
[ntfy-android#151](https://github.com/binwiederhier/ntfy-android/pull/151), thanks to [@wunter8](https://github.com/wunter8) for the initial implementation)
|
||||
* Configure [custom Twilio call format](config.md#phone-calls) for phone calls ([#1289](https://github.com/binwiederhier/ntfy/pull/1289), thanks to [@mmichaa](https://github.com/mmichaa) for the initial implementation)
|
||||
* `ntfy serve` now works on Windows, including support for running it as a Windows service ([#1104](https://github.com/binwiederhier/ntfy/issues/1104), [#1552](https://github.com/binwiederhier/ntfy/pull/1552), originally [#1328](https://github.com/binwiederhier/ntfy/pull/1328), thanks to [@wtf911](https://github.com/wtf911))
|
||||
* Web app: "New version available" banner ([#1554](https://github.com/binwiederhier/ntfy/pull/1554))
|
||||
|
||||
### ntfy Android app v1.22.x (UNRELEASED)
|
||||
|
||||
**Features:**
|
||||
|
||||
* Support for [updating and deleting notifications](publish.md#updating-deleting-notifications)
|
||||
([#303](https://github.com/binwiederhier/ntfy/issues/303), [#1536](https://github.com/binwiederhier/ntfy/pull/1536),
|
||||
[ntfy-android#151](https://github.com/binwiederhier/ntfy-android/pull/151), thanks to [@wunter8](https://github.com/wunter8)
|
||||
for the initial implementation)
|
||||
* Support for self-signed certs and client certs for mTLS ([#215](https://github.com/binwiederhier/ntfy/issues/215),
|
||||
[#530](https://github.com/binwiederhier/ntfy/issues/530), [ntfy-android#149](https://github.com/binwiederhier/ntfy-android/pull/149),
|
||||
thanks to [@cyb3rko](https://github.com/cyb3rko) for reviewing)
|
||||
* Connection error dialog to help diagnose connection issues
|
||||
|
||||
**Bug fixes + maintenance:**
|
||||
|
||||
* UnifiedPush subscriptions now include the `Rate-Topics` header to facilitate subscriber-based billing ([#652](https://github.com/binwiederhier/ntfy/issues/652), thanks to [@wunter8](https://github.com/wunter8))
|
||||
* Subscriptions without icons no longer appear to use another subscription's icon ([#634](https://github.com/binwiederhier/ntfy/issues/634), thanks to [@topcaser](https://github.com/topcaser) for reporting and to [@wunter8](https://github.com/wunter8) for fixing)
|
||||
|
||||
**Additional languages:**
|
||||
|
||||
* Swedish (thanks to [@hellbown](https://hosted.weblate.org/user/hellbown/))
|
||||
* Use server-specific user for attachment downloads ([#1529](https://github.com/binwiederhier/ntfy/issues/1529),
|
||||
thanks to [@ManInDark](https://github.com/ManInDark) for reporting and testing)
|
||||
* Fix crash in sharing dialog (thanks to [@rogeliodh](https://github.com/rogeliodh))
|
||||
* Fix crash when exiting multi-delete in detail view
|
||||
* Fix potential crashes with icon downloader and backuper
|
||||
|
||||
BIN
docs/static/audio/ntfy-phone-call.mp3
vendored
Normal file
BIN
docs/static/audio/ntfy-phone-call.ogg
vendored
Normal file
31
docs/static/css/extra.css
vendored
@@ -1,10 +1,10 @@
|
||||
:root > * {
|
||||
--md-primary-fg-color: #338574;
|
||||
--md-primary-fg-color: #338574;
|
||||
--md-primary-fg-color--light: #338574;
|
||||
--md-primary-fg-color--dark: #338574;
|
||||
--md-footer-bg-color: #353744;
|
||||
--md-text-font: "Roboto";
|
||||
--md-code-font: "Roboto Mono";
|
||||
--md-primary-fg-color--dark: #338574;
|
||||
--md-footer-bg-color: #353744;
|
||||
--md-text-font: "Roboto";
|
||||
--md-code-font: "Roboto Mono";
|
||||
}
|
||||
|
||||
.md-header__button.md-logo :is(img, svg) {
|
||||
@@ -34,7 +34,7 @@ figure img, figure video {
|
||||
}
|
||||
|
||||
header {
|
||||
background: linear-gradient(150deg, rgba(51,133,116,1) 0%, rgba(86,189,168,1) 100%);
|
||||
background: linear-gradient(150deg, rgba(51, 133, 116, 1) 0%, rgba(86, 189, 168, 1) 100%);
|
||||
}
|
||||
|
||||
body[data-md-color-scheme="default"] header {
|
||||
@@ -71,7 +71,18 @@ figure video {
|
||||
}
|
||||
|
||||
.remove-md-box td {
|
||||
padding: 0 10px
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.emoji-table .c {
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
|
||||
.emoji-table .e {
|
||||
font-size: 2.5em;
|
||||
padding: 0 2px !important;
|
||||
text-align: center !important;
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
|
||||
/* Lightbox; thanks to https://yossiabramov.com/blog/vanilla-js-lightbox */
|
||||
@@ -82,7 +93,7 @@ figure video {
|
||||
|
||||
.screenshots img {
|
||||
max-height: 230px;
|
||||
max-width: 300px;
|
||||
max-width: 350px;
|
||||
margin: 3px;
|
||||
border-radius: 5px;
|
||||
filter: drop-shadow(2px 2px 2px #ddd);
|
||||
@@ -96,7 +107,7 @@ figure video {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
position: fixed;
|
||||
left:0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
@@ -108,7 +119,7 @@ figure video {
|
||||
}
|
||||
|
||||
.lightbox.show {
|
||||
background-color: rgba(0,0,0, 0.75);
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
z-index: 1000;
|
||||
|
||||
BIN
docs/static/img/android-screenshot-logs.jpg
vendored
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
docs/static/img/android-screenshot-notification-update-1.png
vendored
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
docs/static/img/android-screenshot-notification-update-2.png
vendored
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
docs/static/img/android-screenshot-template-custom.png
vendored
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
docs/static/img/android-screenshot-template-predefined.png
vendored
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
docs/static/img/android-screenshot-template.jpg
vendored
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
docs/static/img/badge-appstore.png
vendored
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 24 KiB |
BIN
docs/static/img/badge-fdroid.png
vendored
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 17 KiB |
BIN
docs/static/img/badge-googleplay.png
vendored
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 4.6 KiB |
BIN
docs/static/img/cdio-setup.jpg
vendored
Normal file
|
After Width: | Height: | Size: 155 KiB |
BIN
docs/static/img/favicon.ico
vendored
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
docs/static/img/favicon.png
vendored
|
Before Width: | Height: | Size: 4.6 KiB |
BIN
docs/static/img/grafana-dashboard.png
vendored
Normal file
|
After Width: | Height: | Size: 334 KiB |
BIN
docs/static/img/mobile-screenshot-notification.png
vendored
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
docs/static/img/pwa-badge.png
vendored
Normal file
|
After Width: | Height: | Size: 185 KiB |
BIN
docs/static/img/pwa-install-chrome-android-menu.jpg
vendored
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
docs/static/img/pwa-install-chrome-android-popup.jpg
vendored
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
docs/static/img/pwa-install-chrome-android.jpg
vendored
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
docs/static/img/pwa-install-firefox-android-menu.jpg
vendored
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
docs/static/img/pwa-install-firefox-android-popup.jpg
vendored
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
docs/static/img/pwa-install-macos-safari-add-to-dock.png
vendored
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
docs/static/img/pwa-install-safari-ios-add-icon.jpg
vendored
Normal file
|
After Width: | Height: | Size: 100 KiB |