Compare commits

...

59 Commits

Author SHA1 Message Date
binwiederhier
5ee62033b5 Bump 2024-03-25 17:21:23 -04:00
binwiederhier
3e02d7b0bb Bump 2024-03-25 17:20:27 -04:00
binwiederhier
290ed1124e Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web 2024-03-25 17:12:04 -04:00
binwiederhier
fc62682334 Thank you @jceloria for your donation 2024-03-25 09:42:09 -04:00
binwiederhier
28404565d2 Thank you @Circenn5130 for your donation 2024-03-25 09:41:37 -04:00
binwiederhier
f8548e9d46 Thank you @tomroth04 for your donation 2024-03-25 09:41:08 -04:00
Philipp C. Heckel
d90b290cd2 Merge pull request #1066 from binwiederhier/templating-disallow
Disallow certain templates
2024-03-25 09:38:33 -04:00
binwiederhier
21c6776269 Fix linter 2024-03-25 09:34:44 -04:00
binwiederhier
7fed392e0c Disallow certain templates 2024-03-24 23:19:16 -04:00
Philipp C. Heckel
913b59b5e3 Merge pull request #1064 from binwiederhier/templating-3
Message templating
2024-03-24 14:47:32 -04:00
binwiederhier
4692ca7b7f REvert parallel tests 2024-03-24 14:36:14 -04:00
binwiederhier
af16542d02 Removed unused vars 2024-03-24 14:28:10 -04:00
binwiederhier
5511812e30 JSON templating 2024-03-24 14:21:28 -04:00
binwiederhier
547b09a7e5 docs 2024-03-23 14:22:56 -04:00
binwiederhier
b9c176ddba Tests 2024-03-22 22:01:41 -04:00
Philipp C. Heckel
f971377cbb Merge pull request #1062 from theparadox1083/patch-1
Update publish.md (PowerShell 7+ Access Token Example)
2024-03-22 21:43:00 -04:00
binwiederhier
a04f2f9c9a Bla 2024-03-22 20:45:16 -04:00
Patrick
763eafd5dd Update publish.md (PowerShell 7+ Access Token Example)
Corrected PowerShell 7+ Parameter from Authorization to Authentication
Converted Token text to SecureString Inline - token must be of type SecureString
Added comment noting the Token parameter must be a SecureString
2024-03-21 09:38:55 -04:00
binwiederhier
9247dac50d Move things, revert naming 2024-03-20 21:39:17 -04:00
binwiederhier
de65d07518 Simplify(?) templating cases 2024-03-20 21:33:54 -04:00
binwiederhier
1966f80855 Merge branch 'main' into templating 2024-03-20 20:27:12 -04:00
binwiederhier
4b2e38320d BUmp 2024-03-20 20:26:18 -04:00
wunter8
83356f565e remove debug print statement 2024-03-20 10:54:41 -06:00
Hunter Kehoe
7fd5f0b29d allow large HTTP body so long as resulting message is small 2024-03-19 21:56:55 -06:00
Hunter Kehoe
03737dbf5c update docs 2024-03-19 20:55:36 -06:00
Hunter Kehoe
867cf28080 refactor gjson parsing code 2024-03-19 20:30:24 -06:00
Hunter Kehoe
b2eb5b94bd use existing message and title fields for templates 2024-03-18 20:04:40 -06:00
Hunter Kehoe
df7d6baec5 add templating for title and message fields 2024-03-17 21:55:50 -06:00
josé m
a4f5c8dee7 Translated using Weblate (Galician)
Currently translated at 100.0% (405 of 405 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/gl/
2024-03-12 13:01:53 +01:00
binwiederhier
4c0ec3f75b Thank you @arthurgleckler for your generous donation 2024-03-10 22:47:19 -04:00
binwiederhier
12bbe9a1ae Thank you @danbartram for your sponsorship 2024-03-10 22:44:43 -04:00
Mazurky
0a589f6242 Translated using Weblate (Slovak)
Currently translated at 100.0% (405 of 405 strings)

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

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

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

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

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/tr/
2024-03-09 16:15:59 +01:00
Philipp C. Heckel
7936c38feb Update README.md 2024-03-08 12:32:37 -05:00
binwiederhier
d0beaa900f Thank you @stannynuytkens for your sponsorship 2024-03-08 11:12:55 -05:00
Linerly
f4bf8fd9bb Translated using Weblate (Indonesian)
Currently translated at 99.7% (404 of 405 strings)

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

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/bg/
2024-03-08 07:42:47 +01:00
binwiederhier
0ab6171962 Blog posts 2024-03-07 23:07:07 -05:00
binwiederhier
b7c2898b9c Thank you @dav23r for your sponsorship 2024-03-07 21:24:33 -05:00
binwiederhier
d155ebb3a4 Thank you @TheTomik1 for your donation 2024-03-07 21:24:07 -05:00
binwiederhier
3d5bb92774 Thank you @Integral-Tech for your sponsorship 2024-03-07 21:23:35 -05:00
binwiederhier
a2f1a45097 Thank you @rwxd for your sponsorship 2024-03-07 21:22:59 -05:00
binwiederhier
b43fff7c7e Thank you @hermannx5 for your donation 2024-03-07 21:22:02 -05:00
binwiederhier
b186c7a324 Thank you for your donation @rxsantos 2024-03-07 21:21:15 -05:00
binwiederhier
e50b6f4075 Thank you @siebej for your donation 2024-03-07 21:19:21 -05:00
binwiederhier
e5342d5eca Thank you @brookmg for your donation 2024-03-07 21:18:55 -05:00
binwiederhier
a25fc1daaa Thank you @dwain-lab for your donation 2024-03-07 21:18:25 -05:00
binwiederhier
6e75108aa9 Thank you @vovayartsev for your sponsorship 2024-03-07 21:17:40 -05:00
binwiederhier
0735a5cb48 Thank you @FrameXX for your donation 2024-03-07 21:17:13 -05:00
binwiederhier
088aede833 Thank you @mursec for your donation 2024-03-07 21:15:58 -05:00
binwiederhier
2119954f67 Thank you @ajay-actuary for your sponsorship 2024-03-07 21:15:00 -05:00
binwiederhier
220b012ae3 Thank you @CataIana for your donation 2024-03-07 21:11:29 -05:00
binwiederhier
e1278a5e92 Thank you @tomershvueli for your donation 2024-03-07 21:10:46 -05:00
binwiederhier
0ae62d36d2 Thank you @zark0s for your sponsorship 2024-03-07 21:10:00 -05:00
binwiederhier
70729edb2b Thank you @Emiliaaah for your donation 2024-03-07 21:09:14 -05:00
binwiederhier
de2f7d3e9b Remove Lemmy 2024-03-07 21:06:46 -05:00
24 changed files with 1639 additions and 1939 deletions

View File

@@ -9,7 +9,6 @@
[![Discord](https://img.shields.io/discord/874398661709295626?label=Discord)](https://discord.gg/cT7ECsZj9w) [![Discord](https://img.shields.io/discord/874398661709295626?label=Discord)](https://discord.gg/cT7ECsZj9w)
[![Matrix](https://img.shields.io/matrix/ntfy:matrix.org?label=Matrix)](https://matrix.to/#/#ntfy:matrix.org) [![Matrix](https://img.shields.io/matrix/ntfy:matrix.org?label=Matrix)](https://matrix.to/#/#ntfy:matrix.org)
[![Matrix space](https://img.shields.io/matrix/ntfy-space:matrix.org?label=Matrix+space)](https://matrix.to/#/#ntfy-space:matrix.org) [![Matrix space](https://img.shields.io/matrix/ntfy-space:matrix.org?label=Matrix+space)](https://matrix.to/#/#ntfy-space:matrix.org)
[![Lemmy](https://img.shields.io/badge/Lemmy-discuss-green)](https://discuss.ntfy.sh/c/ntfy)
[![Healthcheck](https://healthchecks.io/badge/68b65976-b3b0-4102-aec9-980921/kcoEgrLY.svg)](https://ntfy.statuspage.io/) [![Healthcheck](https://healthchecks.io/badge/68b65976-b3b0-4102-aec9-980921/kcoEgrLY.svg)](https://ntfy.statuspage.io/)
[![Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod)](https://gitpod.io/#https://github.com/binwiederhier/ntfy) [![Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod)](https://gitpod.io/#https://github.com/binwiederhier/ntfy)
@@ -50,7 +49,6 @@ works best for you:
* [Discord server](https://discord.gg/cT7ECsZj9w) - direct chat with the community * [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 * [Matrix room #ntfy](https://matrix.to/#/#ntfy:matrix.org) (+ [Matrix space](https://matrix.to/#/#ntfy-space:matrix.org)) - same chat, bridged from Discord
* [Lemmy discussion board](https://discuss.ntfy.sh/c/ntfy) - asynchronous forum (_new as of June 2023_)
* [GitHub issues](https://github.com/binwiederhier/ntfy/issues) - questions, features, bugs * [GitHub issues](https://github.com/binwiederhier/ntfy/issues) - questions, features, bugs
## Announcements/beta testers ## Announcements/beta testers
@@ -71,7 +69,7 @@ for the server and the Android app. Or, if you'd like to help translate 🇩🇪
## Sponsors ## Sponsors
I have just very recently started accepting donations via [GitHub Sponsors](https://github.com/sponsors/binwiederhier), 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 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: account costs. Even small donations are very much appreciated. A big fat **Thank You** to the folks who have sponsored ntfy in the past, or are still sponsoring ntfy:
<a href="https://github.com/neutralinsomniac"><img src="https://github.com/neutralinsomniac.png" width="40px" /></a> <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> <a href="https://github.com/aspyct"><img src="https://github.com/aspyct.png" width="40px" /></a>
@@ -168,6 +166,29 @@ account costs. Even small donations are very much appreciated. A big fat **Thank
<a href="https://github.com/ubipo"><img src="https://github.com/ubipo.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/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/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>
I'd also like to thank JetBrains for their awesome [IntelliJ IDEA](https://www.jetbrains.com/idea/), I'd also like to thank JetBrains for their awesome [IntelliJ IDEA](https://www.jetbrains.com/idea/),
and [DigitalOcean](https://m.do.co/c/442b929528db) (*referral link*) for supporting the project: and [DigitalOcean](https://m.do.co/c/442b929528db) (*referral link*) for supporting the project:

View File

@@ -30,37 +30,37 @@ deb/rpm packages.
=== "x86_64/amd64" === "x86_64/amd64"
```bash ```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_amd64.tar.gz wget https://github.com/binwiederhier/ntfy/releases/download/v2.10.0/ntfy_2.10.0_linux_amd64.tar.gz
tar zxvf ntfy_2.9.0_linux_amd64.tar.gz tar zxvf ntfy_2.10.0_linux_amd64.tar.gz
sudo cp -a ntfy_2.9.0_linux_amd64/ntfy /usr/local/bin/ntfy sudo cp -a ntfy_2.10.0_linux_amd64/ntfy /usr/local/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.9.0_linux_amd64/{client,server}/*.yml /etc/ntfy sudo mkdir /etc/ntfy && sudo cp ntfy_2.10.0_linux_amd64/{client,server}/*.yml /etc/ntfy
sudo ntfy serve sudo ntfy serve
``` ```
=== "armv6" === "armv6"
```bash ```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_armv6.tar.gz wget https://github.com/binwiederhier/ntfy/releases/download/v2.10.0/ntfy_2.10.0_linux_armv6.tar.gz
tar zxvf ntfy_2.9.0_linux_armv6.tar.gz tar zxvf ntfy_2.10.0_linux_armv6.tar.gz
sudo cp -a ntfy_2.9.0_linux_armv6/ntfy /usr/bin/ntfy sudo cp -a ntfy_2.10.0_linux_armv6/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.9.0_linux_armv6/{client,server}/*.yml /etc/ntfy sudo mkdir /etc/ntfy && sudo cp ntfy_2.10.0_linux_armv6/{client,server}/*.yml /etc/ntfy
sudo ntfy serve sudo ntfy serve
``` ```
=== "armv7/armhf" === "armv7/armhf"
```bash ```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_armv7.tar.gz wget https://github.com/binwiederhier/ntfy/releases/download/v2.10.0/ntfy_2.10.0_linux_armv7.tar.gz
tar zxvf ntfy_2.9.0_linux_armv7.tar.gz tar zxvf ntfy_2.10.0_linux_armv7.tar.gz
sudo cp -a ntfy_2.9.0_linux_armv7/ntfy /usr/bin/ntfy sudo cp -a ntfy_2.10.0_linux_armv7/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.9.0_linux_armv7/{client,server}/*.yml /etc/ntfy sudo mkdir /etc/ntfy && sudo cp ntfy_2.10.0_linux_armv7/{client,server}/*.yml /etc/ntfy
sudo ntfy serve sudo ntfy serve
``` ```
=== "arm64" === "arm64"
```bash ```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_arm64.tar.gz wget https://github.com/binwiederhier/ntfy/releases/download/v2.10.0/ntfy_2.10.0_linux_arm64.tar.gz
tar zxvf ntfy_2.9.0_linux_arm64.tar.gz tar zxvf ntfy_2.10.0_linux_arm64.tar.gz
sudo cp -a ntfy_2.9.0_linux_arm64/ntfy /usr/bin/ntfy sudo cp -a ntfy_2.10.0_linux_arm64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.9.0_linux_arm64/{client,server}/*.yml /etc/ntfy sudo mkdir /etc/ntfy && sudo cp ntfy_2.10.0_linux_arm64/{client,server}/*.yml /etc/ntfy
sudo ntfy serve sudo ntfy serve
``` ```
@@ -110,7 +110,7 @@ Manually installing the .deb file:
=== "x86_64/amd64" === "x86_64/amd64"
```bash ```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_amd64.deb wget https://github.com/binwiederhier/ntfy/releases/download/v2.10.0/ntfy_2.10.0_linux_amd64.deb
sudo dpkg -i ntfy_*.deb sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy sudo systemctl enable ntfy
sudo systemctl start ntfy sudo systemctl start ntfy
@@ -118,7 +118,7 @@ Manually installing the .deb file:
=== "armv6" === "armv6"
```bash ```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_armv6.deb wget https://github.com/binwiederhier/ntfy/releases/download/v2.10.0/ntfy_2.10.0_linux_armv6.deb
sudo dpkg -i ntfy_*.deb sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy sudo systemctl enable ntfy
sudo systemctl start ntfy sudo systemctl start ntfy
@@ -126,7 +126,7 @@ Manually installing the .deb file:
=== "armv7/armhf" === "armv7/armhf"
```bash ```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_armv7.deb wget https://github.com/binwiederhier/ntfy/releases/download/v2.10.0/ntfy_2.10.0_linux_armv7.deb
sudo dpkg -i ntfy_*.deb sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy sudo systemctl enable ntfy
sudo systemctl start ntfy sudo systemctl start ntfy
@@ -134,7 +134,7 @@ Manually installing the .deb file:
=== "arm64" === "arm64"
```bash ```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_arm64.deb wget https://github.com/binwiederhier/ntfy/releases/download/v2.10.0/ntfy_2.10.0_linux_arm64.deb
sudo dpkg -i ntfy_*.deb sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy sudo systemctl enable ntfy
sudo systemctl start ntfy sudo systemctl start ntfy
@@ -144,28 +144,28 @@ Manually installing the .deb file:
=== "x86_64/amd64" === "x86_64/amd64"
```bash ```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_amd64.rpm sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.10.0/ntfy_2.10.0_linux_amd64.rpm
sudo systemctl enable ntfy sudo systemctl enable ntfy
sudo systemctl start ntfy sudo systemctl start ntfy
``` ```
=== "armv6" === "armv6"
```bash ```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_armv6.rpm sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.10.0/ntfy_2.10.0_linux_armv6.rpm
sudo systemctl enable ntfy sudo systemctl enable ntfy
sudo systemctl start ntfy sudo systemctl start ntfy
``` ```
=== "armv7/armhf" === "armv7/armhf"
```bash ```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_armv7.rpm sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.10.0/ntfy_2.10.0_linux_armv7.rpm
sudo systemctl enable ntfy sudo systemctl enable ntfy
sudo systemctl start ntfy sudo systemctl start ntfy
``` ```
=== "arm64" === "arm64"
```bash ```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_arm64.rpm sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.10.0/ntfy_2.10.0_linux_arm64.rpm
sudo systemctl enable ntfy sudo systemctl enable ntfy
sudo systemctl start ntfy sudo systemctl start ntfy
``` ```
@@ -195,18 +195,18 @@ NixOS also supports [declarative setup of the ntfy server](https://search.nixos.
## macOS ## macOS
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on macOS as well. The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on macOS as well.
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_darwin_all.tar.gz), To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.10.0/ntfy_2.10.0_darwin_all.tar.gz),
extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`). 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 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). `~/Library/Application Support/ntfy/client.yml` (sample included in the tarball).
```bash ```bash
curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_darwin_all.tar.gz > ntfy_2.9.0_darwin_all.tar.gz curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.10.0/ntfy_2.10.0_darwin_all.tar.gz > ntfy_2.10.0_darwin_all.tar.gz
tar zxvf ntfy_2.9.0_darwin_all.tar.gz tar zxvf ntfy_2.10.0_darwin_all.tar.gz
sudo cp -a ntfy_2.9.0_darwin_all/ntfy /usr/local/bin/ntfy sudo cp -a ntfy_2.10.0_darwin_all/ntfy /usr/local/bin/ntfy
mkdir ~/Library/Application\ Support/ntfy mkdir ~/Library/Application\ Support/ntfy
cp ntfy_2.9.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml cp ntfy_2.10.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
ntfy --help ntfy --help
``` ```
@@ -224,7 +224,7 @@ brew install ntfy
## Windows ## Windows
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well. The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well.
To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_windows_amd64.zip), To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.10.0/ntfy_2.10.0_windows_amd64.zip),
extract it and place the `ntfy.exe` binary somewhere in your `%Path%`. extract it and place the `ntfy.exe` binary somewhere in your `%Path%`.
The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file). The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file).

View File

@@ -144,9 +144,18 @@ I've added a ⭐ to projects or posts that have a significant following, or had
## Blog + forum posts ## Blog + forum posts
- [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 - [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
- [Lets 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 - [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 - [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 - [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 - [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 - [Homelab Notifications with ntfy](https://blog.alexsguardian.net/posts/2023/09/12/selfhosting-ntfy/) ⭐ - alexsguardian.net - 9/2023

File diff suppressed because one or more lines are too long

View File

@@ -2,13 +2,28 @@
Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases) 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). and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).
### 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 ### ntfy server v2.9.0
Released Mar 7, 2024 Released Mar 7, 2024
A small release after a long pause (lots of day job work). This release adds for **larger messages** and **longer message delays** in scheduled delivery messages. The web app also now supports pasting images from the clipboard. Other than that, only a few bug fixes and documentation updates, and a teeny tiny breaking change 😬. A small release after a long pause (lots of day job work). This release adds for **larger messages** and **longer
message delays** in scheduled delivery messages. The web app also now supports pasting images from the clipboard. Other
than that, only a few bug fixes and documentation updates, and a teeny tiny breaking change 😬.
!!! info !!! info
⚠️ **Breaking change**: The `Rate-Topics` header was removed due to a [DoS issue](https://github.com/binwiederhier/ntfy/issues/1048). This only affects installations with `visitor-subscriber-rate-limiting: true`, which is not the default and likely very rarely used. Normally I'd never remove a feature, but this is a security issue, and likely affects almost nobody. ⚠️ **Breaking change**: The `Rate-Topics` header was removed due to a [DoS issue](https://github.com/binwiederhier/ntfy/issues/1048). This only affects
installations with `visitor-subscriber-rate-limiting: true`, which is not the default and likely very rarely used.
Normally I'd never remove a feature, but this is a security issue, and likely affects almost nobody.
**Features:** **Features:**
@@ -30,7 +45,9 @@ A small release after a long pause (lots of day job work). This release adds for
## ntfy iOS app v1.3 ## ntfy iOS app v1.3
Released Nov 26, 2023 Released Nov 26, 2023
This release (hopefully) fixes the issues with the iOS UI not updating properly when new notifications arrive, as well as notifications not being received (anymore) after previously working. Both issues have been annoying and known bugs for a long time, and I hope that they are finally fixed. This release (hopefully) fixes the issues with the iOS UI not updating properly when new notifications arrive, as well
as notifications not being received (anymore) after previously working. Both issues have been annoying and known bugs
for a long time, and I hope that they are finally fixed.
Many thanks to [@tcaputi](https://github.com/tcaputi) for fixing the issues, and to the anonymous donor for sponsoring these fixes. Many thanks to [@tcaputi](https://github.com/tcaputi) for fixing the issues, and to the anonymous donor for sponsoring these fixes.
@@ -41,7 +58,10 @@ Many thanks to [@tcaputi](https://github.com/tcaputi) for fixing the issues, and
## ntfy server v2.8.0 ## ntfy server v2.8.0
Released November 19, 2023 Released November 19, 2023
This release brings a handful of random bug fixes: two unrelated access control list fixes, a fix around web app crashes for languages with underscores in the language code (e.g. `zh_Hant`, `zh_Hans`, `pt_BR`, ...), a workaround for the `Priority` header (often used in Cloudflare setups), and support among others support for HTML-only emails (finally), web app crash fixes This release brings a handful of random bug fixes: two unrelated access control list fixes, a fix around web app crashes
for languages with underscores in the language code (e.g. `zh_Hant`, `zh_Hans`, `pt_BR`, ...), a workaround for the
`Priority` header (often used in Cloudflare setups), and support among others support for HTML-only emails (finally),
web app crash fixes
**Bug fixes + maintenance:** **Bug fixes + maintenance:**

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

View File

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

24
go.mod
View File

@@ -6,9 +6,9 @@ toolchain go1.21.3
require ( require (
cloud.google.com/go/firestore v1.15.0 // indirect cloud.google.com/go/firestore v1.15.0 // indirect
cloud.google.com/go/storage v1.39.0 // indirect cloud.google.com/go/storage v1.39.1 // indirect
github.com/BurntSushi/toml v1.3.2 // indirect github.com/BurntSushi/toml v1.3.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/emersion/go-smtp v0.18.0 github.com/emersion/go-smtp v0.18.0
github.com/gabriel-vasile/mimetype v1.4.3 github.com/gabriel-vasile/mimetype v1.4.3
github.com/gorilla/websocket v1.5.1 github.com/gorilla/websocket v1.5.1
@@ -21,7 +21,7 @@ require (
golang.org/x/sync v0.6.0 golang.org/x/sync v0.6.0
golang.org/x/term v0.18.0 golang.org/x/term v0.18.0
golang.org/x/time v0.5.0 golang.org/x/time v0.5.0
google.golang.org/api v0.168.0 google.golang.org/api v0.171.0
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
) )
@@ -39,10 +39,10 @@ require (
require ( require (
cloud.google.com/go v0.112.1 // indirect cloud.google.com/go v0.112.1 // indirect
cloud.google.com/go/compute v1.25.0 // indirect cloud.google.com/go/compute v1.25.1 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v1.1.6 // indirect cloud.google.com/go/iam v1.1.7 // indirect
cloud.google.com/go/longrunning v0.5.5 // indirect cloud.google.com/go/longrunning v0.5.6 // indirect
github.com/AlekSi/pointer v1.2.0 // indirect github.com/AlekSi/pointer v1.2.0 // indirect
github.com/MicahParks/keyfunc v1.9.0 // indirect github.com/MicahParks/keyfunc v1.9.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
@@ -60,16 +60,16 @@ require (
github.com/google/s2a-go v0.1.7 // indirect github.com/google/s2a-go v0.1.7 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.2 // indirect github.com/googleapis/gax-go/v2 v2.12.3 // indirect
github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/css v1.0.1 // indirect
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.0 // indirect github.com/prometheus/client_model v0.6.0 // indirect
github.com/prometheus/common v0.50.0 // indirect github.com/prometheus/common v0.51.1 // indirect
github.com/prometheus/procfs v0.13.0 // indirect github.com/prometheus/procfs v0.13.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect github.com/stretchr/objx v0.5.0 // indirect
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
go.opencensus.io v0.24.0 // indirect go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
@@ -81,9 +81,9 @@ require (
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect
google.golang.org/appengine v1.6.8 // indirect google.golang.org/appengine v1.6.8 // indirect
google.golang.org/appengine/v2 v2.0.5 // indirect google.golang.org/appengine/v2 v2.0.5 // indirect
google.golang.org/genproto v0.0.0-20240304212257-790db918fca8 // indirect google.golang.org/genproto v0.0.0-20240325203815-454cdb8f5daa // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240304212257-790db918fca8 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240325203815-454cdb8f5daa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240304212257-790db918fca8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240325203815-454cdb8f5daa // indirect
google.golang.org/grpc v1.62.1 // indirect google.golang.org/grpc v1.62.1 // indirect
google.golang.org/protobuf v1.33.0 // indirect google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect

54
go.sum
View File

@@ -1,18 +1,18 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM= cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM=
cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4= cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4=
cloud.google.com/go/compute v1.25.0 h1:H1/4SqSUhjPFE7L5ddzHOfY2bCAvjwNRZPNl6Ni5oYU= cloud.google.com/go/compute v1.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU=
cloud.google.com/go/compute v1.25.0/go.mod h1:GR7F0ZPZH8EhChlMo9FkLd7eUTwEymjqQagxzilIxIE= cloud.google.com/go/compute v1.25.1/go.mod h1:oopOIR53ly6viBYxaDhBfJwzUAxf1zE//uf3IB011ls=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/firestore v1.15.0 h1:/k8ppuWOtNuDHt2tsRV42yI21uaGnKDEQnRFeBpbFF8= cloud.google.com/go/firestore v1.15.0 h1:/k8ppuWOtNuDHt2tsRV42yI21uaGnKDEQnRFeBpbFF8=
cloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk= cloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk=
cloud.google.com/go/iam v1.1.6 h1:bEa06k05IO4f4uJonbB5iAgKTPpABy1ayxaIZV/GHVc= cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM=
cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI= cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA=
cloud.google.com/go/longrunning v0.5.5 h1:GOE6pZFdSrTb4KAiKnXsJBtlE6mEyaW44oKyMILWnOg= cloud.google.com/go/longrunning v0.5.6 h1:xAe8+0YaWoCKr9t1+aWe+OeQgN/iJK1fEgZSXmjuEaE=
cloud.google.com/go/longrunning v0.5.5/go.mod h1:WV2LAxD8/rg5Z1cNW6FJ/ZpX4E4VnDnoTk0yawPBB7s= cloud.google.com/go/longrunning v0.5.6/go.mod h1:vUaDrWYOMKRuhiv6JBnn49YxCPz2Ayn9GqyjaBT8/mA=
cloud.google.com/go/storage v1.39.0 h1:brbjUa4hbDHhpQf48tjqMaXEV+f1OGoaTmQau9tmCsA= cloud.google.com/go/storage v1.39.1 h1:MvraqHKhogCOTXTlct/9C3K3+Uy2jBmFYb3/Sp6dVtY=
cloud.google.com/go/storage v1.39.0/go.mod h1:OAEj/WZwUYjA3YHQ10/YcN9ttGuEpLwvaoyBXIPikEk= cloud.google.com/go/storage v1.39.1/go.mod h1:xK6xZmxZmo+fyP7+DEF6FhNc24/JAe95OLyOHCXFH1o=
firebase.google.com/go/v4 v4.13.0 h1:meFz9nvDNh/FDyrEykoAzSfComcQbmnQSjoHrePRqeI= firebase.google.com/go/v4 v4.13.0 h1:meFz9nvDNh/FDyrEykoAzSfComcQbmnQSjoHrePRqeI=
firebase.google.com/go/v4 v4.13.0/go.mod h1:e1/gaR6EnbQfsmTnAMx1hnz+ninJIrrr/RAh59Tpfn8= firebase.google.com/go/v4 v4.13.0/go.mod h1:e1/gaR6EnbQfsmTnAMx1hnz+ninJIrrr/RAh59Tpfn8=
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
@@ -33,8 +33,8 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -98,8 +98,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/gax-go/v2 v2.12.2 h1:mhN09QQW1jEWeMF74zGR81R30z4VJzjZsfkUhuHF+DA= github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA=
github.com/googleapis/gax-go/v2 v2.12.2/go.mod h1:61M8vcyyXR2kqKFxKrfA22jaA8JGF7Dc8App1U3H6jc= github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
@@ -125,6 +125,8 @@ github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZ
github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
github.com/prometheus/common v0.50.0 h1:YSZE6aa9+luNa2da6/Tik0q0A5AbR+U003TItK57CPQ= github.com/prometheus/common v0.50.0 h1:YSZE6aa9+luNa2da6/Tik0q0A5AbR+U003TItK57CPQ=
github.com/prometheus/common v0.50.0/go.mod h1:wHFBCEVWVmHMUpg7pYcOm2QUR/ocQdYSJVQJKnHc3xQ= github.com/prometheus/common v0.50.0/go.mod h1:wHFBCEVWVmHMUpg7pYcOm2QUR/ocQdYSJVQJKnHc3xQ=
github.com/prometheus/common v0.51.1 h1:eIjN50Bwglz6a/c3hAgSMcofL3nD+nFQkV6Dd4DsQCw=
github.com/prometheus/common v0.51.1/go.mod h1:lrWtQx+iDfn2mbH5GUzlH9TSHyfZpHkSiG1W7y3sF2Q=
github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o= github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g= github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
@@ -145,8 +147,8 @@ github.com/stripe/stripe-go/v74 v74.30.0 h1:0Kf0KkeFnY7iRhOwvTerX0Ia1BRw+eV1CVJ5
github.com/stripe/stripe-go/v74 v74.30.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw= github.com/stripe/stripe-go/v74 v74.30.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw=
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
@@ -240,8 +242,10 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/api v0.168.0 h1:MBRe+Ki4mMN93jhDDbpuRLjRddooArz4FeSObvUMmjY= google.golang.org/api v0.170.0 h1:zMaruDePM88zxZBG+NG8+reALO2rfLhe/JShitLyT48=
google.golang.org/api v0.168.0/go.mod h1:gpNOiMA2tZ4mf5R9Iwf4rK/Dcz0fbdIgWYWVoxmsyLg= google.golang.org/api v0.170.0/go.mod h1:/xql9M2btF85xac/VAm4PsLMTLVGUOpq4BE9R8jyNy8=
google.golang.org/api v0.171.0 h1:w174hnBPqut76FzW5Qaupt7zY8Kql6fiVjgys4f58sU=
google.golang.org/api v0.171.0/go.mod h1:Hnq5AHm4OTMt2BUVjael2CWZFD6vksJdWCWiUAmjC9o=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
@@ -251,12 +255,18 @@ google.golang.org/appengine/v2 v2.0.5/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20240304212257-790db918fca8 h1:Fe8QycXyEd9mJgnwB9kmw00WgB43eQ/xYO5C6gceybQ= google.golang.org/genproto v0.0.0-20240318140521-94a12d6c2237 h1:PgNlNSx2Nq2/j4juYzQBG0/Zdr+WP4z5N01Vk4VYBCY=
google.golang.org/genproto v0.0.0-20240304212257-790db918fca8/go.mod h1:yA7a1bW1kwl459Ol0m0lV4hLTfrL/7Bkk4Mj2Ir1mWI= google.golang.org/genproto v0.0.0-20240318140521-94a12d6c2237/go.mod h1:9sVD8c25Af3p0rGs7S7LLsxWKFiJt/65LdSyqXBkX/Y=
google.golang.org/genproto/googleapis/api v0.0.0-20240304212257-790db918fca8 h1:8eadJkXbwDEMNwcB5O0s5Y5eCfyuCLdvaiOIaGTrWmQ= google.golang.org/genproto v0.0.0-20240325203815-454cdb8f5daa h1:ePqxpG3LVx+feAUOx8YmR5T7rc0rdzK8DyxM8cQ9zq0=
google.golang.org/genproto/googleapis/api v0.0.0-20240304212257-790db918fca8/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y= google.golang.org/genproto v0.0.0-20240325203815-454cdb8f5daa/go.mod h1:CnZenrTdRJb7jc+jOm0Rkywq+9wh0QC4U8tyiRbEPPM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240304212257-790db918fca8 h1:IR+hp6ypxjH24bkMfEJ0yHR21+gwPWdV+/IBrPQyn3k= google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240304212257-790db918fca8/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs= google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE=
google.golang.org/genproto/googleapis/api v0.0.0-20240325203815-454cdb8f5daa h1:Jt1XW5PaLXF1/ePZrznsh/aAUvI7Adfc3LY1dAKlzRs=
google.golang.org/genproto/googleapis/api v0.0.0-20240325203815-454cdb8f5daa/go.mod h1:K4kfzHtI0kqWA79gecJarFtDn/Mls+GxQcg3Zox91Ac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240325203815-454cdb8f5daa h1:RBgMaUMP+6soRkik4VoN8ojR2nex2TqZwjSSogic+eo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240325203815-454cdb8f5daa/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=

View File

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

View File

@@ -23,6 +23,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"text/template"
"time" "time"
"unicode/utf8" "unicode/utf8"
@@ -123,15 +124,22 @@ var (
const ( const (
firebaseControlTopic = "~control" // See Android if changed firebaseControlTopic = "~control" // See Android if changed
firebasePollTopic = "~poll" // See iOS if changed firebasePollTopic = "~poll" // See iOS if changed (DISABLED for now)
emptyMessageBody = "triggered" // Used if message body is empty emptyMessageBody = "triggered" // Used if message body is empty
newMessageBody = "New message" // Used in poll requests as generic message newMessageBody = "New message" // Used in poll requests as generic message
defaultAttachmentMessage = "You received a file: %s" // Used if message body is empty, and there is an attachment defaultAttachmentMessage = "You received a file: %s" // Used if message body is empty, and there is an attachment
encodingBase64 = "base64" // Used mainly for binary UnifiedPush messages encodingBase64 = "base64" // Used mainly for binary UnifiedPush messages
jsonBodyBytesLimit = 16384 // Max number of bytes for a JSON request body jsonBodyBytesLimit = 32768 // Max number of bytes for a request bodys (unless MessageLimit is higher)
unifiedPushTopicPrefix = "up" // Temporarily, we rate limit all "up*" topics based on the subscriber unifiedPushTopicPrefix = "up" // Temporarily, we rate limit all "up*" topics based on the subscriber
unifiedPushTopicLength = 14 // Length of UnifiedPush topics, including the "up" part unifiedPushTopicLength = 14 // Length of UnifiedPush topics, including the "up" part
messagesHistoryMax = 10 // Number of message count values to keep in memory messagesHistoryMax = 10 // Number of message count values to keep in memory
templateMaxExecutionTime = 100 * time.Millisecond
)
var (
// templateDisallowedRegex tests a template for disallowed expressions. While not really dangerous, they
// are not useful, and seem potentially troublesome.
templateDisallowedRegex = regexp.MustCompile(`(?m)\{\{-?\s*(call|template|define)\b`)
) )
// WebSocket constants // WebSocket constants
@@ -673,7 +681,7 @@ func (s *Server) handleFile(w http.ResponseWriter, r *http.Request, v *visitor)
// - avoid abuse (e.g. 1 uploader, 1k downloaders) // - avoid abuse (e.g. 1 uploader, 1k downloaders)
// - and also uses the higher bandwidth limits of a paying user // - and also uses the higher bandwidth limits of a paying user
m, err := s.messageCache.Message(messageID) m, err := s.messageCache.Message(messageID)
if err == errMessageNotFound { if errors.Is(err, errMessageNotFound) {
if s.config.CacheBatchTimeout > 0 { if s.config.CacheBatchTimeout > 0 {
// Strange edge case: If we immediately after upload request the file (the web app does this for images), // Strange edge case: If we immediately after upload request the file (the web app does this for images),
// and messages are persisted asynchronously, retry fetching from the database // and messages are persisted asynchronously, retry fetching from the database
@@ -738,7 +746,7 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e
return nil, err return nil, err
} }
m := newDefaultMessage(t.ID, "") m := newDefaultMessage(t.ID, "")
cache, firebase, email, call, unifiedpush, e := s.parsePublishParams(r, m) cache, firebase, email, call, template, unifiedpush, e := s.parsePublishParams(r, m)
if e != nil { if e != nil {
return nil, e.With(t) return nil, e.With(t)
} }
@@ -769,7 +777,7 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e
if cache { if cache {
m.Expires = time.Unix(m.Time, 0).Add(v.Limits().MessageExpiryDuration).Unix() m.Expires = time.Unix(m.Time, 0).Add(v.Limits().MessageExpiryDuration).Unix()
} }
if err := s.handlePublishBody(r, v, m, body, unifiedpush); err != nil { if err := s.handlePublishBody(r, v, m, body, template, unifiedpush); err != nil {
return nil, err return nil, err
} }
if m.Message == "" { if m.Message == "" {
@@ -872,7 +880,7 @@ func (s *Server) sendToFirebase(v *visitor, m *message) {
logvm(v, m).Tag(tagFirebase).Debug("Publishing to Firebase") logvm(v, m).Tag(tagFirebase).Debug("Publishing to Firebase")
if err := s.firebaseClient.Send(v, m); err != nil { if err := s.firebaseClient.Send(v, m); err != nil {
minc(metricFirebasePublishedFailure) minc(metricFirebasePublishedFailure)
if err == errFirebaseTemporarilyBanned { if errors.Is(err, errFirebaseTemporarilyBanned) {
logvm(v, m).Tag(tagFirebase).Err(err).Debug("Unable to publish to Firebase: %v", err.Error()) logvm(v, m).Tag(tagFirebase).Err(err).Debug("Unable to publish to Firebase: %v", err.Error())
} else { } else {
logvm(v, m).Tag(tagFirebase).Err(err).Warn("Unable to publish to Firebase: %v", err.Error()) logvm(v, m).Tag(tagFirebase).Err(err).Warn("Unable to publish to Firebase: %v", err.Error())
@@ -924,7 +932,7 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) {
} }
} }
func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email, call string, unifiedpush bool, err *errHTTP) { func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email, call string, template bool, unifiedpush bool, err *errHTTP) {
cache = readBoolParam(r, true, "x-cache", "cache") cache = readBoolParam(r, true, "x-cache", "cache")
firebase = readBoolParam(r, true, "x-firebase", "firebase") firebase = readBoolParam(r, true, "x-firebase", "firebase")
m.Title = readParam(r, "x-title", "title", "t") m.Title = readParam(r, "x-title", "title", "t")
@@ -940,7 +948,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
} }
if attach != "" { if attach != "" {
if !urlRegex.MatchString(attach) { if !urlRegex.MatchString(attach) {
return false, false, "", "", false, errHTTPBadRequestAttachmentURLInvalid return false, false, "", "", false, false, errHTTPBadRequestAttachmentURLInvalid
} }
m.Attachment.URL = attach m.Attachment.URL = attach
if m.Attachment.Name == "" { if m.Attachment.Name == "" {
@@ -958,19 +966,19 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
} }
if icon != "" { if icon != "" {
if !urlRegex.MatchString(icon) { if !urlRegex.MatchString(icon) {
return false, false, "", "", false, errHTTPBadRequestIconURLInvalid return false, false, "", "", false, false, errHTTPBadRequestIconURLInvalid
} }
m.Icon = icon m.Icon = icon
} }
email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e") email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e")
if s.smtpSender == nil && email != "" { if s.smtpSender == nil && email != "" {
return false, false, "", "", false, errHTTPBadRequestEmailDisabled return false, false, "", "", false, false, errHTTPBadRequestEmailDisabled
} }
call = readParam(r, "x-call", "call") call = readParam(r, "x-call", "call")
if call != "" && (s.config.TwilioAccount == "" || s.userManager == nil) { if call != "" && (s.config.TwilioAccount == "" || s.userManager == nil) {
return false, false, "", "", false, errHTTPBadRequestPhoneCallsDisabled return false, false, "", "", false, false, errHTTPBadRequestPhoneCallsDisabled
} else if call != "" && !isBoolValue(call) && !phoneNumberRegex.MatchString(call) { } else if call != "" && !isBoolValue(call) && !phoneNumberRegex.MatchString(call) {
return false, false, "", "", false, errHTTPBadRequestPhoneNumberInvalid return false, false, "", "", false, false, errHTTPBadRequestPhoneNumberInvalid
} }
messageStr := strings.ReplaceAll(readParam(r, "x-message", "message", "m"), "\\n", "\n") messageStr := strings.ReplaceAll(readParam(r, "x-message", "message", "m"), "\\n", "\n")
if messageStr != "" { if messageStr != "" {
@@ -979,27 +987,27 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
var e error var e error
m.Priority, e = util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p")) m.Priority, e = util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p"))
if e != nil { if e != nil {
return false, false, "", "", false, errHTTPBadRequestPriorityInvalid return false, false, "", "", false, false, errHTTPBadRequestPriorityInvalid
} }
m.Tags = readCommaSeparatedParam(r, "x-tags", "tags", "tag", "ta") m.Tags = readCommaSeparatedParam(r, "x-tags", "tags", "tag", "ta")
delayStr := readParam(r, "x-delay", "delay", "x-at", "at", "x-in", "in") delayStr := readParam(r, "x-delay", "delay", "x-at", "at", "x-in", "in")
if delayStr != "" { if delayStr != "" {
if !cache { if !cache {
return false, false, "", "", false, errHTTPBadRequestDelayNoCache return false, false, "", "", false, false, errHTTPBadRequestDelayNoCache
} }
if email != "" { if email != "" {
return false, false, "", "", false, errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet) return false, false, "", "", false, false, errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet)
} }
if call != "" { if call != "" {
return false, false, "", "", false, errHTTPBadRequestDelayNoCall // we cannot store the phone number (yet) return false, false, "", "", false, false, errHTTPBadRequestDelayNoCall // we cannot store the phone number (yet)
} }
delay, err := util.ParseFutureTime(delayStr, time.Now()) delay, err := util.ParseFutureTime(delayStr, time.Now())
if err != nil { if err != nil {
return false, false, "", "", false, errHTTPBadRequestDelayCannotParse return false, false, "", "", false, false, errHTTPBadRequestDelayCannotParse
} else if delay.Unix() < time.Now().Add(s.config.MessageDelayMin).Unix() { } else if delay.Unix() < time.Now().Add(s.config.MessageDelayMin).Unix() {
return false, false, "", "", false, errHTTPBadRequestDelayTooSmall return false, false, "", "", false, false, errHTTPBadRequestDelayTooSmall
} else if delay.Unix() > time.Now().Add(s.config.MessageDelayMax).Unix() { } else if delay.Unix() > time.Now().Add(s.config.MessageDelayMax).Unix() {
return false, false, "", "", false, errHTTPBadRequestDelayTooLarge return false, false, "", "", false, false, errHTTPBadRequestDelayTooLarge
} }
m.Time = delay.Unix() m.Time = delay.Unix()
} }
@@ -1007,13 +1015,14 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
if actionsStr != "" { if actionsStr != "" {
m.Actions, e = parseActions(actionsStr) m.Actions, e = parseActions(actionsStr)
if e != nil { if e != nil {
return false, false, "", "", false, errHTTPBadRequestActionsInvalid.Wrap(e.Error()) return false, false, "", "", false, false, errHTTPBadRequestActionsInvalid.Wrap(e.Error())
} }
} }
contentType, markdown := readParam(r, "content-type", "content_type"), readBoolParam(r, false, "x-markdown", "markdown", "md") contentType, markdown := readParam(r, "content-type", "content_type"), readBoolParam(r, false, "x-markdown", "markdown", "md")
if markdown || strings.ToLower(contentType) == "text/markdown" { if markdown || strings.ToLower(contentType) == "text/markdown" {
m.ContentType = "text/markdown" m.ContentType = "text/markdown"
} }
template = readBoolParam(r, false, "x-template", "template", "tpl")
unifiedpush = readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see GET too! unifiedpush = readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see GET too!
if unifiedpush { if unifiedpush {
firebase = false firebase = false
@@ -1025,7 +1034,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
cache = false cache = false
email = "" email = ""
} }
return cache, firebase, email, call, unifiedpush, nil return cache, firebase, email, call, template, unifiedpush, nil
} }
// handlePublishBody consumes the PUT/POST body and decides whether the body is an attachment or the message. // handlePublishBody consumes the PUT/POST body and decides whether the body is an attachment or the message.
@@ -1033,16 +1042,18 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
// 1. curl -X POST -H "Poll: 1234" ntfy.sh/... // 1. curl -X POST -H "Poll: 1234" ntfy.sh/...
// If a message is flagged as poll request, the body does not matter and is discarded // If a message is flagged as poll request, the body does not matter and is discarded
// 2. curl -T somebinarydata.bin "ntfy.sh/mytopic?up=1" // 2. curl -T somebinarydata.bin "ntfy.sh/mytopic?up=1"
// If body is binary, encode as base64, if not do not encode // If UnifiedPush is enabled, encode as base64 if body is binary, and do not trim
// 3. curl -H "Attach: http://example.com/file.jpg" ntfy.sh/mytopic // 3. curl -H "Attach: http://example.com/file.jpg" ntfy.sh/mytopic
// Body must be a message, because we attached an external URL // Body must be a message, because we attached an external URL
// 4. curl -T short.txt -H "Filename: short.txt" ntfy.sh/mytopic // 4. curl -T short.txt -H "Filename: short.txt" ntfy.sh/mytopic
// Body must be attachment, because we passed a filename // Body must be attachment, because we passed a filename
// 5. curl -T file.txt ntfy.sh/mytopic // 5. curl -H "Template: yes" -T file.txt ntfy.sh/mytopic
// If file.txt is <= 4096 (message limit) and valid UTF-8, treat it as a message // If templating is enabled, read up to 32k and treat message body as JSON
// 6. curl -T file.txt ntfy.sh/mytopic // 6. curl -T file.txt ntfy.sh/mytopic
// If file.txt is > message limit, treat it as an attachment // If file.txt is <= 4096 (message limit) and valid UTF-8, treat it as a message
func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser, unifiedpush bool) error { // 7. curl -T file.txt ntfy.sh/mytopic
// In all other cases, mostly if file.txt is > message limit, treat it as an attachment
func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser, template, unifiedpush bool) error {
if m.Event == pollRequestEvent { // Case 1 if m.Event == pollRequestEvent { // Case 1
return s.handleBodyDiscard(body) return s.handleBodyDiscard(body)
} else if unifiedpush { } else if unifiedpush {
@@ -1051,10 +1062,12 @@ func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body
return s.handleBodyAsTextMessage(m, body) // Case 3 return s.handleBodyAsTextMessage(m, body) // Case 3
} else if m.Attachment != nil && m.Attachment.Name != "" { } else if m.Attachment != nil && m.Attachment.Name != "" {
return s.handleBodyAsAttachment(r, v, m, body) // Case 4 return s.handleBodyAsAttachment(r, v, m, body) // Case 4
} else if template {
return s.handleBodyAsTemplatedTextMessage(m, body) // Case 5
} else if !body.LimitReached && utf8.Valid(body.PeekedBytes) { } else if !body.LimitReached && utf8.Valid(body.PeekedBytes) {
return s.handleBodyAsTextMessage(m, body) // Case 5 return s.handleBodyAsTextMessage(m, body) // Case 6
} }
return s.handleBodyAsAttachment(r, v, m, body) // Case 6 return s.handleBodyAsAttachment(r, v, m, body) // Case 7
} }
func (s *Server) handleBodyDiscard(body *util.PeekedReadCloser) error { func (s *Server) handleBodyDiscard(body *util.PeekedReadCloser) error {
@@ -1086,6 +1099,45 @@ func (s *Server) handleBodyAsTextMessage(m *message, body *util.PeekedReadCloser
return nil return nil
} }
func (s *Server) handleBodyAsTemplatedTextMessage(m *message, body *util.PeekedReadCloser) error {
body, err := util.Peek(body, max(s.config.MessageSizeLimit, jsonBodyBytesLimit))
if err != nil {
return err
} else if body.LimitReached {
return errHTTPEntityTooLargeJSONBody
}
peekedBody := strings.TrimSpace(string(body.PeekedBytes))
if m.Message, err = replaceTemplate(m.Message, peekedBody); err != nil {
return err
}
if m.Title, err = replaceTemplate(m.Title, peekedBody); err != nil {
return err
}
if len(m.Message) > s.config.MessageSizeLimit {
return errHTTPBadRequestTemplateMessageTooLarge
}
return nil
}
func replaceTemplate(tpl string, source string) (string, error) {
if templateDisallowedRegex.MatchString(tpl) {
return "", errHTTPBadRequestTemplateDisallowedFunctionCalls
}
var data any
if err := json.Unmarshal([]byte(source), &data); err != nil {
return "", errHTTPBadRequestTemplateMessageNotJSON
}
t, err := template.New("").Parse(tpl)
if err != nil {
return "", errHTTPBadRequestTemplateInvalid
}
var buf bytes.Buffer
if err := t.Execute(util.NewTimeoutWriter(&buf, templateMaxExecutionTime), data); err != nil {
return "", errHTTPBadRequestTemplateExecuteFailed
}
return buf.String(), nil
}
func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser) error { func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser) error {
if s.fileCache == nil || s.config.BaseURL == "" || s.config.AttachmentCacheDir == "" { if s.fileCache == nil || s.config.BaseURL == "" || s.config.AttachmentCacheDir == "" {
return errHTTPBadRequestAttachmentsDisallowed.With(m) return errHTTPBadRequestAttachmentsDisallowed.With(m)
@@ -1128,7 +1180,7 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message,
util.NewFixedLimiter(vinfo.Stats.AttachmentTotalSizeRemaining), util.NewFixedLimiter(vinfo.Stats.AttachmentTotalSizeRemaining),
} }
m.Attachment.Size, err = s.fileCache.Write(m.ID, body, limiters...) m.Attachment.Size, err = s.fileCache.Write(m.ID, body, limiters...)
if err == util.ErrLimitReached { if errors.Is(err, util.ErrLimitReached) {
return errHTTPEntityTooLargeAttachment.With(m) return errHTTPEntityTooLargeAttachment.With(m)
} else if err != nil { } else if err != nil {
return err return err

View File

@@ -1,6 +1,7 @@
package server package server
import ( import (
"errors"
"heckel.io/ntfy/v2/user" "heckel.io/ntfy/v2/user"
"net/http" "net/http"
) )
@@ -45,7 +46,7 @@ func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visit
return errHTTPBadRequest.Wrap("username invalid, or password missing") return errHTTPBadRequest.Wrap("username invalid, or password missing")
} }
u, err := s.userManager.User(req.Username) u, err := s.userManager.User(req.Username)
if err != nil && err != user.ErrUserNotFound { if err != nil && !errors.Is(err, user.ErrUserNotFound) {
return err return err
} else if u != nil { } else if u != nil {
return errHTTPConflictUserExists return errHTTPConflictUserExists
@@ -53,7 +54,7 @@ func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visit
var tier *user.Tier var tier *user.Tier
if req.Tier != "" { if req.Tier != "" {
tier, err = s.userManager.Tier(req.Tier) tier, err = s.userManager.Tier(req.Tier)
if err == user.ErrTierNotFound { if errors.Is(err, user.ErrTierNotFound) {
return errHTTPBadRequestTierInvalid return errHTTPBadRequestTierInvalid
} else if err != nil { } else if err != nil {
return err return err
@@ -76,7 +77,7 @@ func (s *Server) handleUsersDelete(w http.ResponseWriter, r *http.Request, v *vi
return err return err
} }
u, err := s.userManager.User(req.Username) u, err := s.userManager.User(req.Username)
if err == user.ErrUserNotFound { if errors.Is(err, user.ErrUserNotFound) {
return errHTTPBadRequestUserNotFound return errHTTPBadRequestUserNotFound
} else if err != nil { } else if err != nil {
return err return err
@@ -98,7 +99,7 @@ func (s *Server) handleAccessAllow(w http.ResponseWriter, r *http.Request, v *vi
return err return err
} }
_, err = s.userManager.User(req.Username) _, err = s.userManager.User(req.Username)
if err == user.ErrUserNotFound { if errors.Is(err, user.ErrUserNotFound) {
return errHTTPBadRequestUserNotFound return errHTTPBadRequestUserNotFound
} else if err != nil { } else if err != nil {
return err return err

File diff suppressed because one or more lines are too long

View File

@@ -2,6 +2,7 @@ package server
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"heckel.io/ntfy/v2/util" "heckel.io/ntfy/v2/util"
"io" "io"
@@ -104,9 +105,9 @@ func extractIPAddress(r *http.Request, behindProxy bool) netip.Addr {
func readJSONWithLimit[T any](r io.ReadCloser, limit int, allowEmpty bool) (*T, error) { func readJSONWithLimit[T any](r io.ReadCloser, limit int, allowEmpty bool) (*T, error) {
obj, err := util.UnmarshalJSONWithLimit[T](r, limit, allowEmpty) obj, err := util.UnmarshalJSONWithLimit[T](r, limit, allowEmpty)
if err == util.ErrUnmarshalJSON { if errors.Is(err, util.ErrUnmarshalJSON) {
return nil, errHTTPBadRequestJSONInvalid return nil, errHTTPBadRequestJSONInvalid
} else if err == util.ErrTooLargeJSON { } else if errors.Is(err, util.ErrTooLargeJSON) {
return nil, errHTTPEntityTooLargeJSONBody return nil, errHTTPEntityTooLargeJSONBody
} else if err != nil { } else if err != nil {
return nil, err return nil, err

View File

@@ -16,7 +16,7 @@ func StartServer(t *testing.T) (*server.Server, int) {
// StartServerWithConfig starts a server.Server with a random port and waits for the server to be up // StartServerWithConfig starts a server.Server with a random port and waits for the server to be up
func StartServerWithConfig(t *testing.T, conf *server.Config) (*server.Server, int) { func StartServerWithConfig(t *testing.T, conf *server.Config) (*server.Server, int) {
port := 10000 + rand.Intn(20000) port := 10000 + rand.Intn(30000)
conf.ListenHTTP = fmt.Sprintf(":%d", port) conf.ListenHTTP = fmt.Sprintf(":%d", port)
conf.AttachmentCacheDir = t.TempDir() conf.AttachmentCacheDir = t.TempDir()
conf.CacheFile = filepath.Join(t.TempDir(), "cache.db") conf.CacheFile = filepath.Join(t.TempDir(), "cache.db")

View File

@@ -2,6 +2,7 @@ package util
import ( import (
"bytes" "bytes"
"errors"
"io" "io"
"strings" "strings"
) )
@@ -26,7 +27,7 @@ func Peek(underlying io.ReadCloser, limit int) (*PeekedReadCloser, error) {
} }
peeked := make([]byte, limit) peeked := make([]byte, limit)
read, err := io.ReadFull(underlying, peeked) read, err := io.ReadFull(underlying, peeked)
if err != nil && err != io.ErrUnexpectedEOF && err != io.EOF { if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) && err != io.EOF {
return nil, err return nil, err
} }
return &PeekedReadCloser{ return &PeekedReadCloser{
@@ -44,7 +45,7 @@ func (r *PeekedReadCloser) Read(p []byte) (n int, err error) {
return 0, io.EOF return 0, io.EOF
} }
n, err = r.peeked.Read(p) n, err = r.peeked.Read(p)
if err == io.EOF { if errors.Is(err, io.EOF) {
return r.underlying.Read(p) return r.underlying.Read(p)
} else if err != nil { } else if err != nil {
return 0, err return 0, err

34
util/timeout_writer.go Normal file
View File

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

2526
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -8,7 +8,7 @@
"message_bar_type_message": "Escriba un mensaje aquí", "message_bar_type_message": "Escriba un mensaje aquí",
"message_bar_error_publishing": "Error al publicar la notificación", "message_bar_error_publishing": "Error al publicar la notificación",
"alert_notification_permission_required_title": "Las notificaciones están deshabilitadas", "alert_notification_permission_required_title": "Las notificaciones están deshabilitadas",
"alert_notification_permission_required_description": "Concede a tu navegador permiso para mostrar notificaciones en el escritorio.", "alert_notification_permission_required_description": "Concede a tu navegador permiso para mostrar notificaciones de escritorio",
"nav_button_all_notifications": "Todas las notificaciones", "nav_button_all_notifications": "Todas las notificaciones",
"nav_button_settings": "Ajustes", "nav_button_settings": "Ajustes",
"nav_button_subscribe": "Suscribirse al tópico", "nav_button_subscribe": "Suscribirse al tópico",
@@ -16,7 +16,7 @@
"nav_button_publish_message": "Publicar notificación", "nav_button_publish_message": "Publicar notificación",
"notifications_copied_to_clipboard": "Copiado al portapapeles", "notifications_copied_to_clipboard": "Copiado al portapapeles",
"alert_not_supported_title": "Notificaciones no soportadas", "alert_not_supported_title": "Notificaciones no soportadas",
"alert_not_supported_description": "Las notificaciones no están soportadas por tu navegador.", "alert_not_supported_description": "Su navegador no admite notificaciones",
"notifications_tags": "Etiquetas", "notifications_tags": "Etiquetas",
"notifications_attachment_copy_url_title": "Copiar la URL del archivo adjunto en el portapapeles", "notifications_attachment_copy_url_title": "Copiar la URL del archivo adjunto en el portapapeles",
"notifications_attachment_copy_url_button": "Copiar URL", "notifications_attachment_copy_url_button": "Copiar URL",
@@ -381,5 +381,28 @@
"account_basics_phone_numbers_dialog_title": "Agregar número de teléfono", "account_basics_phone_numbers_dialog_title": "Agregar número de teléfono",
"account_basics_phone_numbers_dialog_code_placeholder": "p.ej. 123456", "account_basics_phone_numbers_dialog_code_placeholder": "p.ej. 123456",
"publish_dialog_call_item": "Llamar al número de teléfono {{number}}", "publish_dialog_call_item": "Llamar al número de teléfono {{number}}",
"publish_dialog_chip_call_no_verified_numbers_tooltip": "No hay números de teléfono verificados" "publish_dialog_chip_call_no_verified_numbers_tooltip": "No hay números de teléfono verificados",
"action_bar_mute_notifications": "Silenciar Notificaciones",
"action_bar_unmute_notifications": "Reactivar notificaciones",
"alert_notification_permission_denied_title": "Notificaciones bloqueadas",
"alert_notification_permission_denied_description": "Porfavor, reactivelas en su navegador",
"alert_notification_ios_install_required_title": "Requiere instalacion de iOS",
"alert_notification_ios_install_required_description": "Haz click en el icono de compartir y Añadir a pantalla de inicio para activar las notificaciones de iOS",
"notifications_actions_failed_notification": "Acción fallida",
"publish_dialog_checkbox_markdown": "Formatear como Markdown",
"subscribe_dialog_subscribe_use_another_background_info": "Las notificaciones de otros servidores no se recibirán cuando la aplicación web no esté abierta",
"prefs_notifications_web_push_title": "Notificaciones en segundo plano",
"prefs_notifications_web_push_enabled_description": "Las notificaciones se reciben incluso cuando la aplicación web no se está ejecutando (a través de Web Push)",
"prefs_notifications_web_push_disabled": "Desactivado",
"prefs_appearance_theme_title": "Tema",
"prefs_appearance_theme_system": "Sistema (por defecto)",
"error_boundary_button_reload_ntfy": "Volver a cargar ntfy",
"web_push_subscription_expiring_title": "Las notificaciones se pausarán",
"prefs_notifications_web_push_disabled_description": "Las notificaciones se reciben cuando la aplicación web se está ejecutando (a través de WebSocket)",
"prefs_notifications_web_push_enabled": "Activado para {{server}}",
"prefs_appearance_theme_light": "Claro",
"prefs_appearance_theme_dark": "Oscuro",
"web_push_subscription_expiring_body": "Abrir ntfy para seguir recibiendo notificaciones",
"web_push_unknown_notification_title": "Notificación desconocida recibida del servidor",
"web_push_unknown_notification_body": "Puede que necesites actualizar ntfy abriendo la aplicación web"
} }

View File

@@ -51,7 +51,7 @@
"nav_button_muted": "Notificacións acaladas", "nav_button_muted": "Notificacións acaladas",
"nav_button_connecting": "conectando", "nav_button_connecting": "conectando",
"nav_upgrade_banner_label": "Mellorar a ntfy Pro", "nav_upgrade_banner_label": "Mellorar a ntfy Pro",
"alert_not_supported_description": "O teu navegador non ten soporte para notificacións.", "alert_not_supported_description": "O teu navegador non ten soporte para notificacións",
"notifications_priority_x": "Prioridade {{priority}}", "notifications_priority_x": "Prioridade {{priority}}",
"notifications_attachment_link_expires": "a ligazón caduca o {{date}}", "notifications_attachment_link_expires": "a ligazón caduca o {{date}}",
"notifications_attachment_link_expired": "a ligazón de descarga caducou", "notifications_attachment_link_expired": "a ligazón de descarga caducou",
@@ -380,5 +380,31 @@
"account_basics_phone_numbers_dialog_verify_button_call": "Chámame", "account_basics_phone_numbers_dialog_verify_button_call": "Chámame",
"account_usage_emails_title": "Emails enviados", "account_usage_emails_title": "Emails enviados",
"account_basics_phone_numbers_dialog_channel_sms": "SMS", "account_basics_phone_numbers_dialog_channel_sms": "SMS",
"subscribe_dialog_login_description": "Este tema está protexido por contrasinal. Por favor, introduza o usuario e contrasinal para subscribirse." "subscribe_dialog_login_description": "Este tema está protexido por contrasinal. Por favor, introduza o usuario e contrasinal para subscribirse.",
"action_bar_mute_notifications": "Acalar notificacións",
"action_bar_unmute_notifications": "Reactivar notificacións",
"alert_notification_permission_required_title": "Notificacións desactivadas",
"alert_notification_permission_required_description": "Concederlle permisos ao navegador para mostrar notificacións de escritorio",
"alert_notification_permission_required_button": "Conceder",
"alert_notification_permission_denied_title": "Notificacións bloqueadas",
"alert_notification_permission_denied_description": "Por favor reactívaas no navegador",
"alert_notification_ios_install_required_title": "Require instalación iOS",
"alert_notification_ios_install_required_description": "Preme na icona Compartir e Engadir a Pantalla de Inicio para activar as notificacións en iOS",
"notifications_actions_failed_notification": "Non se puido realizar a acción",
"publish_dialog_checkbox_markdown": "Dar formato Markdow",
"prefs_notifications_web_push_title": "Notificacións en segundo plano",
"prefs_notifications_web_push_enabled_description": "Recíbense notificacións incluso se a app web non está en execución (vía Web Push)",
"prefs_notifications_web_push_disabled_description": "Recíbense as notificacións cando a app web está en execución (vía WebSocket)",
"prefs_notifications_web_push_enabled": "Activadas para {{server}}",
"prefs_notifications_web_push_disabled": "Desactivadas",
"prefs_appearance_theme_title": "Decorado",
"prefs_appearance_theme_system": "Sistema (por defecto)",
"prefs_appearance_theme_dark": "Modo escuro",
"prefs_appearance_theme_light": "Modo claro",
"error_boundary_button_reload_ntfy": "Recargar ntfy",
"web_push_subscription_expiring_title": "Vanse pausar as notificacións",
"web_push_subscription_expiring_body": "Abrir ntfy para seguir recibindo notificacións",
"web_push_unknown_notification_title": "Recibida unha notificación descoñecida desde o servidor",
"web_push_unknown_notification_body": "Poderías ter que actualizar ntfy abrindo a app web",
"subscribe_dialog_subscribe_use_another_background_info": "As notificacións procedentes doutros servidores non se van recibir cando a app web estea pechada"
} }

View File

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

View File

@@ -155,7 +155,7 @@
"alert_grant_title": "Oznámenia sú vypnuté", "alert_grant_title": "Oznámenia sú vypnuté",
"alert_grant_button": "Prideliť teraz", "alert_grant_button": "Prideliť teraz",
"alert_not_supported_title": "Oznámenia nie sú podporované", "alert_not_supported_title": "Oznámenia nie sú podporované",
"alert_not_supported_description": "Oznámenia nie sú vo vašom prehliadači podporované.", "alert_not_supported_description": "Oznámenia nie sú vo vašom prehliadači podporované",
"notifications_attachment_copy_url_title": "Kopírovať URL adresu prílohy do schránky", "notifications_attachment_copy_url_title": "Kopírovať URL adresu prílohy do schránky",
"notifications_attachment_copy_url_button": "Kopírovať adresu URL", "notifications_attachment_copy_url_button": "Kopírovať adresu URL",
"notifications_attachment_open_title": "Prejsť na {{url}}", "notifications_attachment_open_title": "Prejsť na {{url}}",
@@ -380,5 +380,31 @@
"account_upgrade_dialog_reservations_warning_other": "Vybraná úroveň umožňuje menej rezervovaných tém ako vaša aktuálna úroveň. Pred zmenou úrovne <strong>vymažte aspoň {{count}} rezervácií</strong>. Rezervácie môžete odstrániť v <Link>Nastaveniach</Link>.", "account_upgrade_dialog_reservations_warning_other": "Vybraná úroveň umožňuje menej rezervovaných tém ako vaša aktuálna úroveň. Pred zmenou úrovne <strong>vymažte aspoň {{count}} rezervácií</strong>. Rezervácie môžete odstrániť v <Link>Nastaveniach</Link>.",
"prefs_users_dialog_title_add": "Pridať používateľa", "prefs_users_dialog_title_add": "Pridať používateľa",
"account_tokens_dialog_button_create": "Vytvoriť token", "account_tokens_dialog_button_create": "Vytvoriť token",
"account_tokens_table_create_token_button": "Vytvoriť prístupový token" "account_tokens_table_create_token_button": "Vytvoriť prístupový token",
"action_bar_mute_notifications": "Stlmiť oznámenia",
"action_bar_unmute_notifications": "Zrušiť stlmenie oznámení",
"alert_notification_permission_required_description": "Udeliť povolenie prehliadaču na zobrazovanie oznámení na ploche",
"alert_notification_permission_required_button": "Udeliť teraz",
"alert_notification_permission_denied_title": "Oznámenia sú zablokované",
"alert_notification_permission_denied_description": "Opätovne ich povoľte vo svojom prehliadači",
"alert_notification_ios_install_required_title": "Vyžaduje sa inštalácia iOS",
"notifications_actions_failed_notification": "Neúspešná akcia",
"publish_dialog_checkbox_markdown": "Formátovať ako Markdown",
"subscribe_dialog_subscribe_use_another_background_info": "Oznámenia z iných serverov sa nebudú prijímať, keď webová aplikácia nie je otvorená",
"prefs_notifications_web_push_title": "Oznámenia na pozadí",
"prefs_notifications_web_push_enabled_description": "Oznámenia sa prijímajú, aj keď webová aplikácia nie je spustená (prostredníctvom Web Push)",
"prefs_notifications_web_push_disabled_description": "Oznámenia sa prijímajú, keď je webová aplikácia spustená (cez WebSocket)",
"prefs_notifications_web_push_enabled": "Povolené pre {{server}}",
"prefs_notifications_web_push_disabled": "Zakázané",
"prefs_appearance_theme_title": "Téma",
"prefs_appearance_theme_system": "Systémové (predvolené)",
"prefs_appearance_theme_dark": "Tmavý režim",
"prefs_appearance_theme_light": "Svetlý režim",
"error_boundary_button_reload_ntfy": "Obnoviť ntfy",
"web_push_subscription_expiring_title": "Oznámenia budú pozastavené",
"web_push_subscription_expiring_body": "Ak chcete pokračovať v prijímaní upozornení, otvorte ntfy",
"web_push_unknown_notification_title": "Neznáme oznámenie prijaté zo servera",
"web_push_unknown_notification_body": "Možno budete musieť aktualizovať ntfy otvorením webovej aplikácie",
"alert_notification_permission_required_title": "Oznámenia sú vypnuté",
"alert_notification_ios_install_required_description": "Kliknutím na Zdieľať a Pridať na domovskú obrazovku povolíte oznámenia v systéme iOS"
} }

View File

@@ -60,8 +60,8 @@
"nav_button_documentation": "Belgelendirme", "nav_button_documentation": "Belgelendirme",
"nav_button_publish_message": "Bildirim yayınla", "nav_button_publish_message": "Bildirim yayınla",
"alert_notification_permission_required_title": "Bildirimler devre dışı", "alert_notification_permission_required_title": "Bildirimler devre dışı",
"alert_notification_permission_required_description": "Tarayıcınıza masaüstü bildirimlerini görüntüleme izni verin.", "alert_notification_permission_required_description": "Tarayıcınıza masaüstü bildirimlerini görüntüleme izni verin",
"alert_not_supported_description": "Tarayıcınızda bildirimler desteklenmiyor.", "alert_not_supported_description": "Tarayıcınızda bildirimler desteklenmiyor",
"notifications_copied_to_clipboard": "Panoya kopyalandı", "notifications_copied_to_clipboard": "Panoya kopyalandı",
"notifications_tags": "Etiketler", "notifications_tags": "Etiketler",
"notifications_attachment_copy_url_title": "Ek URL'sini panoya kopyala", "notifications_attachment_copy_url_title": "Ek URL'sini panoya kopyala",
@@ -380,5 +380,28 @@
"account_basics_phone_numbers_dialog_code_label": "Doğrulama kodu", "account_basics_phone_numbers_dialog_code_label": "Doğrulama kodu",
"account_basics_phone_numbers_dialog_code_placeholder": "örn. 123456", "account_basics_phone_numbers_dialog_code_placeholder": "örn. 123456",
"account_usage_calls_title": "Yapılan telefon aramaları", "account_usage_calls_title": "Yapılan telefon aramaları",
"account_upgrade_dialog_tier_features_no_calls": "Telefon araması yok" "account_upgrade_dialog_tier_features_no_calls": "Telefon araması yok",
"action_bar_mute_notifications": "Bildirimleri sessize al",
"action_bar_unmute_notifications": "Bildirimlerin sesini aç",
"alert_notification_permission_denied_title": "Bildirimler engellendi",
"alert_notification_permission_denied_description": "Lütfen tarayıcınızda yeniden etkinleştirin",
"alert_notification_ios_install_required_title": "iOS kurulumu gerekli",
"alert_notification_ios_install_required_description": "iOS'ta bildirimleri etkinleştirmek için Paylaş simgesine ve Ana Ekrana Ekle'ye tıklayın",
"notifications_actions_failed_notification": "Başarısız eylem",
"publish_dialog_checkbox_markdown": "Markdown olarak biçimlendir",
"prefs_notifications_web_push_title": "Arka plan bildirimleri",
"prefs_notifications_web_push_enabled_description": "Web uygulaması çalışmadığında bile bildirimler alınır (Web Push aracılığıyla)",
"prefs_notifications_web_push_disabled_description": "Web uygulaması çalışırken bildirim alınır (WebSocket aracılığıyla)",
"prefs_notifications_web_push_enabled": "{{server}} için etkinleştirildi",
"prefs_notifications_web_push_disabled": "Devre dışı",
"prefs_appearance_theme_title": "Tema",
"prefs_appearance_theme_system": "Sistem (öntanımlı)",
"prefs_appearance_theme_dark": "Koyu mod",
"prefs_appearance_theme_light": "Açık mod",
"error_boundary_button_reload_ntfy": "ntfy'yi yeniden yükle",
"web_push_subscription_expiring_title": "Bildirimler duraklatılacak",
"web_push_subscription_expiring_body": "Bildirimleri almaya devam etmek için ntfy'yi açın",
"web_push_unknown_notification_title": "Sunucudan bilinmeyen bildirim alındı",
"web_push_unknown_notification_body": "Web uygulamasını açarak ntfy'yi güncellemeniz gerekebilir",
"subscribe_dialog_subscribe_use_another_background_info": "Web uygulamasıık değilken diğer sunuculardan gelen bildirimler alınmayacaktır"
} }