Compare commits

...

45 Commits

Author SHA1 Message Date
binwiederhier
35eac5b9ad Simplify 2023-04-21 21:07:07 -04:00
binwiederhier
6b1f72fec9 Docs 2023-04-21 20:52:17 -04:00
binwiederhier
824ec39d46 Attempt to fix pipeline 2023-04-21 19:36:25 -04:00
binwiederhier
cfa8d92af1 UTF-8 headers 2023-04-21 18:45:27 -04:00
binwiederhier
91d2603fe0 Add tests, and proper rate 2023-04-21 11:09:13 -04:00
binwiederhier
6be95f8285 WIP: persist message stats 2023-04-20 22:04:11 -04:00
binwiederhier
4783cb1211 Thank you @FingerlessGlov3s for your donation 2023-04-19 22:32:33 -04:00
binwiederhier
113ff55426 Merge branch 'main' of github.com:binwiederhier/ntfy 2023-04-19 22:17:24 -04:00
binwiederhier
f2f4bbdbd5 Deps 2023-04-19 22:17:10 -04:00
binwiederhier
d931ce8acc Integrations 2023-04-19 22:12:40 -04:00
Philipp C. Heckel
b1c0d57fb9 Merge pull request #701 from muety/website-watcher-integration
Add website-watcher integration
2023-04-15 10:14:06 -04:00
Ferdinand Mütsch
b3d11f09ba Add website-watcher integration 2023-04-15 15:11:34 +02:00
binwiederhier
1ccf659781 Merge branch 'main' of github.com:binwiederhier/ntfy 2023-04-11 11:49:05 -04:00
binwiederhier
3ad639daed Install instructions for Homebrew 2023-04-11 11:48:51 -04:00
binwiederhier
dc5dbdf6e5 Added Swedish 2023-04-11 11:42:06 -04:00
binwiederhier
e3998d5fce Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web 2023-04-11 11:28:31 -04:00
Philipp C. Heckel
8ad1089053 Merge pull request #699 from wunter8/default-auth-for-cli-sub
fixes #698
2023-04-09 15:11:40 -04:00
Rhodri
1a6b076e87 Translated using Weblate (Welsh)
Currently translated at 11.4% (41 of 357 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/cy/
2023-04-09 13:48:14 +02:00
109247019824
9db9678952 Translated using Weblate (Bulgarian)
Currently translated at 80.9% (289 of 357 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/bg/
2023-04-09 13:48:14 +02:00
Hunter Kehoe
037d1d647d fixes #698 2023-04-08 21:20:21 -06:00
Rhodri
cb9be5b732 Added translation using Weblate (Welsh) 2023-04-08 13:00:53 +02:00
Linerly
99b9792875 Translated using Weblate (Indonesian)
Currently translated at 100.0% (357 of 357 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/id/
2023-04-08 04:01:49 +02:00
binwiederhier
9471429cb3 Derp 2023-04-06 21:55:41 -04:00
binwiederhier
ea538338cf Make emojis in docs larger 2023-04-06 21:51:25 -04:00
Shjosan
5825f20e98 Translated using Weblate (Swedish)
Currently translated at 100.0% (357 of 357 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/sv/
2023-04-07 02:44:22 +02:00
binwiederhier
35ad4a0c03 Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web into patch-1 2023-04-06 09:57:56 -04:00
binwiederhier
b5b4997957 Fixed PS examples 2023-04-06 09:57:45 -04:00
Hugo Hedlund
69dcc380a3 Translated using Weblate (Swedish)
Currently translated at 23.8% (85 of 357 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/sv/
2023-04-06 11:37:25 +02:00
Shjosan
8e04eeaacd Translated using Weblate (Swedish)
Currently translated at 23.8% (85 of 357 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/sv/
2023-04-06 11:37:24 +02:00
Nathan
c63ca95867 Converted PowerShell code to use Splatting, and newer PS7 parameters (where available) 2023-04-05 20:13:23 +01:00
Shoshin Akamine
d6c0ae130f Translated using Weblate (Japanese)
Currently translated at 100.0% (357 of 357 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ja/
2023-04-05 12:47:44 +02:00
binwiederhier
e1339ccde7 Add release notes 2023-04-04 23:14:34 -04:00
Philipp C. Heckel
7c1d892779 Merge pull request #696 from pokej6/windows_hide_country_flags
Hiding language preference flags while on Windows platforms.
2023-04-04 23:04:44 -04:00
binwiederhier
5f2e238a30 Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web 2023-04-04 23:01:24 -04:00
Jeremy S
f69065ca79 Hiding language preference flags while on Windows platforms.
Windows has an issue displaying country flag emoji. This is a platform issue which does not even appear to be fixed in Win11. As a result this fix will just hide the emoji when a windows operating system is detected.
resolves #606
2023-04-04 21:55:05 -04:00
waclaw66
1c731a3cef Translated using Weblate (Czech)
Currently translated at 100.0% (357 of 357 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/cs/
2023-04-01 13:39:49 +02:00
gallegonovato
6cd72683ad Translated using Weblate (Spanish)
Currently translated at 100.0% (357 of 357 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/es/
2023-04-01 13:39:49 +02:00
Oğuz Ersen
e86bdf46db Translated using Weblate (Turkish)
Currently translated at 100.0% (357 of 357 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/tr/
2023-04-01 13:39:49 +02:00
Christian Meis
0adbd87387 Translated using Weblate (German)
Currently translated at 100.0% (357 of 357 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2023-04-01 13:39:48 +02:00
josé m
286ae43d1a Added translation using Weblate (Galician) 2023-03-31 11:51:46 +02:00
binwiederhier
a75fb08ef1 Tidy 2023-03-30 21:06:22 -04:00
binwiederhier
58a0c2a6c6 Bump 2023-03-30 21:04:03 -04:00
binwiederhier
d050956007 Added Ansible role 2023-03-30 14:56:14 -04:00
binwiederhier
bdae48afba Disable iOS polling entirely 2023-03-30 14:48:52 -04:00
binwiederhier
cb5c4c5483 Thank you @R-Gld for your sponsorship 2023-03-30 12:56:47 -04:00
33 changed files with 3469 additions and 2572 deletions

View File

@@ -26,7 +26,7 @@ jobs:
~/go/bin
~/.npm
web/node_modules
key: ${{ runner.os }}-ntfy-${{ hashFiles('**/go.sum', '**/package.lock') }}
key: ${{ runner.os }}-ntfy-${{ hashFiles('go.sum', 'web/package.lock') }}
restore-keys: ${{ runner.os }}-ntfy-
-
name: Install dependencies

View File

@@ -29,7 +29,7 @@ jobs:
~/go/bin
~/.npm
web/node_modules
key: ${{ runner.os }}-ntfy-${{ hashFiles('**/go.sum', '**/package.lock') }}
key: ${{ runner.os }}-ntfy-${{ hashFiles('go.sum', 'web/package.lock') }}
restore-keys: ${{ runner.os }}-ntfy-
-
name: Docker login

View File

@@ -26,7 +26,7 @@ jobs:
~/go/bin
~/.npm
web/node_modules
key: ${{ runner.os }}-ntfy-${{ hashFiles('**/go.sum', '**/package.lock') }}
key: ${{ runner.os }}-ntfy-${{ hashFiles('go.sum', 'web/package.lock') }}
restore-keys: ${{ runner.os }}-ntfy-
-
name: Install dependencies

View File

@@ -129,6 +129,8 @@ account costs. Even small donations are very much appreciated. A big fat **Thank
<a href="https://github.com/nichu42"><img src="https://github.com/nichu42.png" width="40px" /></a>
<a href="https://github.com/samliebow"><img src="https://github.com/samliebow.png" width="40px" /></a>
<a href="https://github.com/johman10"><img src="https://github.com/johman10.png" width="40px" /></a>
<a href="https://github.com/R-Gld"><img src="https://github.com/R-Gld.png" width="40px" /></a>
<a href="https://github.com/FingerlessGlov3s"><img src="https://github.com/FingerlessGlov3s.png" width="40px" /></a>
I'd also like to thank JetBrains for providing their awesome [IntelliJ IDEA](https://www.jetbrains.com/idea/) to me for free,
and [DigitalOcean](https://m.do.co/c/442b929528db) (*referral link*) for supporting the project:

View File

@@ -119,8 +119,7 @@ func execSubscribe(c *cli.Context) error {
}
if token != "" {
options = append(options, client.WithBearerAuth(token))
}
if user != "" {
} else if user != "" {
var pass string
parts := strings.SplitN(user, ":", 2)
if len(parts) == 2 {
@@ -136,6 +135,10 @@ func execSubscribe(c *cli.Context) error {
fmt.Fprintf(c.App.ErrWriter, "\r%s\r", strings.Repeat(" ", 20))
}
options = append(options, client.WithBasicAuth(user, pass))
} else if conf.DefaultToken != "" {
options = append(options, client.WithBearerAuth(conf.DefaultToken))
} else if conf.DefaultUser != "" && conf.DefaultPassword != nil {
options = append(options, client.WithBasicAuth(conf.DefaultUser, *conf.DefaultPassword))
}
if scheduled {
options = append(options, client.WithScheduled())

View File

@@ -310,3 +310,52 @@ func TestCLI_Subscribe_Token_And_UserPass(t *testing.T) {
require.Error(t, err)
require.Equal(t, "cannot set both --user and --token", err.Error())
}
func TestCLI_Subscribe_Default_Token(t *testing.T) {
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/mytopic/json", r.URL.Path)
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
w.WriteHeader(http.StatusOK)
w.Write([]byte(message))
}))
defer server.Close()
filename := filepath.Join(t.TempDir(), "client.yml")
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
default-host: %s
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--config=" + filename, "mytopic"}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}
func TestCLI_Subscribe_Default_UserPass(t *testing.T) {
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/mytopic/json", r.URL.Path)
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
w.WriteHeader(http.StatusOK)
w.Write([]byte(message))
}))
defer server.Close()
filename := filepath.Join(t.TempDir(), "client.yml")
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
default-host: %s
default-user: philipp
default-password: mypass
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--config=" + filename, "mytopic"}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}

File diff suppressed because it is too large Load Diff

View File

@@ -26,37 +26,37 @@ deb/rpm packages.
=== "x86_64/amd64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.3.0/ntfy_2.3.0_linux_x86_64.tar.gz
tar zxvf ntfy_2.3.0_linux_x86_64.tar.gz
sudo cp -a ntfy_2.3.0_linux_x86_64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.3.0_linux_x86_64/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v2.3.1/ntfy_2.3.1_linux_x86_64.tar.gz
tar zxvf ntfy_2.3.1_linux_x86_64.tar.gz
sudo cp -a ntfy_2.3.1_linux_x86_64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.3.1_linux_x86_64/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "armv6"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.3.0/ntfy_2.3.0_linux_armv6.tar.gz
tar zxvf ntfy_2.3.0_linux_armv6.tar.gz
sudo cp -a ntfy_2.3.0_linux_armv6/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.3.0_linux_armv6/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v2.3.1/ntfy_2.3.1_linux_armv6.tar.gz
tar zxvf ntfy_2.3.1_linux_armv6.tar.gz
sudo cp -a ntfy_2.3.1_linux_armv6/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.3.1_linux_armv6/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "armv7/armhf"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.3.0/ntfy_2.3.0_linux_armv7.tar.gz
tar zxvf ntfy_2.3.0_linux_armv7.tar.gz
sudo cp -a ntfy_2.3.0_linux_armv7/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.3.0_linux_armv7/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v2.3.1/ntfy_2.3.1_linux_armv7.tar.gz
tar zxvf ntfy_2.3.1_linux_armv7.tar.gz
sudo cp -a ntfy_2.3.1_linux_armv7/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.3.1_linux_armv7/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "arm64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.3.0/ntfy_2.3.0_linux_arm64.tar.gz
tar zxvf ntfy_2.3.0_linux_arm64.tar.gz
sudo cp -a ntfy_2.3.0_linux_arm64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.3.0_linux_arm64/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v2.3.1/ntfy_2.3.1_linux_arm64.tar.gz
tar zxvf ntfy_2.3.1_linux_arm64.tar.gz
sudo cp -a ntfy_2.3.1_linux_arm64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.3.1_linux_arm64/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
@@ -106,7 +106,7 @@ Manually installing the .deb file:
=== "x86_64/amd64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.3.0/ntfy_2.3.0_linux_amd64.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v2.3.1/ntfy_2.3.1_linux_amd64.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -114,7 +114,7 @@ Manually installing the .deb file:
=== "armv6"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.3.0/ntfy_2.3.0_linux_armv6.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v2.3.1/ntfy_2.3.1_linux_armv6.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -122,7 +122,7 @@ Manually installing the .deb file:
=== "armv7/armhf"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.3.0/ntfy_2.3.0_linux_armv7.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v2.3.1/ntfy_2.3.1_linux_armv7.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -130,7 +130,7 @@ Manually installing the .deb file:
=== "arm64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.3.0/ntfy_2.3.0_linux_arm64.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v2.3.1/ntfy_2.3.1_linux_arm64.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -140,28 +140,28 @@ Manually installing the .deb file:
=== "x86_64/amd64"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.3.0/ntfy_2.3.0_linux_amd64.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.3.1/ntfy_2.3.1_linux_amd64.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "armv6"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.3.0/ntfy_2.3.0_linux_armv6.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.3.1/ntfy_2.3.1_linux_armv6.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "armv7/armhf"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.3.0/ntfy_2.3.0_linux_armv7.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.3.1/ntfy_2.3.1_linux_armv7.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "arm64"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.3.0/ntfy_2.3.0_linux_arm64.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.3.1/ntfy_2.3.1_linux_arm64.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
@@ -189,30 +189,36 @@ NixOS also supports [declarative setup of the ntfy server](https://search.nixos.
## macOS
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on macOS as well.
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.3.0/ntfy_2.3.0_macOS_all.tar.gz),
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.3.1/ntfy_2.3.1_macOS_all.tar.gz),
extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`).
If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For all other users, it'll look for it at
`~/Library/Application Support/ntfy/client.yml` (sample included in the tarball).
```bash
curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.3.0/ntfy_2.3.0_macOS_all.tar.gz > ntfy_2.3.0_macOS_all.tar.gz
tar zxvf ntfy_2.3.0_macOS_all.tar.gz
sudo cp -a ntfy_2.3.0_macOS_all/ntfy /usr/local/bin/ntfy
curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.3.1/ntfy_2.3.1_macOS_all.tar.gz > ntfy_2.3.1_macOS_all.tar.gz
tar zxvf ntfy_2.3.1_macOS_all.tar.gz
sudo cp -a ntfy_2.3.1_macOS_all/ntfy /usr/local/bin/ntfy
mkdir ~/Library/Application\ Support/ntfy
cp ntfy_2.3.0_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
cp ntfy_2.3.1_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
ntfy --help
```
!!! info
There is a [GitHub issue](https://github.com/binwiederhier/ntfy/issues/286) about making ntfy installable via
[Homebrew](https://brew.sh/). I'll eventually get to that, but I'd also love if somebody else stepped up to do it.
Also, you can build and run the ntfy server on macOS as well, though I don't officially support that.
Check out the [build instructions](develop.md) for details.
Only the ntfy CLI is supported on macOS. ntfy server is currently not supported, but you can build and run it for
development as well. Check out the [build instructions](develop.md) for details.
## Homebrew
To install the [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) via Homebrew (Linux and macOS),
simply run:
```
brew install ntfy
```
## Windows
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well.
To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.3.0/ntfy_2.3.0_windows_x86_64.zip),
To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.3.1/ntfy_2.3.1_windows_x86_64.zip),
extract it and place the `ntfy.exe` binary somewhere in your `%Path%`.
The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file).

View File

@@ -35,6 +35,8 @@ and uptime of third party servers, so use of each server is **at your own discre
- [Automatisch](https://automatisch.io/) ⭐ - Open source Zapier alternative / workflow automation tool
- [FlexGet](https://flexget.com/Plugins/Notifiers/ntfysh) ⭐ - Multipurpose automation tool for all of your media
- [Shoutrrr](https://containrrr.dev/shoutrrr/v0.7/services/ntfy/) ⭐ - Notification library for gophers and their furry friends.
- [Netdata](https://learn.netdata.cloud/docs/alerts-and-notifications/notifications/agent-alert-notifications/ntfy) ⭐ - Real-time performance monitoring
- [Deployer](https://github.com/deployphp/deployer) ⭐ - PHP deployment tool
- [Scrt.link](https://scrt.link/) - Share a secret
- [Platypush](https://docs.platypush.tech/platypush/plugins/ntfy.html) - Automation platform aimed to run on any device that can run Python
- [diun](https://crazymax.dev/diun/) - Docker Image Update Notifier
@@ -62,6 +64,7 @@ and uptime of third party servers, so use of each server is **at your own discre
- [ntfy](https://github.com/jonocarroll/ntfy) - Wraps the ntfy API with pipe-friendly tooling (R)
- [ntfy-for-delphi](https://github.com/hazzelnuts/ntfy-for-delphi) - A friendly library to push instant notifications ntfy (Delphi)
- [ntfy](https://github.com/ffflorian/ntfy) - Send notifications over ntfy (JS)
- [ntfy_dart](https://github.com/jr1221/ntfy_dart) - Dart wrapper around the ntfy API (Dart)
## CLIs + GUIs
@@ -87,6 +90,7 @@ and uptime of third party servers, so use of each server is **at your own discre
- [ntfy-server-status](https://github.com/filip2cz/ntfy-server-status) - Checking if server is online and reporting through ntfy (C)
- [borg-based backup](https://github.com/davidhi7/backup) - Simple borg-based backup script with notifications based on ntfy.sh or Discord webhooks (Python/Shell)
- [ntfy.sh *arr script](https://github.com/agent-squirrel/nfty-arr-script) - Quick and hacky script to get sonarr/radarr to notify the ntfy.sh service (Shell)
- [website-watcher](https://github.com/muety/website-watcher) - A small tool to watch websites for changes (with XPath support) (Python)
- [siteeagle](https://github.com/tpanum/siteeagle) - A small Python script to monitor websites and notify changes (Python)
- [send_to_phone](https://github.com/whipped-cream/send_to_phone) - Scripts to upload a file to Transfer.sh and ping ntfy with the download link (Python)
- [ntfy Discord bot](https://github.com/R0dn3yS/ntfy-bot) - WIP ntfy discord bot (TypeScript)
@@ -118,6 +122,12 @@ and uptime of third party servers, so use of each server is **at your own discre
- [n8n-ntfy](https://github.com/raghavanand98/n8n-ntfy.sh) - n8n community node that lets you use ntfy in your workflows
- [nlog-ntfy](https://github.com/MichelMichels/nlog-ntfy) - Send NLog messages over ntfy (C# / .NET / NLog)
- [helm-charts](https://github.com/sarab97/helm-charts) - Helm charts of some of the selfhosted services, incl. ntfy
- [ntfy_ansible_role](https://github.com/stevenengland/ntfy_ansible_role) (on [Ansible Galaxy](https://galaxy.ansible.com/stevenengland/ntfy)) - Ansible role to install ntfy
- [easy2ntfy](https://github.com/chromoxdor/easy2ntfy) - Gateway for ESPeasy to receive commands through ntfy and using easyfetch (HTML/JS)
- [ntfy_lite](https://github.com/MPI-IS/ntfy_lite) - Minimalist python API for pushing ntfy notifications (Python)
- [notify](https://github.com/guanguans/notify) - 推送通知 (PHP)
- [zpool-events](https://github.com/maglar0/zpool-events) - Notify on ZFS pool events (Python)
- [ntfyd](https://github.com/joachimschmidt557/ntfyd) - ntfy desktop daemon (Zig)
## Blog + forum posts
@@ -142,6 +152,7 @@ and uptime of third party servers, so use of each server is **at your own discre
- [Using ntfy to warn me when my computer is discharging](https://ulysseszh.github.io/programming/2022/11/28/ntfy-warn-discharge.html) - ulysseszh.github.io - 11/2022
- [ntfy - Push Notification Service](https://dizzytech.de/posts/ntfy/) - dizzytech.de - 11/2022
- [Console #132](https://console.substack.com/p/console-132) ⭐ - console.substack.com - 11/2022
- [How to make my phone buzz*](https://evbogue.com/howtomakemyphonebuzz) - evbogue.com - 11/2022
- [MeshCentral - Ntfy Push Notifications ](https://www.youtube.com/watch?v=wyE4rtUd4Bg) - youtube.com - 11/2022
- [Changelog | Tracking layoffs, tech worker demand still high, ntfy, ...](https://changelog.com/news/tracking-layoffs-tech-worker-demand-still-high-ntfy-devenv-markdoc-mike-bifulco-Y1jW) ⭐ - changelog.com - 11/2022
- [Pointer | Issue #367](https://www.pointer.io/archives/a9495a2a6f/) - pointer.io - 11/2022

View File

@@ -38,7 +38,12 @@ Here's an example showing how to publish a simple message using a POST request:
=== "PowerShell"
``` powershell
Invoke-RestMethod -Method 'Post' -Uri https://ntfy.sh/mytopic -Body "Backup successful" -UseBasicParsing
$Request = @{
Method = "POST"
URI = "https://ntfy.sh/mytopic"
Body = "Backup successful"
}
Invoke-RestMethod @Request
```
=== "Python"
@@ -124,12 +129,17 @@ a [title](#message-title), and [tag messages](#tags-emojis) 🥳 🎉. Here's an
=== "PowerShell"
``` powershell
$uri = "https://ntfy.sh/phil_alerts"
$headers = @{ Title="Unauthorized access detected"
Priority="urgent"
Tags="warning,skull" }
$body = "Remote access to phils-laptop detected. Act right away."
Invoke-RestMethod -Method 'Post' -Uri $uri -Headers $headers -Body $body -UseBasicParsing
$Request = @{
Method = "POST"
URI = "https://ntfy.sh/phil_alerts"
Headers = @{
Title = "Unauthorized access detected"
Priority = "urgent"
Tags = "warning,skull"
}
Body = "Remote access to phils-laptop detected. Act right away."
}
Invoke-RestMethod @Request
```
=== "Python"
@@ -242,18 +252,21 @@ an [external image attachment](#attach-file-from-a-url) and [email publishing](#
=== "PowerShell"
``` powershell
$uri = "https://ntfy.sh/mydoorbell"
$headers = @{ Click="https://home.nest.com/"
Attach="https://nest.com/view/yAxkasd.jpg"
Actions="http, Open door, https://api.nest.com/open/yAxkasd, clear=true"
Email="phil@example.com" }
$body = @'
There's someone at the door. 🐶
Please check if it's a good boy or a hooman.
Doggies have been known to ring the doorbell.
'@
Invoke-RestMethod -Method 'Post' -Uri $uri -Headers $headers -Body $body -UseBasicParsing
$Request = @{
Method = "POST"
URI = "https://ntfy.sh/mydoorbell"
Headers = @{
Click = "https://home.nest.com"
Attach = "https://nest.com/view/yAxksd.jpg"
Actions = "http, Open door, https://api.nest.com/open/yAxkasd, clear=true"
Email = "phil@example.com"
}
Body = "There's someone at the door. 🐶`n
`n
Please check if it's a good boy or a hooman.`n
Doggies have been known to ring the doorbell.`n"
}
Invoke-RestMethod @Request
```
=== "Python"
@@ -342,10 +355,15 @@ you can set the `X-Title` header (or any of its aliases: `Title`, `ti`, or `t`).
=== "PowerShell"
``` powershell
$uri = "https://ntfy.sh/controversial"
$headers = @{ Title="Dogs are better than cats" }
$body = "Oh my ..."
Invoke-RestMethod -Method 'Post' -Uri $uri -Headers $headers -Body $body -UseBasicParsing
$Request = @{
Method = "POST"
URI = "https://ntfy.sh/controversial"
Headers = @{
Title = "Dogs are better than cats"
}
Body = "Oh my ..."
}
Invoke-RestMethod @Request
```
=== "Python"
@@ -373,6 +391,12 @@ you can set the `X-Title` header (or any of its aliases: `Title`, `ti`, or `t`).
<figcaption>Detail view of notification with title</figcaption>
</figure>
!!! info
ntfy supports UTF-8 in HTTP headers, but [not every library or programming language does](https://www.jmix.io/blog/utf-8-in-http-headers/).
If non-ASCII characters are causing issues for you in the title (i.e. you're seeing `?` symbols), you may also encode the `X-Title` or `X-Message`
header as [RFC 2047](https://datatracker.ietf.org/doc/html/rfc2047#section-2), e.g. `=?UTF-8?B?8J+HqfCfh6o=?=` ([base64](https://en.wikipedia.org/wiki/Base64)),
or `=?UTF-8?Q?=C3=84pfel?=` ([quoted-printable](https://en.wikipedia.org/wiki/Quoted-printable)).
## Message priority
_Supported on:_ :material-android: :material-apple: :material-firefox:
@@ -432,10 +456,14 @@ You can set the priority with the header `X-Priority` (or any of its aliases: `P
=== "PowerShell"
``` powershell
$uri = "https://ntfy.sh/phil_alerts"
$headers = @{ Priority="5" }
$body = "An urgent message"
Invoke-RestMethod -Method 'Post' -Uri $uri -Headers $headers -Body $body -UseBasicParsing
$Request = @{
URI = "https://ntfy.sh/phil_alerts"
Headers = @{
Priority = "5"
}
Body = "An urgent message"
}
Invoke-RestMethod @Request
```
=== "Python"
@@ -553,10 +581,15 @@ them with a comma, e.g. `tag1,tag2,tag3`.
=== "PowerShell"
``` powershell
$uri = "https://ntfy.sh/backups"
$headers = @{ Tags="warning,mailsrv13,daily-backup" }
$body = "Backup of mailsrv13 failed"
Invoke-RestMethod -Method 'Post' -Uri $uri -Headers $headers -Body $body -UseBasicParsing
$Request = @{
Method = "POST"
URI = "https://ntfy.sh/backups"
Headers = @{
Tags = "warning,mailsrv13,daily-backup"
}
Body = "Backup of mailsrv13 failed"
}
Invoke-RestMethod @Request
```
=== "Python"
@@ -645,10 +678,15 @@ to be delivered in 3 days, it'll remain in the cache for 3 days and 12 hours. Al
=== "PowerShell"
``` powershell
$uri = "https://ntfy.sh/hello"
$headers = @{ At="tomorrow, 10am" }
$body = "Good morning"
Invoke-RestMethod -Method 'Post' -Uri $uri -Headers $headers -Body $body -UseBasicParsing
$Request = @{
Method = "POST"
URI = "https://ntfy.sh/hello"
Headers = @{
At = "tomorrow, 10am"
}
Body = "Good morning"
}
Invoke-RestMethod @Request
```
=== "Python"
@@ -729,7 +767,7 @@ For instance, assuming your topic is `mywebhook`, you can simply call `/mywebhoo
=== "PowerShell"
``` powershell
Invoke-RestMethod -Method 'Get' -Uri "ntfy.sh/mywebhook/trigger"
Invoke-RestMethod "ntfy.sh/mywebhook/trigger"
```
=== "Python"
@@ -778,7 +816,7 @@ Here's an example with a custom message, tags and a priority:
=== "PowerShell"
``` powershell
Invoke-RestMethod -Method 'Get' -Uri "ntfy.sh/mywebhook/publish?message=Webhook+triggered&priority=high&tags=warning,skull"
Invoke-RestMethod "ntfy.sh/mywebhook/publish?message=Webhook+triggered&priority=high&tags=warning,skull"
```
=== "Python"
@@ -883,25 +921,29 @@ is the only required one:
=== "PowerShell"
``` powershell
$uri = "https://ntfy.sh"
$body = @{
topic = "mytopic"
title = "Low disk space alert"
message = "Disk space is low at 5.1 GB"
priority = 4
attach = "https://filesrv.lan/space.jpg"
filename = "diskspace.jpg"
tags = @("warning", "cd")
click = "https://homecamera.lan/xasds1h2xsSsa/"
actions = @(
@{
action = "view"
label = "Admin panel"
url = "https://filesrv.lan/admin"
}
$Request = @{
Method = "POST"
URI = "https://ntfy.sh"
Body = @{
Topic = "mytopic"
Title = "Low disk space alert"
Message = "Disk space is low at 5.1 GB"
Priority = 4
Attach = "https://filesrv.lan/space.jpg"
FileName = "diskspace.jpg"
Tags = @("warning", "cd")
Click = "https://homecamera.lan/xasds1h2xsSsa/"
Actions = ConvertTo-JSON @(
@{
Action = "view"
Label = "Admin panel"
URL = "https://filesrv.lan/admin"
}
)
} | ConvertTo-Json
Invoke-RestMethod -Method 'Post' -Uri $uri -Body $body -ContentType "application/json" -UseBasicParsing
}
ContentType = "application/json"
}
Invoke-RestMethod @Request
```
=== "Python"
@@ -1061,10 +1103,15 @@ As an example, here's how you can create the above notification using this forma
=== "PowerShell"
``` powershell
$uri = "https://ntfy.sh/myhome"
$headers = @{ Actions="view, Open portal, https://home.nest.com/, clear=true; http, Turn down, https://api.nest.com/, body='{\"temperature\": 65}'" }
$body = "You left the house. Turn down the A/C?"
Invoke-RestMethod -Method 'Post' -Uri $uri -Headers $headers -Body $body -UseBasicParsing
$Request = @{
Method = "POST"
URI = "https://ntfy.sh/myhome"
Headers = @{
Actions="view, Open portal, https://home.nest.com/, clear=true; http, Turn down, https://api.nest.com/, body='{\"temperature\": 65}'"
}
Body = "You left the house. Turn down the A/C?"
}
Invoke-RestMethod @Request
```
=== "Python"
@@ -1214,26 +1261,30 @@ Alternatively, the same actions can be defined as **JSON array**, if the notific
=== "PowerShell"
``` powershell
$uri = "https://ntfy.sh"
$body = @{
topic = "myhome"
message = "You left the house. Turn down the A/C?"
actions = @(
@{
action = "view"
label = "Open portal"
url = "https://home.nest.com/"
clear = $true
},
@{
action = "http"
label = "Turn down"
url = "https://api.nest.com/"
body = '{"temperature": 65}'
}
$Request = @{
Method = "POST"
URI = "https://ntfy.sh"
Body = ConvertTo-JSON @{
Topic = "myhome"
Message = "You left the house. Turn down the A/C?"
Actions = @(
@{
Action = "view"
Label = "Open portal"
URL = "https://home.nest.com/"
Clear = $true
},
@{
Action = "http"
Label = "Turn down"
URL = "https://api.nest.com/"
Body = '{"temperature": 65}'
}
)
} | ConvertTo-Json
Invoke-RestMethod -Method 'Post' -Uri $uri -Body $body -ContentType "application/json" -UseBasicParsing
}
ContentType = "application/json"
}
Invoke-RestMethod @Request
```
=== "Python"
@@ -1358,10 +1409,15 @@ Here's an example using the [`X-Actions` header](#using-a-header):
=== "PowerShell"
``` powershell
$uri = "https://ntfy.sh/myhome"
$headers = @{ Actions="view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392" }
$body = "Somebody retweeted your tweet."
Invoke-RestMethod -Method 'Post' -Uri $uri -Headers $headers -Body $body -UseBasicParsing
$Request = @{
Method = "POST"
URI = "https://ntfy.sh/myhome"
Headers = @{
Actions = "view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392"
}
Body = "Somebody retweeted your tweet."
}
Invoke-RestMethod @Request
```
=== "Python"
@@ -1474,19 +1530,23 @@ And the same example using [JSON publishing](#publish-as-json):
=== "PowerShell"
``` powershell
$uri = "https://ntfy.sh"
$body = @{
topic = "myhome"
message = "Somebody retweeted your tweet."
actions = @(
@{
"action"="view"
"label"="Open Twitter"
"url"="https://twitter.com/binwiederhier/status/1467633927951163392"
}
$Request = @{
Method = "POST"
URI = "https://ntfy.sh"
Body = ConvertTo-JSON @{
Topic = "myhome"
Message = "Somebody retweeted your tweet."
Actions = @(
@{
Action = "view"
Label = "Open Twitter"
URL = "https://twitter.com/binwiederhier/status/1467633927951163392"
}
)
} | ConvertTo-Json
Invoke-RestMethod -Method 'Post' -Uri $uri -Body $body -ContentType "application/json" -UseBasicParsing
}
ContentType = "application/json"
}
Invoke-RestMethod @Request
```
=== "Python"
@@ -1600,10 +1660,15 @@ Here's an example using the [`X-Actions` header](#using-a-header):
=== "PowerShell"
``` powershell
$uri = "https://ntfy.sh/wifey"
$headers = @{ Actions="broadcast, Take picture, extras.cmd=pic, extras.camera=front" }
$body = "Your wife requested you send a picture of yourself."
Invoke-RestMethod -Method 'Post' -Uri $uri -Headers $headers -Body $body -UseBasicParsing
$Request = @{
Method = "POST"
URI = "https://ntfy.sh/wifey"
Headers = @{
Actions = "broadcast, Take picture, extras.cmd=pic, extras.camera=front"
}
Body = "Your wife requested you send a picture of yourself."
}
Invoke-RestMethod @Request
```
=== "Python"
@@ -1733,23 +1798,26 @@ And the same example using [JSON publishing](#publish-as-json):
``` powershell
# Powershell requires the 'Depth' argument to equal 3 here to expand 'Extras',
# otherwise it will read System.Collections.Hashtable in the returned JSON
$uri = "https://ntfy.sh"
$body = @{
topic = "wifey"
message = "Your wife requested you send a picture of yourself."
actions = @(
@{
action = "broadcast"
label = "Take picture"
extras = @{
cmd ="pic"
camera = "front"
}
$Request = @{
Method = "POST"
URI = "https://ntfy.sh"
Body = @{
Topic = "wifey"
Message = "Your wife requested you send a picture of yourself."
Actions = ConvertTo-Json -Depth 3 @(
@{
Action = "broadcast"
Label = "Take picture"
Extras = @{
CMD ="pic"
Camera = "front"
}
}
)
} | ConvertTo-Json -Depth 3
Invoke-RestMethod -Method 'Post' -Uri $uri -Body $body -ContentType "application/json" -UseBasicParsing
}
ContentType = "application/json"
}
Invoke-RestMethod @Request
```
=== "Python"
@@ -1861,10 +1929,15 @@ Here's an example using the [`X-Actions` header](#using-a-header):
=== "PowerShell"
``` powershell
$uri = "https://ntfy.sh/myhome"
$headers = @{ Actions="http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}" }
$body = "Garage door has been open for 15 minutes. Close it?"
Invoke-RestMethod -Method 'Post' -Uri $uri -Headers $headers -Body $body -UseBasicParsing
$Request = @{
Method = "POST"
URI = "https://ntfy.sh/myhome"
Headers = @{
Actions="http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}"
}
Body = "Garage door has been open for 15 minutes. Close it?"
}
Invoke-RestMethod @Request
```
=== "Python"
@@ -2005,24 +2078,28 @@ And the same example using [JSON publishing](#publish-as-json):
# Powershell requires the 'Depth' argument to equal 3 here to expand 'headers',
# otherwise it will read System.Collections.Hashtable in the returned JSON
$uri = "https://ntfy.sh"
$body = @{
topic = "myhome"
message = "Garage door has been open for 15 minutes. Close it?"
actions = @(
@{
action = "http"
label = "Close door"
url = "https://api.mygarage.lan/"
method = "PUT"
headers = @{
Authorization = "Bearer zAzsx1sk.."
}
body = '{"action": "close"}'
$Request = @{
Method = "POST"
URI = "https://ntfy.sh"
Body = @{
Topic = "myhome"
Message = "Garage door has been open for 15 minutes. Close it?"
Actions = ConvertTo-Json -Depth 3 @(
@{
Action = "http"
Label = "Close door"
URL = "https://api.mygarage.lan/"
Method = "PUT"
Headers = @{
Authorization = "Bearer zAzsx1sk.."
}
Body = ConvertTo-JSON @{Action = "close"}
}
)
} | ConvertTo-Json -Depth 3
Invoke-RestMethod -Method 'Post' -Uri $uri -Body $body -ContentType "application/json" -UseBasicParsing
}
ContentType = "application/json"
}
Invoke-RestMethod @Request
```
=== "Python"
@@ -2149,10 +2226,13 @@ Here's an example that will open Reddit when the notification is clicked:
=== "PowerShell"
``` powershell
$uri = "https://ntfy.sh/reddit_alerts"
$headers = @{ Click="https://www.reddit.com/message/messages" }
$body = "New messages on Reddit"
Invoke-RestMethod -Method 'Post' -Uri $uri -Headers $headers -Body $body -UseBasicParsing
$Request = @{
Method = "POST"
URI = "https://ntfy.sh/reddit_alerts"
Headers = @{ Click="https://www.reddit.com/message/messages" }
Body = "New messages on Reddit"
}
Invoke-RestMethod @Request
```
=== "Python"
@@ -2321,9 +2401,12 @@ Here's an example showing how to attach an APK file:
=== "PowerShell"
``` powershell
$uri = "https://ntfy.sh/mydownloads"
$headers = @{ Attach="https://f-droid.org/F-Droid.apk" }
Invoke-RestMethod -Method 'Post' -Uri $uri -Headers $headers -UseBasicParsing
$Request = @{
Method = "POST"
URI = "https://ntfy.sh/mydownloads"
Headers = @{ Attach="https://f-droid.org/F-Droid.apk" }
}
Invoke-RestMethod @Request
```
=== "Python"
@@ -2414,12 +2497,17 @@ Here's an example showing how to include an icon:
=== "PowerShell"
``` powershell
$uri = "https://ntfy.sh/tvshows"
$headers = @{ Title"="Kodi: Resuming Playback"
Tags="arrow_forward"
Icon="https://styles.redditmedia.com/t5_32uhe/styles/communityIcon_xnt6chtnr2j21.png" }
$body = "The Wire, S01E01"
Invoke-RestMethod -Method 'Post' -Uri $uri -Headers $headers -Body $body -UseBasicParsing
$Request = @{
Method = "POST"
URI = "https://ntfy.sh/tvshows"
Headers = @{
Title = "Kodi: Resuming Playback"
Tags = "arrow_forward"
Icon = "https://styles.redditmedia.com/t5_32uhe/styles/communityIcon_xnt6chtnr2j21.png"
}
Body = "The Wire, S01E01"
}
Invoke-RestMethod @Request
```
=== "Python"
@@ -2525,13 +2613,18 @@ that, your IP address appears in the e-mail body. This is to prevent abuse.
=== "PowerShell"
``` powershell
$uri = "https://ntfy.sh/alerts"
$headers = @{ Title"="Low disk space alert"
Priority="high"
Tags="warning,skull,backup-host,ssh-login")
Email="phil@example.com" }
$body = "Unknown login from 5.31.23.83 to backups.example.com"
Invoke-RestMethod -Method 'Post' -Uri $uri -Body $body -UseBasicParsing
$Request = @{
Method = "POST"
URI = "https://ntfy.sh/alerts"
Headers = @{
Title = "Low disk space alert"
Priority = "high"
Tags = "warning,skull,backup-host,ssh-login")
Email = "phil@example.com"
}
Body = "Unknown login from 5.31.23.83 to backups.example.com"
}
Invoke-RestMethod @Request
```
=== "Python"
@@ -2657,14 +2750,36 @@ Here's an example with a user `testuser` and password `fakepassword`:
http.DefaultClient.Do(req)
```
=== "PowerShell"
=== "PowerShell 7+"
``` powershell
$uri = "https://ntfy.example.com/mysecrets"
$credentials = 'testuser:fakepassword'
$encodedCredentials = [convert]::ToBase64String([text.Encoding]::UTF8.GetBytes($credentials))
$headers = @{Authorization="Basic $encodedCredentials"}
$message = "Look ma, with auth"
Invoke-RestMethod -Uri $uri -Body $message -Headers $headers -Method "Post" -UseBasicParsing
# Get the credentials from the user
$Credential = Get-Credential testuser
# Alternatively, create a PSCredential object with the password from scratch
$Credential = [PSCredential]::new("testuser", (ConvertTo-SecureString "password" -AsPlainText -Force))
# Note that the Authentication parameter requires PowerShell 7 or later
$Request = @{
Method = "POST"
URI = "https://ntfy.example.com/mysecrets"
Authentication = "Basic"
Credential = $Credential
Body = "Look ma, with auth"
}
Invoke-RestMethod @Request
```
=== "PowerShell 5 and earlier"
# With PowerShell 5 or earlier, we need to create the base64 username:password string ourselves
$CredentialString = "$($Credential.Username):$($Credential.GetNetworkCredential().Password)"
$EncodedCredential = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($CredentialString))
$Request = @{
Method = "POST"
URI = "https://ntfy.example.com/mysecrets"
Headers = @{ Authorization = "Basic $EncodedCredential"}
Body = "Look ma, with auth"
}
Invoke-RestMethod @Request
```
=== "Python"
@@ -2761,12 +2876,29 @@ with the token `tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2`:
http.DefaultClient.Do(req)
```
=== "PowerShell"
=== "PowerShell 7+"
``` powershell
$uri = "https://ntfy.example.com/mysecrets"
$headers = @{Authorization="Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2"}
$message = "Look ma, with auth"
Invoke-RestMethod -Uri $uri -Body $message -Headers $headers -Method "Post" -UseBasicParsing
# With PowerShell 7 or greater, we can use the Authentication and Token parameters
$Request = @{
Method = "POST"
URI = "https://ntfy.example.com/mysecrets"
Authorization = "Bearer"
Token = "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2"
Body = "Look ma, with auth"
}
Invoke-RestMethod @Request
```
=== "PowerShell 5 and earlier"
``` powershell
# In PowerShell 5 and below, we can only send the Bearer token as a string in the Headers
$Request = @{
Method = "POST"
URI = "https://ntfy.example.com/mysecrets"
Headers = @{ Authorization = "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2" }
Body = "Look ma, with auth"
}
Invoke-RestMethod @Request
```
=== "Python"
@@ -2841,10 +2973,16 @@ access token. This is primarily useful to make `curl` calls easier, e.g. `curl -
=== "PowerShell"
``` powershell
$uri = "https://ntfy.example.com/mysecrets"
$headers = @{Authorization="Basic OnRrX0FnUWRxN21WQm9GRDM3elFWTjI5Umh1TXpOSXoy"}
$message = "Look ma, with auth"
Invoke-RestMethod -Uri $uri -Body $message -Headers $headers -Method "Post" -UseBasicParsing
# Note that PSCredentials *must* have a username, so we fall back to placing the authorization in the Headers as with PowerShell 5
$Request = @{
Method = "POST"
URI = "https://ntfy.example.com/mysecrets"
Headers = @{
Authorization = "Basic OnRrX0FnUWRxN21WQm9GRDM3elFWTjI5Umh1TXpOSXoy"
}
Body = "Look ma, with auth"
}
Invoke-RestMethod @Request
```
=== "Python"
@@ -2913,9 +3051,12 @@ Here's an example using the `auth` query parameter:
=== "PowerShell"
``` powershell
$uri = "https://ntfy.example.com/mysecrets?auth=QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw"
$message = "Look ma, with auth"
Invoke-RestMethod -Uri $uri -Body $message -Method "Post" -UseBasicParsing
$Request = @{
Method = "POST"
URI = "https://ntfy.example.com/mysecrets?auth=QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw"
Body = "Look ma, with auth"
}
Invoke-RestMethod @Request
```
=== "Python"
@@ -3012,10 +3153,13 @@ are still delivered to connected subscribers, but [`since=`](subscribe/api.md#fe
=== "PowerShell"
``` powershell
$uri = "https://ntfy.sh/mytopic"
$headers = @{ Cache="no" }
$body = "This message won't be stored server-side"
Invoke-RestMethod -Method 'Post' -Uri $uri -Body $body -Headers $headers -UseBasicParsing
$Request = @{
Method = "POST"
URI = "https://ntfy.sh/mytopic"
Headers = @{ Cache="no" }
Body = "This message won't be stored server-side"
}
Invoke-RestMethod @Request
```
=== "Python"
@@ -3092,10 +3236,13 @@ to `no`. This will instruct the server not to forward messages to Firebase.
=== "PowerShell"
``` powershell
$uri = "https://ntfy.sh/mytopic"
$headers = @{ Firebase="no" }
$body = "This message won't be forwarded to FCM"
Invoke-RestMethod -Method 'Post' -Uri $uri -Body $body -Headers $headers -UseBasicParsing
$Request = @{
Method = "POST"
URI = "https://ntfy.sh/mytopic"
Headers = @{ Firebase="no" }
Body = "This message won't be forwarded to FCM"
}
Invoke-RestMethod @Request
```
=== "Python"
@@ -3181,6 +3328,12 @@ The following is a list of all parameters that can be passed when publishing a m
when used in **HTTP headers**, and must be **lowercase** when used as **query parameters in the URL**. They are listed in the
table in their canonical form.
!!! info
ntfy supports UTF-8 in HTTP headers, but [not every library or programming language does](https://www.jmix.io/blog/utf-8-in-http-headers/).
If non-ASCII characters are causing issues for you in the title (i.e. you're seeing `?` symbols), you may also encode the `X-Title` or `X-Message`
header as [RFC 2047](https://datatracker.ietf.org/doc/html/rfc2047#section-2), e.g. `=?UTF-8?B?8J+HqfCfh6o=?=` ([base64](https://en.wikipedia.org/wiki/Base64)),
or `=?UTF-8?Q?=C3=84pfel?=` ([quoted-printable](https://en.wikipedia.org/wiki/Quoted-printable)).
| Parameter | Aliases | Description |
|-----------------|--------------------------------------------|-----------------------------------------------------------------------------------------------|
| `X-Message` | `Message`, `m` | Main body of the message as shown in the notification |

View File

@@ -2,6 +2,18 @@
Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases)
and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).
### ntfy server v2.3.1
Released March 30, 2023
This release disables server-initiated polling of iOS devices entirely, thereby eliminating the thundering herd problem
on ntfy.sh that we observe every 20 minutes. The polling was never strictly necessary, and has actually caused duplicate
delivery issues as well, so disabling it should not have any negative effects. iOS users, please reach out via Discord
or Matrix if there are issues.
**Bug fixes + maintenance:**
* Disable iOS polling entirely ([#677](https://github.com/binwiederhier/ntfy/issues/677)/[#509](https://github.com/binwiederhier/ntfy/issues/509))
## ntfy server v2.3.0
Released March 29, 2023
@@ -1151,3 +1163,24 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
**Additional languages:**
* Swedish (thanks to [@hellbown](https://hosted.weblate.org/user/hellbown/))
### ntfy server v2.4.0 (UNRELEASED)
**Features:**
* [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) can now be installed via Homebrew (thanks to [@Moulick](https://github.com/Moulick))
* Added `v1/stats` endpoint to expose messages stats (no ticket)
* Support [RFC 2047](https://datatracker.ietf.org/doc/html/rfc2047#section-2) encoded headers (no ticket, honorable mention to [mqttwarn](https://github.com/jpmens/mqttwarn/pull/638) and [@amotl](https://github.com/amotl))
**Bug fixes + maintenance:**
* Hide country flags on Windows ([#606](https://github.com/binwiederhier/ntfy/issues/606), thanks to [@cmeis](https://github.com/cmeis) for reporting, and to [@pokej6](https://github.com/pokej6) for fixing it)
* `ntfy sub` now uses default auth credentials as defined in `client.yml` ([#698](https://github.com/binwiederhier/ntfy/issues/698), thanks to [@CrimsonFez](https://github.com/CrimsonFez) for reporting, and to [@wunter8](https://github.com/wunter8) for fixing it)
**Documentation:**
* Updated PowerShell examples ([#697](https://github.com/binwiederhier/ntfy/pull/697), thanks to [@Natfan](https://github.com/Natfan))
**Additional languages:**
* Swedish (thanks to [@hellbown](https://hosted.weblate.org/user/Shjosan/))

View File

@@ -71,7 +71,18 @@ figure video {
}
.remove-md-box td {
padding: 0 10px
padding: 0 10px;
}
.emoji-table .c {
vertical-align: middle !important;
}
.emoji-table .e {
font-size: 2.5em;
padding: 0 2px !important;
text-align: center !important;
vertical-align: middle !important;
}
/* Lightbox; thanks to https://yossiabramov.com/blog/vanilla-js-lightbox */

29
go.mod
View File

@@ -14,28 +14,28 @@ require (
github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8
github.com/stretchr/testify v1.8.1
github.com/urfave/cli/v2 v2.25.1
golang.org/x/crypto v0.7.0
golang.org/x/oauth2 v0.6.0 // indirect
golang.org/x/crypto v0.8.0
golang.org/x/oauth2 v0.7.0 // indirect
golang.org/x/sync v0.1.0
golang.org/x/term v0.6.0
golang.org/x/term v0.7.0
golang.org/x/time v0.3.0
google.golang.org/api v0.114.0
google.golang.org/api v0.119.0
gopkg.in/yaml.v2 v2.4.0
)
require github.com/pkg/errors v0.9.1 // indirect
require (
firebase.google.com/go/v4 v4.10.0
github.com/prometheus/client_golang v1.14.0
github.com/stripe/stripe-go/v74 v74.13.0
firebase.google.com/go/v4 v4.11.0
github.com/prometheus/client_golang v1.15.0
github.com/stripe/stripe-go/v74 v74.15.0
)
require (
cloud.google.com/go v0.110.0 // indirect
cloud.google.com/go/compute v1.19.0 // indirect
cloud.google.com/go/compute v1.19.1 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v0.13.0 // indirect
cloud.google.com/go/iam v1.0.0 // indirect
cloud.google.com/go/longrunning v0.4.1 // indirect
github.com/AlekSi/pointer v1.2.0 // indirect
github.com/MicahParks/keyfunc v1.9.0 // indirect
@@ -47,6 +47,7 @@ require (
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/s2a-go v0.1.2 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
github.com/googleapis/gax-go/v2 v2.8.0 // indirect
@@ -60,13 +61,13 @@ require (
github.com/stretchr/objx v0.5.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/appengine/v2 v2.0.2 // indirect
google.golang.org/genproto v0.0.0-20230327215041-6ac7f18bb9d5 // indirect
google.golang.org/appengine/v2 v2.0.3 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
google.golang.org/grpc v1.54.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

82
go.sum
View File

@@ -1,20 +1,27 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys=
cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY=
cloud.google.com/go/compute v1.19.0 h1:+9zda3WGgW1ZSTlVppLCYFIr48Pa35q1uG2N1itbCEQ=
cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU=
cloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY=
cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/firestore v1.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA=
cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE=
cloud.google.com/go/iam v0.13.0 h1:+CmB+K0J/33d0zSQ9SlFWUeCCEn5XJA0ZMZ3pHE9u8k=
cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0=
cloud.google.com/go/iam v1.0.0 h1:hlQJMovyJJwYjZcTohUH4o1L8Z8kYz+E+W/zktiLCBc=
cloud.google.com/go/iam v1.0.0/go.mod h1:ikbQ4f1r91wTmBmmOtBCOtuEOei6taatNXytzB7Cxew=
cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM=
cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo=
cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM=
cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E=
firebase.google.com/go/v4 v4.10.0 h1:dgK/8uwfJbzc5LZK/GyRRfIkZEDObN9q0kgEXsjlXN4=
firebase.google.com/go/v4 v4.10.0/go.mod h1:m0gLwPY9fxKggizzglgCNWOGnFnVPifLpqZzo5u3e/A=
firebase.google.com/go/v4 v4.11.0 h1:szjBoiF33A2FavRLIDZjW1mw+OsW/XAtHoYNIqWOjRk=
firebase.google.com/go/v4 v4.11.0/go.mod h1:60c36dWLK4+j05Vw5XMllek3b3PCynU3BfI46OSwsUE=
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
@@ -22,13 +29,20 @@ github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@@ -43,9 +57,12 @@ github.com/emersion/go-smtp v0.16.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVR
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
@@ -57,6 +74,7 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
@@ -64,8 +82,10 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -78,6 +98,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
github.com/google/s2a-go v0.1.2 h1:WVtYAYuYxKeYajAmThMRYWP6K3wXkcqbGHeUgeubUHY=
github.com/google/s2a-go v0.1.2/go.mod h1:OJpEgntRZo8ugHpF9hkoLJbS5dSI20XZeXJ9JVywLlM=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -87,6 +109,7 @@ github.com/googleapis/gax-go/v2 v2.8.0 h1:UBtEZqx1bjXtOQ5BVTkuYghXrr3N4V123VKJK6
github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -104,6 +127,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM=
github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
@@ -111,6 +136,7 @@ github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
@@ -119,68 +145,100 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stripe/stripe-go/v74 v74.13.0 h1:n9VIeApHaGsqRQcEsr8ANldfFrLzFSasfNBkq0roPTw=
github.com/stripe/stripe-go/v74 v74.13.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw=
github.com/stripe/stripe-go/v74 v74.14.0 h1:hB1Ocu/m3BUZ+PrTePsPSv8TKcXTrleCL5Y5JfB8zCo=
github.com/stripe/stripe-go/v74 v74.14.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw=
github.com/stripe/stripe-go/v74 v74.15.0 h1:P3ZYrY4CdZeV8Pc/205utqjur+5gcTef+9hgtj8P8IY=
github.com/stripe/stripe-go/v74 v74.15.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw=
github.com/urfave/cli/v2 v2.25.1 h1:zw8dSP7ghX0Gmm8vugrs6q9Ku0wzweqPyshy+syu9Gw=
github.com/urfave/cli/v2 v2.25.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -188,27 +246,41 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/api v0.114.0 h1:1xQPji6cO2E2vLiI+C/XiFAnsn1WV3mjaEwGLhi3grE=
google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
google.golang.org/api v0.119.0 h1:Dzq+ARD6+8jmd5wknJE1crpuzu1JiovEU6gCp9PkoKA=
google.golang.org/api v0.119.0/go.mod h1:CrSvlNEFCFLae9ZUtL1z+61+rEBD7J/aCYwVYKZoWFU=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine/v2 v2.0.2 h1:MSqyWy2shDLwG7chbwBJ5uMyw6SNqJzhJHNDwYB0Akk=
google.golang.org/appengine/v2 v2.0.2/go.mod h1:PkgRUWz4o1XOvbqtWTkBtCitEJ5Tp4HoVEdMMYQR/8E=
google.golang.org/appengine/v2 v2.0.3 h1:AyY/mipuqiyCIAqOevfmu5fMDc5/9P/QggWfCQYdkSA=
google.golang.org/appengine/v2 v2.0.3/go.mod h1:2Z0TTdcXxnHdXzmp8drrmOExUDM2WQgyT33c6JDUlJM=
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-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20230327215041-6ac7f18bb9d5 h1:Kd6tRRHXw8z4TlPlWi+NaK10gsePL6GdZBQChptOLGA=
google.golang.org/genproto v0.0.0-20230327215041-6ac7f18bb9d5/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
google.golang.org/genproto v0.0.0-20230330200707-38013875ee22 h1:n3ThVoQnHbCbnkhZZ1fx3+3fBAisViSwrpbtLV7vydY=
google.golang.org/genproto v0.0.0-20230330200707-38013875ee22/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
@@ -226,6 +298,8 @@ google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cn
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -29,7 +29,7 @@ You can [tag messages](../publish/#tags-emojis) with emojis 🥳 🎉 and other
converted to emojis. This is a reference of all supported emojis. To learn more about the feature, please refer to the
[tagging and emojis page](../publish/#tags-emojis).
<table class="remove-md-box"><tr>
<table class=\"remove-md-box emoji-table\"><tr>
" > "$1"
count="$(cat "$SCRIPTDIR/emoji.json" | jq -r '.[] | .emoji' | wc -l)"
@@ -37,9 +37,9 @@ converted to emojis. This is a reference of all supported emojis. To learn more
for col in 0 1 2; do
from="$(($col * $percolumn + 1))"
to="$(($col * $percolumn + 1 + $percolumn))"
echo "<td><table><thead><tr><th>Tag</th><th>Emoji</th></tr></thead><tbody>" >> "$1"
echo "<td><table><thead><tr><th>Tag</th><th style='text-align: center'>Emoji</th></tr></thead><tbody>" >> "$1"
cat "$SCRIPTDIR/emoji.json" \
| jq -r '.[] | "<tr><td><code>" + .aliases[0] + "</code></td><td>" + .emoji + "</td></tr>"' \
| jq -r '.[] | "<tr><td class=c><code>" + .aliases[0] + "</code></td><td class=e>" + .emoji + "</td></tr>"' \
| sed -n "${from},${to}p" >> "$1"
echo "</tbody></table></td>" >> "$1"
done

View File

@@ -17,6 +17,7 @@ import (
var (
errUnexpectedMessageType = errors.New("unexpected message type")
errMessageNotFound = errors.New("message not found")
errNoRows = errors.New("no rows found")
)
// Messages cache
@@ -54,6 +55,11 @@ const (
CREATE INDEX IF NOT EXISTS idx_sender ON messages (sender);
CREATE INDEX IF NOT EXISTS idx_user ON messages (user);
CREATE INDEX IF NOT EXISTS idx_attachment_expires ON messages (attachment_expires);
CREATE TABLE IF NOT EXISTS stats (
key TEXT PRIMARY KEY,
value INT
);
INSERT INTO stats (key, value) VALUES ('messages', 0);
COMMIT;
`
insertMessageQuery = `
@@ -108,11 +114,14 @@ const (
selectAttachmentsExpiredQuery = `SELECT mid FROM messages WHERE attachment_expires > 0 AND attachment_expires <= ? AND attachment_deleted = 0`
selectAttachmentsSizeBySenderQuery = `SELECT IFNULL(SUM(attachment_size), 0) FROM messages WHERE user = '' AND sender = ? AND attachment_expires >= ?`
selectAttachmentsSizeByUserIDQuery = `SELECT IFNULL(SUM(attachment_size), 0) FROM messages WHERE user = ? AND attachment_expires >= ?`
selectStatsQuery = `SELECT value FROM stats WHERE key = 'messages'`
updateStatsQuery = `UPDATE stats SET value = ? WHERE key = 'messages'`
)
// Schema management queries
const (
currentSchemaVersion = 10
currentSchemaVersion = 11
createSchemaVersionTableQuery = `
CREATE TABLE IF NOT EXISTS schemaVersion (
id INT PRIMARY KEY,
@@ -222,20 +231,30 @@ const (
CREATE INDEX IF NOT EXISTS idx_attachment_expires ON messages (attachment_expires);
`
migrate9To10UpdateMessageExpiryQuery = `UPDATE messages SET expires = time + ?`
// 10 -> 11
migrate10To11AlterMessagesTableQuery = `
CREATE TABLE IF NOT EXISTS stats (
key TEXT PRIMARY KEY,
value INT
);
INSERT INTO stats (key, value) VALUES ('messages', 0);
`
)
var (
migrations = map[int]func(db *sql.DB, cacheDuration time.Duration) error{
0: migrateFrom0,
1: migrateFrom1,
2: migrateFrom2,
3: migrateFrom3,
4: migrateFrom4,
5: migrateFrom5,
6: migrateFrom6,
7: migrateFrom7,
8: migrateFrom8,
9: migrateFrom9,
0: migrateFrom0,
1: migrateFrom1,
2: migrateFrom2,
3: migrateFrom3,
4: migrateFrom4,
5: migrateFrom5,
6: migrateFrom6,
7: migrateFrom7,
8: migrateFrom8,
9: migrateFrom9,
10: migrateFrom10,
}
)
@@ -706,6 +725,26 @@ func readMessage(rows *sql.Rows) (*message, error) {
}, nil
}
func (c *messageCache) UpdateStats(messages int64) error {
_, err := c.db.Exec(updateStatsQuery, messages)
return err
}
func (c *messageCache) Stats() (messages int64, err error) {
rows, err := c.db.Query(selectStatsQuery)
if err != nil {
return 0, err
}
defer rows.Close()
if !rows.Next() {
return 0, errNoRows
}
if err := rows.Scan(&messages); err != nil {
return 0, err
}
return messages, nil
}
func (c *messageCache) Close() error {
return c.db.Close()
}
@@ -889,3 +928,19 @@ func migrateFrom9(db *sql.DB, cacheDuration time.Duration) error {
}
return tx.Commit()
}
func migrateFrom10(db *sql.DB, cacheDuration time.Duration) error {
log.Tag(tagMessageCache).Info("Migrating cache database schema: from 10 to 11")
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.Exec(migrate10To11AlterMessagesTableQuery); err != nil {
return err
}
if _, err := tx.Exec(updateSchemaVersion, 11); err != nil {
return err
}
return tx.Commit()
}

View File

@@ -48,7 +48,8 @@ type Server struct {
topics map[string]*topic
visitors map[string]*visitor // ip:<ip> or user:<user>
firebaseClient *firebaseClient
messages int64
messages int64 // Total number of messages (persisted if messageCache enabled)
messagesHistory []int64 // Last n values of the messages counter, used to determine rate
userManager *user.Manager // Might be nil!
messageCache *messageCache // Database that stores the messages
fileCache *fileCache // File system based cache that stores attachments
@@ -56,7 +57,7 @@ type Server struct {
priceCache *util.LookupCache[map[string]int64] // Stripe price ID -> price as cents (USD implied!)
metricsHandler http.Handler // Handles /metrics if enable-metrics set, and listen-metrics-http not set
closeChan chan bool
mu sync.Mutex
mu sync.RWMutex
}
// handleFunc extends the normal http.HandlerFunc to be able to easily return errors
@@ -79,7 +80,8 @@ var (
matrixPushPath = "/_matrix/push/v1/notify"
metricsPath = "/metrics"
apiHealthPath = "/v1/health"
apiTiers = "/v1/tiers"
apiStatsPath = "/v1/stats"
apiTiersPath = "/v1/tiers"
apiAccountPath = "/v1/account"
apiAccountTokenPath = "/v1/account/token"
apiAccountPasswordPath = "/v1/account/password"
@@ -116,9 +118,10 @@ const (
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
encodingBase64 = "base64" // Used mainly for binary UnifiedPush messages
jsonBodyBytesLimit = 16384
unifiedPushTopicPrefix = "up" // Temporarily, we rate limit all "up*" topics based on the subscriber
unifiedPushTopicLength = 14
jsonBodyBytesLimit = 16384 // Max number of bytes for a JSON request body
unifiedPushTopicPrefix = "up" // Temporarily, we rate limit all "up*" topics based on the subscriber
unifiedPushTopicLength = 14 // Length of UnifiedPush topics, including the "up" part
messagesHistoryMax = 10 // Number of message count values to keep in memory
)
// WebSocket constants
@@ -148,6 +151,10 @@ func New(conf *Config) (*Server, error) {
if err != nil {
return nil, err
}
messages, err := messageCache.Stats()
if err != nil {
return nil, err
}
var fileCache *fileCache
if conf.AttachmentCacheDir != "" {
fileCache, err = newFileCache(conf.AttachmentCacheDir, conf.AttachmentTotalSizeLimit)
@@ -177,15 +184,17 @@ func New(conf *Config) (*Server, error) {
firebaseClient = newFirebaseClient(sender, auther)
}
s := &Server{
config: conf,
messageCache: messageCache,
fileCache: fileCache,
firebaseClient: firebaseClient,
smtpSender: mailer,
topics: topics,
userManager: userManager,
visitors: make(map[string]*visitor),
stripe: stripe,
config: conf,
messageCache: messageCache,
fileCache: fileCache,
firebaseClient: firebaseClient,
smtpSender: mailer,
topics: topics,
userManager: userManager,
messages: messages,
messagesHistory: []int64{messages},
visitors: make(map[string]*visitor),
stripe: stripe,
}
s.priceCache = util.NewLookupCache(s.fetchStripePrices, conf.StripePriceCacheDuration)
return s, nil
@@ -441,7 +450,9 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
return s.ensurePaymentsEnabled(s.ensureStripeCustomer(s.handleAccountBillingPortalSessionCreate))(w, r, v)
} else if r.Method == http.MethodPost && r.URL.Path == apiAccountBillingWebhookPath {
return s.ensurePaymentsEnabled(s.ensureUserManager(s.handleAccountBillingWebhook))(w, r, v) // This request comes from Stripe!
} else if r.Method == http.MethodGet && r.URL.Path == apiTiers {
} else if r.Method == http.MethodGet && r.URL.Path == apiStatsPath {
return s.handleStats(w, r, v)
} else if r.Method == http.MethodGet && r.URL.Path == apiTiersPath {
return s.ensurePaymentsEnabled(s.handleBillingTiersGet)(w, r, v)
} else if r.Method == http.MethodGet && r.URL.Path == matrixPushPath {
return s.handleMatrixDiscovery(w)
@@ -546,17 +557,34 @@ func (s *Server) handleMetrics(w http.ResponseWriter, r *http.Request, _ *visito
return nil
}
// handleStatic returns all static resources (excluding the docs), including the web app
func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request, _ *visitor) error {
r.URL.Path = webSiteDir + r.URL.Path
util.Gzip(http.FileServer(http.FS(webFsCached))).ServeHTTP(w, r)
return nil
}
// handleDocs returns static resources related to the docs
func (s *Server) handleDocs(w http.ResponseWriter, r *http.Request, _ *visitor) error {
util.Gzip(http.FileServer(http.FS(docsStaticCached))).ServeHTTP(w, r)
return nil
}
// handleStats returns the publicly available server stats
func (s *Server) handleStats(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
s.mu.RLock()
messages, n, rate := s.messages, len(s.messagesHistory), float64(0)
if n > 1 {
rate = float64(s.messagesHistory[n-1]-s.messagesHistory[0]) / (float64(n-1) * s.config.ManagerInterval.Seconds())
}
s.mu.RUnlock()
response := &apiStatsResponse{
Messages: messages,
MessagesRate: rate,
}
return s.writeJSON(w, response)
}
// handleFile processes the download of attachment files. The method handles GET and HEAD requests against a file.
// Before streaming the file to a client, it locates uploader (m.Sender or m.User) in the message cache, so it
// can associate the download bandwidth with the uploader.
@@ -815,7 +843,7 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) {
func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email string, unifiedpush bool, err *errHTTP) {
cache = readBoolParam(r, true, "x-cache", "cache")
firebase = readBoolParam(r, true, "x-firebase", "firebase")
m.Title = readParam(r, "x-title", "title", "t")
m.Title = maybeDecodeHeader(readParam(r, "x-title", "title", "t"))
m.Click = readParam(r, "x-click", "click")
icon := readParam(r, "x-icon", "icon")
filename := readParam(r, "x-filename", "filename", "file", "f")
@@ -856,7 +884,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
}
messageStr := strings.ReplaceAll(readParam(r, "x-message", "message", "m"), "\\n", "\n")
if messageStr != "" {
m.Message = messageStr
m.Message = maybeDecodeHeader(messageStr)
}
var e error
m.Priority, e = util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p"))
@@ -1529,8 +1557,14 @@ func (s *Server) runFirebaseKeepaliver() {
select {
case <-time.After(s.config.FirebaseKeepaliveInterval):
s.sendToFirebase(v, newKeepaliveMessage(firebaseControlTopic))
case <-time.After(s.config.FirebasePollInterval):
s.sendToFirebase(v, newKeepaliveMessage(firebasePollTopic))
/*
FIXME: Disable iOS polling entirely for now due to thundering herd problem (see #677)
To solve this, we'd have to shard the iOS poll topics to spread out the polling evenly.
Given that it's not really necessary to poll, turning it off for now should not have any impact.
case <-time.After(s.config.FirebasePollInterval):
s.sendToFirebase(v, newKeepaliveMessage(firebasePollTopic))
*/
case <-s.closeChan:
return
}
@@ -1574,9 +1608,9 @@ func (s *Server) sendDelayedMessages() error {
func (s *Server) sendDelayedMessage(v *visitor, m *message) error {
logvm(v, m).Debug("Sending delayed message")
s.mu.Lock()
s.mu.RLock()
t, ok := s.topics[m.Topic] // If no subscribers, just mark message as published
s.mu.Unlock()
s.mu.RUnlock()
if ok {
go func() {
// We do not rate-limit messages here, since we've rate limited them in the PUT/POST handler
@@ -1815,3 +1849,17 @@ func (s *Server) writeJSON(w http.ResponseWriter, v any) error {
}
return nil
}
func (s *Server) updateAndWriteStats(messagesCount int64) {
s.mu.Lock()
s.messagesHistory = append(s.messagesHistory, messagesCount)
if len(s.messagesHistory) > messagesHistoryMax {
s.messagesHistory = s.messagesHistory[1:]
}
s.mu.Unlock()
go func() {
if err := s.messageCache.UpdateStats(messagesCount); err != nil {
log.Tag(tagManager).Err(err).Warn("Cannot write messages stats")
}
}()
}

View File

@@ -73,9 +73,14 @@ func (s *Server) execManager() {
}
// Print stats
s.mu.Lock()
s.mu.RLock()
messagesCount, topicsCount, visitorsCount := s.messages, len(s.topics), len(s.visitors)
s.mu.Unlock()
s.mu.RUnlock()
// Update stats
s.updateAndWriteStats(messagesCount)
// Log stats
log.
Tag(tagManager).
Fields(log.Context{

View File

@@ -21,8 +21,6 @@ import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"heckel.io/ntfy/log"
"heckel.io/ntfy/util"
@@ -2106,8 +2104,8 @@ func TestServer_PublishWhileUpdatingStatsWithLotsOfMessages(t *testing.T) {
start = time.Now()
response := request(t, s, "PUT", "/mytopic", "some body", nil)
m := toMessage(t, response.Body.String())
assert.Equal(t, "some body", m.Message)
assert.True(t, time.Since(start) < 100*time.Millisecond)
require.Equal(t, "some body", m.Message)
require.True(t, time.Since(start) < 100*time.Millisecond)
log.Info("Done: Publishing message; took %s", time.Since(start).Round(time.Millisecond))
// Wait for all goroutines
@@ -2399,6 +2397,91 @@ func TestServer_SubscriberRateLimiting_ProtectedTopics_WithDefaultReadWrite(t *t
require.Nil(t, s.topics["announcements"].rateVisitor)
}
func TestServer_MessageHistoryAndStatsEndpoint(t *testing.T) {
c := newTestConfig(t)
c.ManagerInterval = 2 * time.Second
s := newTestServer(t, c)
// Publish some messages, and get stats
for i := 0; i < 5; i++ {
response := request(t, s, "POST", "/mytopic", "some message", nil)
require.Equal(t, 200, response.Code)
}
require.Equal(t, int64(5), s.messages)
require.Equal(t, []int64{0}, s.messagesHistory)
response := request(t, s, "GET", "/v1/stats", "", nil)
require.Equal(t, 200, response.Code)
require.Equal(t, `{"messages":5,"messages_rate":0}`+"\n", response.Body.String())
// Run manager and see message history update
s.execManager()
require.Equal(t, []int64{0, 5}, s.messagesHistory)
response = request(t, s, "GET", "/v1/stats", "", nil)
require.Equal(t, 200, response.Code)
require.Equal(t, `{"messages":5,"messages_rate":2.5}`+"\n", response.Body.String()) // 5 messages in 2 seconds = 2.5 messages per second
// Publish some more messages
for i := 0; i < 10; i++ {
response := request(t, s, "POST", "/mytopic", "some message", nil)
require.Equal(t, 200, response.Code)
}
require.Equal(t, int64(15), s.messages)
require.Equal(t, []int64{0, 5}, s.messagesHistory)
response = request(t, s, "GET", "/v1/stats", "", nil)
require.Equal(t, 200, response.Code)
require.Equal(t, `{"messages":15,"messages_rate":2.5}`+"\n", response.Body.String()) // Rate did not update yet
// Run manager and see message history update
s.execManager()
require.Equal(t, []int64{0, 5, 15}, s.messagesHistory)
response = request(t, s, "GET", "/v1/stats", "", nil)
require.Equal(t, 200, response.Code)
require.Equal(t, `{"messages":15,"messages_rate":3.75}`+"\n", response.Body.String()) // 15 messages in 4 seconds = 3.75 messages per second
}
func TestServer_MessageHistoryMaxSize(t *testing.T) {
s := newTestServer(t, newTestConfig(t))
for i := 0; i < 20; i++ {
s.messages = int64(i)
s.execManager()
}
require.Equal(t, []int64{10, 11, 12, 13, 14, 15, 16, 17, 18, 19}, s.messagesHistory)
}
func TestServer_MessageCountPersistence(t *testing.T) {
c := newTestConfig(t)
s := newTestServer(t, c)
s.messages = 1234
s.execManager()
waitFor(t, func() bool {
messages, err := s.messageCache.Stats()
require.Nil(t, err)
return messages == 1234
})
s = newTestServer(t, c)
require.Equal(t, int64(1234), s.messages)
}
func TestServer_PublishWithUTF8MimeHeader(t *testing.T) {
s := newTestServer(t, newTestConfig(t))
response := request(t, s, "POST", "/mytopic", "some attachment", map[string]string{
"X-Filename": "some attachment.txt",
"X-Message": "=?UTF-8?B?8J+HqfCfh6o=?=",
"X-Title": "=?UTF-8?B?bnRmeSDlvojmo5I=?=, no really I mean it! =?UTF-8?Q?This is q=C3=BC=C3=B6ted-print=C3=A4ble.?=",
})
require.Equal(t, 200, response.Code)
m := toMessage(t, response.Body.String())
require.Equal(t, "🇩🇪", m.Message)
require.Equal(t, "ntfy 很棒, no really I mean it! This is qüöted-printäble.", m.Title)
require.Equal(t, "some attachment.txt", m.Attachment.Name)
}
func newTestConfig(t *testing.T) *Config {
conf := NewConfig()
conf.BaseURL = "http://127.0.0.1:12345"

View File

@@ -239,6 +239,11 @@ type apiHealthResponse struct {
Healthy bool `json:"healthy"`
}
type apiStatsResponse struct {
Messages int64 `json:"messages"`
MessagesRate float64 `json:"messages_rate"` // Average number of messages per second
}
type apiAccountCreateRequest struct {
Username string `json:"username"`
Password string `json:"password"`

View File

@@ -5,11 +5,14 @@ import (
"fmt"
"heckel.io/ntfy/util"
"io"
"mime"
"net/http"
"net/netip"
"strings"
)
var mimeDecoder mime.WordDecoder
func readBoolParam(r *http.Request, defaultValue bool, names ...string) bool {
value := strings.ToLower(readParam(r, names...))
if value == "" {
@@ -114,3 +117,11 @@ func fromContext[T any](r *http.Request, key contextKey) (T, error) {
}
return t, nil
}
func maybeDecodeHeader(header string) string {
decoded, err := mimeDecoder.DecodeHeader(header)
if err != nil {
return header
}
return decoded
}

915
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -278,5 +278,14 @@
"account_basics_tier_upgrade_button": "Надграждане до Pro",
"account_usage_messages_title": "Публикувани съобщения",
"account_tokens_table_last_access_header": "Последен достъп",
"account_basics_tier_payment_overdue": "Имате просрочено задължение. Обновете начина на плащане, защото в противен случай скоро профилът ви ще загуби предимствата на абонамента."
"account_basics_tier_payment_overdue": "Имате просрочено задължение. Обновете начина на плащане, защото в противен случай скоро профилът ви ще загуби предимствата на абонамента.",
"account_usage_basis_ip_description": "Статистиката и ограниченията на използване се отчитат по IP адрес, така че може да бъдат споделени с други потребители. Показаните по-горе ограничения са приблизителни и се основават на съществуващите ограничения на използване.",
"account_delete_dialog_description": "Това действие ще доведе до безвъзвратното изтриване на профила ви, включително на всички данни, които се съхраняват на сървъра. След изтриването потребителското ви име няма да бъде достъпно в продължение на 7 дни. Ако наистина искате да продължите, потвърдете с паролата си в полето по-долу.",
"account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} резервирана тема",
"account_upgrade_dialog_interval_yearly_discount_save_up_to": "спестете до {{discount}}%",
"account_delete_dialog_billing_warning": "Изтриването на профила незабавно отменя и платения абонамент. Няма да имате достъп до таблото за плащания.",
"account_upgrade_dialog_cancel_warning": "Това действие ще <strong>прекрати абонамента</strong> и ще промени профила ви на неплатен на {{date}}. На тази дата резервираните теми, както и пазените на сървъра съобщения, <strong> ще бъдат премахнати</strong>.",
"account_upgrade_dialog_proration_info": "<strong>Преизчисляване на плащания</strong>: При надграждане между платени планове разликата в цената ще бъде <strong>начислена незабавно</strong>. При преминаване към по-евтин план надплатената сума ще бъде използвана за плащане за бъдещи периоди.",
"account_basics_tier_manage_billing_button": "Управление на плащанията",
"account_basics_tier_canceled_subscription": "Абонаментът е прекратен и профилът ще бъде променен на неплатен на {{date}}."
}

View File

@@ -352,5 +352,8 @@
"account_upgrade_dialog_interval_yearly": "Roční",
"account_upgrade_dialog_tier_price_billed_monthly": "{{price}} za rok. Účtuje se měsíčně.",
"account_upgrade_dialog_billing_contact_email": "V případě dotazů týkajících se fakturace nás prosím <Link>kontaktujte</Link> přímo.",
"account_upgrade_dialog_billing_contact_website": "Otázky týkající se fakturace naleznete na našich <Link>webových stránkách</Link>."
"account_upgrade_dialog_billing_contact_website": "Otázky týkající se fakturace naleznete na našich <Link>webových stránkách</Link>.",
"account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} rezervované téma",
"account_upgrade_dialog_tier_features_messages_one": "{{messages}} denní zpráva",
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} denní e-mail"
}

View File

@@ -0,0 +1,43 @@
{
"notifications_delete": "Dileu",
"action_bar_sign_in": "Mewngofnodi",
"notifications_copied_to_clipboard": "Wedi'i gopio i'r clipfwrdd",
"common_cancel": "Canslo",
"nav_button_account": "Cyfrif",
"common_save": "Arbed",
"common_add": "Ychwanegu",
"signup_title": "Creu cyfrif ntfy",
"signup_form_username": "Enw defnyddiwr",
"signup_form_password": "Cyfrinair",
"action_bar_logo_alt": "logo ntfy",
"action_bar_settings": "Gosodiadau",
"action_bar_profile_title": "Proffil",
"action_bar_profile_logout": "Allgofnodi",
"message_bar_publish": "Cyhoeddi neges",
"notifications_attachment_copy_url_button": "Copio URL",
"notifications_attachment_open_title": "Ewch i {{url}}",
"publish_dialog_base_url_label": "URL y Gwasanaeth",
"publish_dialog_priority_high": "Blaenoriaeth uchel",
"publish_dialog_title_label": "Teitl",
"publish_dialog_message_label": "Neges",
"publish_dialog_attach_label": "URL Atodiad",
"publish_dialog_filename_label": "Enw ffeil",
"publish_dialog_filename_placeholder": "Enw ffeil yr atodiad",
"action_bar_account": "Cyfrif",
"action_bar_unsubscribe": "Dad-danysgrifio",
"login_title": "Mewngofnodi i'ch cyfrif ntfy",
"login_form_button_submit": "Mewngofnodi",
"action_bar_change_display_name": "Newid enw arddangos",
"action_bar_profile_settings": "Gosodiadau",
"nav_button_settings": "Gosodiadau",
"nav_button_documentation": "Dogfennaeth",
"alert_not_supported_context_description": "Dim ond dros HTTPS y gellir derbyn cyhoeddiadau. Mae hyn yn gyfyngiad ar yr API <mdnLink>Notifications</mdnLink>.",
"notifications_attachment_open_button": "Agor atodiad",
"notifications_attachment_file_document": "dogfen arall",
"notifications_click_open_button": "Agor linc",
"publish_dialog_base_url_placeholder": "URL y Gwasanaeth, e.e. https://example.com",
"publish_dialog_attach_placeholder": "Atodi ffeil drwy URL, e.e. https://f-droid.org/F-Droid.apk",
"notifications_click_copy_url_button": "Copio linc",
"notifications_actions_open_url_title": "Ewch i {{url}}",
"publish_dialog_email_label": "Ebost"
}

View File

@@ -352,5 +352,8 @@
"account_basics_tier_interval_monthly": "monatlich",
"account_upgrade_dialog_interval_monthly": "Monatlich",
"account_upgrade_dialog_tier_price_billed_monthly": "{{price}} pro Jahr. Monatlich abgerechnet.",
"account_upgrade_dialog_interval_yearly": "Jährlich"
"account_upgrade_dialog_interval_yearly": "Jährlich",
"account_upgrade_dialog_tier_features_messages_one": "{{messages}} tägliche Nachricht",
"account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} reserviertes Thema",
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} tägliche E-Mail"
}

View File

@@ -107,7 +107,7 @@
"prefs_appearance_language_title": "Idioma",
"error_boundary_title": "Oh no, ntfy tuvo un error",
"error_boundary_button_copy_stack_trace": "Copiar el stack trace",
"error_boundary_stack_trace": "Stack trace",
"error_boundary_stack_trace": "Rastreo de pila",
"error_boundary_gathering_info": "Reunir más información …",
"notifications_example": "Ejemplo",
"prefs_notifications_min_priority_title": "Prioridad mínima",
@@ -352,5 +352,8 @@
"account_upgrade_dialog_tier_price_billed_yearly": "{{price}} facturado anualmente. Guardar {{save}}.",
"account_upgrade_dialog_billing_contact_website": "Si tiene preguntas sobre facturación, consulte nuestra <Link>página web</Link>.",
"account_upgrade_dialog_tier_price_billed_monthly": "{{price}} al año. Facturación mensual.",
"account_upgrade_dialog_billing_contact_email": "Para preguntas sobre facturación, por favor <Link>contáctenos</Link> directamente."
"account_upgrade_dialog_billing_contact_email": "Para preguntas sobre facturación, por favor <Link>contáctenos</Link> directamente.",
"account_upgrade_dialog_tier_features_messages_one": "{{messages}} mensaje diario",
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} correo electrónico diario",
"account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} tema reservado"
}

View File

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

View File

@@ -352,5 +352,8 @@
"account_upgrade_dialog_tier_price_per_month": "bulan",
"account_upgrade_dialog_tier_price_billed_monthly": "{{price}} per bulan. Ditagih setiap bulan.",
"account_upgrade_dialog_billing_contact_email": "Untuk pertanyaan penagihan, silakan <Link>hubungi kami</Link> secara langsung.",
"account_upgrade_dialog_billing_contact_website": "Untuk pertanyaan penagihan, silakan menuju ke <Link>situs web</Link> kami."
"account_upgrade_dialog_billing_contact_website": "Untuk pertanyaan penagihan, silakan menuju ke <Link>situs web</Link> kami.",
"account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} topik yang direservasi",
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} surel harian",
"account_upgrade_dialog_tier_features_messages_one": "{{messages}} pesan harian"
}

View File

@@ -352,5 +352,8 @@
"account_upgrade_dialog_tier_price_per_month": "月",
"account_upgrade_dialog_tier_price_billed_monthly": "年間{{price}}。月毎の支払い。",
"account_upgrade_dialog_tier_price_billed_yearly": "年間{{price}}の支払い。{{save}}節約。",
"account_upgrade_dialog_billing_contact_website": "支払いに関する質問は、<Link>ウェブサイト</Link>を参照して下さい。"
"account_upgrade_dialog_billing_contact_website": "支払いに関する質問は、<Link>ウェブサイト</Link>を参照して下さい。",
"account_upgrade_dialog_tier_features_messages_one": "毎日 {{messages}} メッセージ",
"account_upgrade_dialog_tier_features_reservations_one": "予約済みトピック {{reservations}} 件",
"account_upgrade_dialog_tier_features_emails_one": "毎日メール {{emails}} 件"
}

View File

@@ -76,5 +76,284 @@
"signup_form_username": "Användarnamn",
"signup_already_have_account": "Har du redan ett konto? Logga in!",
"signup_disabled": "Registrering är inaktiverad",
"signup_error_username_taken": "Användarnamn [[username]] används redan"
"signup_error_username_taken": "Användarnamn [[username]] används redan",
"notifications_attachment_file_document": "annat dokument",
"notifications_attachment_file_app": "Android app fil",
"notifications_click_copy_url_title": "Kopiera länk till urklipp",
"notifications_none_for_topic_title": "Du har inte fått några notiser för detta ämnet ännu.",
"notifications_none_for_topic_description": "För att kunna skicka notiser till detta ämnet, använd PUT eller POST till ämnets URL.",
"notifications_actions_http_request_title": "Skicka HTTP {{method}} till {{url}}",
"publish_dialog_progress_uploading": "Laddar upp …",
"nav_upgrade_banner_description": "Reservera ämnen, fler meddelanden och e-postmeddelanden och större bilagor",
"publish_dialog_attachment_limits_file_and_quota_reached": "överskrider {{fileSizeLimit}} filgräns och kvot, {{remainingBytes}} återstående",
"publish_dialog_attachment_limits_file_reached": "överskrider {{fileSizeLimit}} filgräns",
"publish_dialog_attachment_limits_quota_reached": "överskrider kvoten, {{remainingBytes}} återstår",
"publish_dialog_message_placeholder": "Skriv ett meddelande här",
"publish_dialog_checkbox_publish_another": "Publicera en till",
"subscribe_dialog_error_user_anonymous": "anonym",
"account_basics_password_dialog_confirm_password_label": "Bekräfta lösenord",
"publish_dialog_email_placeholder": "Adress att vidarebefordra meddelandet till, t.ex. phil@example.com",
"publish_dialog_details_examples_description": "Exempel och en detaljerad beskrivning av alla sändningsfunktioner finns i <docsLink>dokumentationen</docsLink> .",
"publish_dialog_button_send": "Skicka",
"subscribe_dialog_login_button_back": "Tillbaka",
"account_basics_tier_free": "Gratis",
"account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} reserverat ämne",
"account_delete_title": "Ta bort konto",
"account_upgrade_dialog_tier_features_messages_other": "{{messages}} dagliga meddelanden",
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} dagligt e-postmeddelande",
"account_upgrade_dialog_button_cancel": "Avbryt",
"account_tokens_table_copy_to_clipboard": "Kopiera till urklipp",
"account_tokens_table_copied_to_clipboard": "Åtkomsttoken kopierat",
"account_tokens_description": "Använd åtkomsttoken när du publicerar och prenumererar via ntfy API, så att du inte behöver skicka dina kontouppgifter. Läs mer i <Link>dokumentationen</Link>.",
"account_tokens_table_create_token_button": "Skapa åtkomsttoken",
"prefs_users_description_no_sync": "Användare och lösenord synkroniseras inte till ditt konto.",
"error_boundary_unsupported_indexeddb_description": "ntfy-webbappen behöver IndexedDB för att fungera och din webbläsare har inte stöd för IndexedDB i privat surfläge.<br/><br/>Detta är beklagligt, men det är inte heller särskilt meningsfullt att använda ntfy-webbappen i privat surfläge, eftersom allt lagras i webbläsarens lagringsutrymme. Du kan läsa mer om det <githubLink>i detta GitHub-ärende</githubLink>, eller prata med oss på <discordLink>Discord</discordLink> eller <matrixLink>Matrix</matrixLink>.",
"account_basics_tier_interval_monthly": "månadsvis",
"account_basics_tier_interval_yearly": "årligen",
"account_basics_tier_canceled_subscription": "Din prenumeration avbröts och kommer att nedgraderas till ett gratis konto den {{date}}.",
"account_basics_tier_manage_billing_button": "Hantera fakturering",
"account_usage_messages_title": "Publicerade meddelande",
"account_usage_emails_title": "Skickade e-postmeddelanden",
"account_usage_reservations_title": "Reserverade ämnen",
"account_usage_reservations_none": "Inga reserverade ämnen för det här kontot",
"account_usage_attachment_storage_title": "Lagring av bilagor",
"account_usage_attachment_storage_description": "{{filesize}} per fil, raderas efter {{expiry}}",
"account_delete_description": "Ta bort ditt konto permanent",
"account_delete_dialog_description": "Detta kommer att radera ditt konto permanent, inklusive all data som lagras på servern. Efter raderingen kommer ditt användarnamn att vara otillgängligt i 7 dagar. Om du verkligen vill fortsätta, bekräfta med ditt lösenord i rutan nedan.",
"account_delete_dialog_label": "Lösenord",
"account_delete_dialog_button_cancel": "Avbryt",
"account_delete_dialog_button_submit": "Ta bort kontot permanent",
"account_delete_dialog_billing_warning": "Om du raderar ditt konto annulleras också din faktureringsprenumeration omedelbart. Du kommer inte längre att ha tillgång till instrumentpanelen för fakturering.",
"account_upgrade_dialog_title": "Ändra kontonivå",
"account_upgrade_dialog_interval_monthly": "Månadsvis",
"account_upgrade_dialog_interval_yearly": "Årligen",
"account_upgrade_dialog_interval_yearly_discount_save": "spara {{discount}}%",
"account_upgrade_dialog_interval_yearly_discount_save_up_to": "spara upp till {{discount}}%",
"account_upgrade_dialog_cancel_warning": "Detta kommer att <strong>säga upp din prenumeration</strong> och nedgradera ditt konto på {{date}}. På det datumet kommer ämnesreservationer och meddelanden som ligger i cacheminnet på servern <strong>att raderas</strong>.",
"account_upgrade_dialog_proration_info": "<strong>Deklaration</strong>: När du uppgraderar mellan betalda planer kommer prisskillnaden att <strong>debiteras omedelbart</strong>. Vid nedgradering till en lägre nivå kommer saldot att användas för att betala för framtida faktureringsperioder.",
"account_upgrade_dialog_reservations_warning_one": "Den valda nivån tillåter färre reserverade ämnen än din nuvarande nivå. Innan du ändrar nivå, <strong>bör du ta bort minst en reservation</strong>. Du kan ta bort reservationer i <Link>Inställningar</Link>.",
"account_upgrade_dialog_reservations_warning_other": "Den valda nivån tillåter färre reserverade ämnen än din nuvarande nivå. Innan du ändrar nivå, <strong>ta bort minst {{count}} reservationer</strong>. Du kan ta bort reservationer i <Link>Inställningar</Link>.",
"account_upgrade_dialog_tier_features_no_reservations": "Inga reserverade ämnen",
"account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} per fil",
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} total lagring",
"account_upgrade_dialog_tier_price_per_month": "månad",
"account_upgrade_dialog_tier_selected_label": "Vald",
"account_tokens_table_token_header": "Token",
"account_tokens_dialog_title_create": "Skapa åtkomsttoken",
"account_tokens_dialog_title_delete": "Ta bort åtkomsttoken",
"account_tokens_dialog_label": "Etikett, t.ex. Radarr-meddelanden",
"account_tokens_dialog_title_edit": "Redigera åtkomsttoken",
"account_tokens_dialog_button_create": "Skapa token",
"account_tokens_dialog_button_update": "Uppdatera token",
"account_tokens_delete_dialog_submit_button": "Ta bort token permanent",
"prefs_notifications_delete_after_one_day": "Efter en dag",
"reservation_delete_dialog_action_delete_description": "Cachade meddelanden och bilagor raderas permanent. Denna åtgärd kan inte ångras.",
"error_boundary_gathering_info": "Samla mer information …",
"error_boundary_unsupported_indexeddb_title": "Privat surfning stöds inte",
"reservation_delete_dialog_submit_button": "Ta bort reservationen",
"priority_low": "låg",
"error_boundary_title": "Åh nej, ntfy kraschade",
"error_boundary_description": "Detta får naturligtvis inte ske. Vi beklagar verkligen detta.<br/>Om du har tid, vänligen <githubLink>rapportera detta på GitHub</githubLink>, eller meddela oss via <discordLink>Discord</discordLink> eller <matrixLink>Matrix</matrixLink>.",
"notifications_no_subscriptions_title": "Det ser ut som om du inte har några prenumerationer ännu.",
"notifications_more_details": "Mer information finns på <websiteLink>webbplatsen</websiteLink> eller i <docsLink>dokumentationen</docsLink> .",
"publish_dialog_title_topic": "Publicera till {{topic}}",
"publish_dialog_message_published": "Meddelande publicerat",
"publish_dialog_emoji_picker_show": "Välj emoji",
"publish_dialog_base_url_placeholder": "Service-URL, t.ex. https://example.com",
"publish_dialog_topic_label": "Ämnesnamn",
"publish_dialog_topic_placeholder": "Ämnesnamn, t.ex. phils_alerts",
"publish_dialog_topic_reset": "Återställ ämne",
"publish_dialog_title_label": "Titel",
"publish_dialog_title_placeholder": "Meddelandets rubrik, t.ex. Varning för diskutrymme",
"publish_dialog_tags_label": "Taggar",
"publish_dialog_message_label": "Meddelande",
"publish_dialog_tags_placeholder": "Kommaseparerad lista med taggar, t.ex. warning, srv1-backup",
"publish_dialog_priority_label": "Prioritet",
"publish_dialog_click_label": "Klicka på URL",
"publish_dialog_click_placeholder": "URL som öppnas när man klickar på anmälan",
"publish_dialog_click_reset": "Ta bort klickbar URL",
"publish_dialog_email_reset": "Ta bort vidarebefordran av e-post",
"publish_dialog_attach_label": "URL för bifogade filer",
"publish_dialog_attach_placeholder": "Bifoga fil via URL, t.ex. https://f-droid.org/F-Droid.apk",
"publish_dialog_filename_label": "Filnamn",
"publish_dialog_delay_label": "Fördröjning",
"publish_dialog_filename_placeholder": "Filnamn för bifogad fil",
"publish_dialog_delay_placeholder": "Fördröj leverans, t.ex. {{unixTimestamp}}, {{relativeTime}} eller \"{{naturalLanguage}}\" (endast engelska)",
"publish_dialog_delay_reset": "Ta bort försenad leverans",
"publish_dialog_other_features": "Andra funktioner:",
"publish_dialog_chip_click_label": "Klicka på URL",
"publish_dialog_attached_file_title": "Bifogad fil:",
"publish_dialog_attached_file_filename_placeholder": "Filnamn för bifogad fil",
"emoji_picker_search_placeholder": "Sök emoji",
"subscribe_dialog_subscribe_button_cancel": "Avbryt",
"prefs_notifications_sound_description_some": "Meddelanden spelar upp ljudet {{sound}} när de anländer",
"prefs_notifications_sound_no_sound": "Inget ljud",
"prefs_notifications_min_priority_any": "Alla prioriteringar",
"prefs_notifications_min_priority_low_and_higher": "Låg prioritet och högre",
"prefs_notifications_delete_after_three_hours": "Efter tre timmar",
"prefs_notifications_delete_after_never": "Aldrig",
"prefs_users_table": "Användartabell",
"prefs_users_add_button": "Lägg till användare",
"prefs_users_edit_button": "Redigera användare",
"prefs_users_dialog_title_add": "Lägg till användare",
"prefs_users_dialog_title_edit": "Redigera användare",
"prefs_users_dialog_base_url_label": "Tjänstens URL, t.ex. https://ntfy.sh",
"prefs_users_dialog_password_label": "Lösenord",
"prefs_appearance_title": "Utseende",
"prefs_appearance_language_title": "Språk",
"priority_min": "min",
"priority_default": "standard",
"priority_high": "hög",
"priority_max": "max",
"error_boundary_button_copy_stack_trace": "Kopiera stackspårning",
"error_boundary_stack_trace": "Stackspårning",
"account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} reserverade ämnen",
"account_upgrade_dialog_tier_features_messages_one": "{{messages}} dagligt meddelande",
"account_upgrade_dialog_tier_features_emails_other": "{{emails}} dagliga e-postmeddelanden",
"account_upgrade_dialog_tier_price_billed_monthly": "{{price}} per år. Faktureras månadsvis.",
"account_upgrade_dialog_tier_price_billed_yearly": "{{price}} faktureras årligen. Spara {{save}}.",
"account_upgrade_dialog_tier_current_label": "Aktuell",
"account_upgrade_dialog_billing_contact_email": "För faktureringsfrågor, vänligen <Link>kontakta oss</Link> direkt.",
"account_upgrade_dialog_billing_contact_website": "För frågor om fakturering hänvisar vi till vår <Link>webbplats</Link>.",
"account_upgrade_dialog_button_redirect_signup": "Registrera dig nu",
"account_upgrade_dialog_button_pay_now": "Betala nu och prenumerera",
"account_upgrade_dialog_button_cancel_subscription": "Avbryt prenumeration",
"account_upgrade_dialog_button_update_subscription": "Uppdatera prenumeration",
"account_tokens_table_label_header": "Etikett",
"account_tokens_table_last_access_header": "Sista åtkomst",
"account_tokens_table_expires_header": "Upphör",
"account_tokens_table_never_expires": "Upphör aldrig",
"account_tokens_table_current_session": "Nuvarande webbläsarsession",
"account_tokens_table_cannot_delete_or_edit": "Det går inte att redigera eller ta bort aktuell sessionstoken",
"account_tokens_table_last_origin_tooltip": "Från IP-adress {{ip}}, klicka för att söka upp",
"account_tokens_dialog_button_cancel": "Avbryt",
"account_tokens_dialog_expires_label": "Åtkomsttoken löper ut om",
"account_tokens_dialog_expires_unchanged": "Lämna utgångsdatumet oförändrat",
"account_tokens_dialog_expires_x_hours": "Token går ut om {{hours}} timmar",
"account_tokens_dialog_expires_x_days": "Token löper ut om {{days}} dagar",
"account_tokens_dialog_expires_never": "Token upphör aldrig att gälla",
"account_tokens_delete_dialog_title": "Ta bort åtkomsttoken",
"account_tokens_delete_dialog_description": "Innan du tar bort en åtkomsttoken bör du se till att inga program eller skript använder den aktivt. <strong>Den här åtgärden kan inte ångras</strong>.",
"prefs_notifications_title": "Notifieringar",
"prefs_notifications_sound_title": "Ljud för meddelanden",
"prefs_notifications_sound_description_none": "Meddelanden spelar inte upp något ljud när de kommer",
"prefs_notifications_sound_play": "Spela upp valt ljud",
"prefs_notifications_min_priority_title": "Lägsta prioritet",
"prefs_notifications_min_priority_description_any": "Visa alla meddelanden, oavsett prioritet",
"prefs_notifications_min_priority_description_x_or_higher": "Visa meddelanden om prioritet är {{number}} ({{name}}) eller högre",
"prefs_notifications_min_priority_description_max": "Visa notifieringar om prioritet är 5 (max)",
"prefs_notifications_min_priority_default_and_higher": "Standardprioritet och högre",
"prefs_notifications_min_priority_high_and_higher": "Hög prioritet och högre",
"prefs_notifications_min_priority_max_only": "Bara högsta prioritet",
"prefs_notifications_delete_after_title": "Radera meddelanden",
"prefs_notifications_delete_after_one_week": "Efter en vecka",
"prefs_notifications_delete_after_one_month": "Efter en månad",
"prefs_notifications_delete_after_never_description": "Meddelanden raderas aldrig automatiskt",
"prefs_notifications_delete_after_three_hours_description": "Meddelanden raderas automatiskt efter tre timmar",
"prefs_users_description": "Lägg till/ta bort användare för dina skyddade ämnen här. Observera att användarnamn och lösenord lagras i webbläsarens lokala lagring.",
"prefs_users_delete_button": "Ta bort användare",
"prefs_users_table_cannot_delete_or_edit": "Kan inte ta bort eller redigera inloggad användare",
"prefs_users_table_user_header": "Användare",
"prefs_users_table_base_url_header": "Service-URL",
"prefs_users_dialog_username_label": "Användarnamn, t.ex. phil",
"prefs_reservations_title": "Reserverade ämnen",
"prefs_reservations_description": "Du kan reservera ämnesnamn för personligt bruk här. Genom att reservera ett ämne får du äganderätt till ämnet och kan definiera åtkomstbehörigheter för andra användare till ämnet.",
"prefs_reservations_limit_reached": "Du har nått gränsen för reserverade ämnen.",
"prefs_reservations_add_button": "Lägg till reserverat ämne",
"prefs_reservations_dialog_title_edit": "Redigera reserverat ämne",
"prefs_reservations_dialog_title_delete": "Ta bort ämnesreservation",
"signup_error_creation_limit_reached": "Gränsen för skapande av konton har uppnåtts",
"alert_not_supported_context_description": "Meddelanden stöds endast via HTTPS. Detta är en begränsning av <mdnLink>Notifications API</mdnLink>.",
"notifications_actions_not_supported": "Åtgärd stöds inte i webbapplikationen",
"notifications_none_for_any_description": "För att skicka meddelanden till ett ämne är det bara att PUT eller POST till ämnets URL. Här är ett exempel med ett av dina ämnen.",
"notifications_no_subscriptions_description": "Klicka på länken \"{{linktext}}\" för att skapa eller prenumerera på ett ämne. Därefter kan du skicka meddelanden via PUT eller POST och du får meddelanden här.",
"display_name_dialog_title": "Ändra visningsnamn",
"display_name_dialog_description": "Ange ett alternativt namn för ett ämne som visas i prenumerationslistan. På så sätt kan du lättare identifiera ämnen med komplicerade namn.",
"display_name_dialog_placeholder": "Visningsnamn",
"reserve_dialog_checkbox_label": "Reservera ämne och konfigurera åtkomst",
"publish_dialog_title_no_topic": "Publicera meddelande",
"publish_dialog_progress_uploading_detail": "Laddar upp {{loaded}}/{{{total}} ({{procent}}}%) …",
"publish_dialog_priority_min": "Lägsta prioritet",
"publish_dialog_priority_low": "Låg prioritet",
"publish_dialog_priority_default": "Standard prioritet",
"publish_dialog_priority_high": "Hög prioritet",
"publish_dialog_priority_max": "Högsta prioritet",
"publish_dialog_base_url_label": "Service-URL",
"publish_dialog_email_label": "E-post",
"publish_dialog_attach_reset": "Ta bort URL för bifogade filer",
"publish_dialog_chip_email_label": "Vidarebefordra till e-post",
"publish_dialog_chip_attach_url_label": "Bifoga fil via URL",
"publish_dialog_chip_attach_file_label": "Bifoga lokal fil",
"publish_dialog_chip_delay_label": "Fördröj leveransen",
"publish_dialog_chip_topic_label": "Ändra ämne",
"publish_dialog_button_cancel_sending": "Avbryt sändning",
"publish_dialog_button_cancel": "Avbryt",
"publish_dialog_attached_file_remove": "Ta bort bifogad fil",
"publish_dialog_drop_file_here": "Släpp filen här",
"emoji_picker_search_clear": "Rensa sökning",
"subscribe_dialog_subscribe_title": "Prenumerera på ämnet",
"subscribe_dialog_subscribe_description": "Ämnen kanske inte är lösenordsskyddade, så välj ett namn som inte är lätt att gissa. När du har prenumererat kan du lägga in/lägga in meddelanden.",
"subscribe_dialog_subscribe_topic_placeholder": "Ämnesnamn, t.ex. phils_alerts",
"subscribe_dialog_subscribe_use_another_label": "Använd en annan server",
"subscribe_dialog_subscribe_base_url_label": "Service-URL",
"subscribe_dialog_subscribe_button_generate_topic_name": "Generera namn",
"subscribe_dialog_subscribe_button_subscribe": "Prenumerera",
"subscribe_dialog_login_title": "Inloggning krävs",
"subscribe_dialog_login_description": "Det här ämnet är lösenordsskyddat. Ange användarnamn och lösenord för att prenumerera.",
"subscribe_dialog_login_username_label": "Användarnamn, t.ex. phil",
"subscribe_dialog_login_password_label": "Lösenord",
"subscribe_dialog_login_button_login": "Logga in",
"subscribe_dialog_error_user_not_authorized": "Användaren {{användarnamn}} inte auktoriserad",
"subscribe_dialog_error_topic_already_reserved": "Ämnet är redan reserverat",
"account_basics_title": "Konto",
"account_basics_tier_paid_until": "Prenumerationen är betald fram till {{datum}}, och kommer att förnyas automatiskt",
"account_basics_username_title": "Användarnamn",
"account_basics_username_description": "Hej, det är du ❤",
"account_basics_username_admin_tooltip": "Du är admin",
"account_basics_password_title": "Lösenord",
"account_basics_password_description": "Ändra lösenordet till ditt konto",
"account_basics_tier_payment_overdue": "Din betalning är försenad. Vänligen uppdatera din betalningsmetod, annars kommer ditt konto att nedgraderas inom kort.",
"account_basics_password_dialog_title": "Byt lösenord",
"account_basics_password_dialog_current_password_label": "Aktuellt lösenord",
"account_basics_password_dialog_new_password_label": "Nytt lösenord",
"account_basics_password_dialog_button_submit": "Byt lösenord",
"account_basics_password_dialog_current_password_incorrect": "Felaktigt lösenord",
"account_usage_title": "Användning",
"account_usage_of_limit": "av {{limit}}",
"account_usage_unlimited": "Obegränsad",
"account_usage_limits_reset_daily": "Användningsgränserna återställs dagligen vid midnatt (UTC)",
"account_basics_tier_title": "Kontotyp",
"account_basics_tier_description": "Ditt kontos nivå",
"account_basics_tier_admin": "Admin",
"account_basics_tier_admin_suffix_with_tier": "(med {{tier}}} nivå)",
"account_basics_tier_admin_suffix_no_tier": "(ingen nivå)",
"account_basics_tier_basic": "Grundläggande",
"account_basics_tier_upgrade_button": "Uppgradera till Pro",
"account_basics_tier_change_button": "Ändra",
"account_usage_cannot_create_portal_session": "Det går inte att öppna faktureringsportalen",
"account_usage_basis_ip_description": "Användningsstatistik och begränsningar för det här kontot baseras på din IP-adress, så de kan delas med andra användare. De gränser som visas ovan är ungefärliga och baseras på befintliga gränser.",
"account_tokens_title": "Åtkomsttoken",
"prefs_notifications_delete_after_one_day_description": "Meddelanden raderas automatiskt efter en dag",
"prefs_notifications_delete_after_one_week_description": "Meddelanden raderas automatiskt efter en vecka",
"prefs_notifications_delete_after_one_month_description": "Meddelanden raderas automatiskt efter en månad",
"prefs_users_title": "Hantera användare",
"prefs_reservations_table_not_subscribed": "Prenumererar inte",
"prefs_reservations_table_click_to_subscribe": "Klicka för att prenumerera",
"prefs_reservations_edit_button": "Redigera ämnesåtkomst",
"prefs_reservations_delete_button": "Återställ ämnesåtkomst",
"prefs_reservations_table": "Tabell över reserverade ämnen",
"prefs_reservations_table_topic_header": "Ämne",
"prefs_reservations_table_access_header": "Tillgång",
"prefs_reservations_table_everyone_deny_all": "Endast jag kan publicera och prenumerera",
"prefs_reservations_table_everyone_read_only": "Jag kan publicera och prenumerera, alla kan prenumerera",
"prefs_reservations_table_everyone_write_only": "Jag kan publicera och prenumerera, alla kan publicera",
"prefs_reservations_table_everyone_read_write": "Alla kan publicera och prenumerera",
"prefs_reservations_dialog_title_add": "Reserverade ämnen",
"prefs_reservations_dialog_description": "Genom att reservera ett ämne får du äganderätt till ämnet och kan definiera åtkomstbehörigheter för andra användare till ämnet.",
"prefs_reservations_dialog_topic_label": "Ämne",
"prefs_reservations_dialog_access_label": "Tillgång",
"reservation_delete_dialog_action_keep_title": "Behåll cachade meddelanden och bilagor",
"reservation_delete_dialog_action_keep_description": "Meddelanden och bilagor som lagras på servern blir offentligt synliga för personer som känner till ämnesnamnet.",
"reservation_delete_dialog_action_delete_title": "Ta bort meddelanden och bilagor som sparats i cacheminnet",
"reservation_delete_dialog_description": "Om du tar bort en reservation ger du upp äganderätten till ämnet och låter andra reservera det. Du kan behålla eller radera befintliga meddelanden och bilagor."
}

View File

@@ -352,5 +352,8 @@
"account_upgrade_dialog_interval_yearly_discount_save_up_to": "%{{discount}} kadar tasarruf edin",
"account_upgrade_dialog_interval_monthly": "Aylık",
"account_basics_tier_interval_monthly": "aylık",
"account_upgrade_dialog_billing_contact_website": "Faturalama ile ilgili sorularınız için lütfen <Link>web sitemizi ziyaret edin</Link>."
"account_upgrade_dialog_billing_contact_website": "Faturalama ile ilgili sorularınız için lütfen <Link>web sitemizi ziyaret edin</Link>.",
"account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} ayırtılan konu",
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} günlük e-posta",
"account_upgrade_dialog_tier_features_messages_one": "{{messages}} günlük mesaj"
}

View File

@@ -436,10 +436,17 @@ const Appearance = () => {
const Language = () => {
const { t, i18n } = useTranslation();
const labelId = "prefLanguage";
const randomFlags = shuffle(["🇬🇧", "🇺🇸", "🇪🇸", "🇫🇷", "🇧🇬", "🇨🇿", "🇩🇪", "🇵🇱", "🇺🇦", "🇨🇳", "🇮🇹", "🇭🇺", "🇧🇷", "🇳🇱", "🇮🇩", "🇯🇵", "🇷🇺", "🇹🇷"]).slice(0, 3);
const title = t("prefs_appearance_language_title") + " " + randomFlags.join(" ");
const lang = i18n.language ?? "en";
// Country flags are displayed using emoji. Emoji rendering is handled by platform fonts.
// Windows in particular does not yet play nicely with flag emoji so for now, hide flags on Windows.
const randomFlags = shuffle(["🇬🇧", "🇺🇸", "🇪🇸", "🇫🇷", "🇧🇬", "🇨🇿", "🇩🇪", "🇵🇱", "🇺🇦", "🇨🇳", "🇮🇹", "🇭🇺", "🇧🇷", "🇳🇱", "🇮🇩", "🇯🇵", "🇷🇺", "🇹🇷"]).slice(0, 3);
const showFlags = !navigator.userAgent.includes("Windows");
let title = t("prefs_appearance_language_title");
if (showFlags) {
title += " " + randomFlags.join(" ");
}
const handleChange = async (ev) => {
await i18n.changeLanguage(ev.target.value);
await maybeUpdateAccountSettings({
@@ -476,6 +483,7 @@ const Language = () => {
<MenuItem value="pt_BR">Português (Brasil)</MenuItem>
<MenuItem value="pl">Polski</MenuItem>
<MenuItem value="ru">Русский</MenuItem>
<MenuItem value="sv">Svenska</MenuItem>
<MenuItem value="tr">Türkçe</MenuItem>
</Select>
</FormControl>