Compare commits
78 Commits
v2.2.0
...
utf8-heade
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35eac5b9ad | ||
|
|
6b1f72fec9 | ||
|
|
824ec39d46 | ||
|
|
cfa8d92af1 | ||
|
|
91d2603fe0 | ||
|
|
6be95f8285 | ||
|
|
4783cb1211 | ||
|
|
113ff55426 | ||
|
|
f2f4bbdbd5 | ||
|
|
d931ce8acc | ||
|
|
b1c0d57fb9 | ||
|
|
b3d11f09ba | ||
|
|
1ccf659781 | ||
|
|
3ad639daed | ||
|
|
dc5dbdf6e5 | ||
|
|
e3998d5fce | ||
|
|
8ad1089053 | ||
|
|
1a6b076e87 | ||
|
|
9db9678952 | ||
|
|
037d1d647d | ||
|
|
cb9be5b732 | ||
|
|
99b9792875 | ||
|
|
9471429cb3 | ||
|
|
ea538338cf | ||
|
|
5825f20e98 | ||
|
|
35ad4a0c03 | ||
|
|
b5b4997957 | ||
|
|
69dcc380a3 | ||
|
|
8e04eeaacd | ||
|
|
c63ca95867 | ||
|
|
d6c0ae130f | ||
|
|
e1339ccde7 | ||
|
|
7c1d892779 | ||
|
|
5f2e238a30 | ||
|
|
f69065ca79 | ||
|
|
1c731a3cef | ||
|
|
6cd72683ad | ||
|
|
e86bdf46db | ||
|
|
0adbd87387 | ||
|
|
286ae43d1a | ||
|
|
a75fb08ef1 | ||
|
|
58a0c2a6c6 | ||
|
|
d050956007 | ||
|
|
bdae48afba | ||
|
|
cb5c4c5483 | ||
|
|
e91f07a081 | ||
|
|
7d96be6fb3 | ||
|
|
46c798c71a | ||
|
|
037a51a9d0 | ||
|
|
4596e4bcab | ||
|
|
9b30ada880 | ||
|
|
96d711e19e | ||
|
|
5af5565fb1 | ||
|
|
29c9551548 | ||
|
|
23c5d4e345 | ||
|
|
ff5bf4acd0 | ||
|
|
34c42c55f6 | ||
|
|
07e5b28868 | ||
|
|
06a0654a5a | ||
|
|
8cc23117fe | ||
|
|
f8c4f20a8f | ||
|
|
8053e992e4 | ||
|
|
9db96140e2 | ||
|
|
502d0a0abd | ||
|
|
80b0a94f7e | ||
|
|
338cab1660 | ||
|
|
b8836d674a | ||
|
|
c6a96d19e2 | ||
|
|
bcb24aecd3 | ||
|
|
d72ae47d1f | ||
|
|
a5d2fc172b | ||
|
|
bbab81a1a2 | ||
|
|
78a1ca81e3 | ||
|
|
f090d1313e | ||
|
|
afa4efa140 | ||
|
|
d2b88005f0 | ||
|
|
9eb1f6a186 | ||
|
|
2d8d5b3b95 |
2
.github/workflows/build.yaml
vendored
2
.github/workflows/build.yaml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/test.yaml
vendored
2
.github/workflows/test.yaml
vendored
@@ -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
|
||||
|
||||
14
Makefile
14
Makefile
@@ -141,25 +141,25 @@ web-deps-update:
|
||||
# Main server/client build
|
||||
|
||||
cli: cli-deps
|
||||
goreleaser build --snapshot --rm-dist
|
||||
goreleaser build --snapshot --clean
|
||||
|
||||
cli-linux-amd64: cli-deps-static-sites
|
||||
goreleaser build --snapshot --rm-dist --id ntfy_linux_amd64
|
||||
goreleaser build --snapshot --clean --id ntfy_linux_amd64
|
||||
|
||||
cli-linux-armv6: cli-deps-static-sites cli-deps-gcc-armv6-armv7
|
||||
goreleaser build --snapshot --rm-dist --id ntfy_linux_armv6
|
||||
goreleaser build --snapshot --clean --id ntfy_linux_armv6
|
||||
|
||||
cli-linux-armv7: cli-deps-static-sites cli-deps-gcc-armv6-armv7
|
||||
goreleaser build --snapshot --rm-dist --id ntfy_linux_armv7
|
||||
goreleaser build --snapshot --clean --id ntfy_linux_armv7
|
||||
|
||||
cli-linux-arm64: cli-deps-static-sites cli-deps-gcc-arm64
|
||||
goreleaser build --snapshot --rm-dist --id ntfy_linux_arm64
|
||||
goreleaser build --snapshot --clean --id ntfy_linux_arm64
|
||||
|
||||
cli-windows-amd64: cli-deps-static-sites
|
||||
goreleaser build --snapshot --rm-dist --id ntfy_windows_amd64
|
||||
goreleaser build --snapshot --clean --id ntfy_windows_amd64
|
||||
|
||||
cli-darwin-all: cli-deps-static-sites
|
||||
goreleaser build --snapshot --rm-dist --id ntfy_darwin_all
|
||||
goreleaser build --snapshot --clean --id ntfy_darwin_all
|
||||
|
||||
cli-linux-server: cli-deps-static-sites
|
||||
# This is a target to build the CLI (including the server) manually.
|
||||
|
||||
@@ -126,7 +126,11 @@ account costs. Even small donations are very much appreciated. A big fat **Thank
|
||||
<a href="https://github.com/caseodilla"><img src="https://github.com/caseodilla.png" width="40px" /></a>
|
||||
<a href="https://github.com/0xAF"><img src="https://github.com/0xAF.png" width="40px" /></a>
|
||||
<a href="https://github.com/soonoo"><img src="https://github.com/soonoo.png" width="40px" /></a>
|
||||
<a href="https://github.com/nichu42"><img src="https://github.com/soonoo.png" width="40px" /></a>
|
||||
<a href="https://github.com/nichu42"><img src="https://github.com/nichu42.png" width="40px" /></a>
|
||||
<a href="https://github.com/samliebow"><img src="https://github.com/samliebow.png" width="40px" /></a>
|
||||
<a href="https://github.com/johman10"><img src="https://github.com/johman10.png" width="40px" /></a>
|
||||
<a href="https://github.com/R-Gld"><img src="https://github.com/R-Gld.png" width="40px" /></a>
|
||||
<a href="https://github.com/FingerlessGlov3s"><img src="https://github.com/FingerlessGlov3s.png" width="40px" /></a>
|
||||
|
||||
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:
|
||||
|
||||
@@ -88,6 +88,7 @@ var flagsServe = append(
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "billing-contact", Aliases: []string{"billing_contact"}, EnvVars: []string{"NTFY_BILLING_CONTACT"}, Value: "", Usage: "e-mail or website to display in upgrade dialog (only if payments are enabled)"}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-metrics", Aliases: []string{"enable_metrics"}, EnvVars: []string{"NTFY_ENABLE_METRICS"}, Value: false, Usage: "if set, Prometheus metrics are exposed via the /metrics endpoint"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "metrics-listen-http", Aliases: []string{"metrics_listen_http"}, EnvVars: []string{"NTFY_METRICS_LISTEN_HTTP"}, Usage: "ip:port used to expose the metrics endpoint (implicitly enables metrics)"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "profile-listen-http", Aliases: []string{"profile_listen_http"}, EnvVars: []string{"NTFY_PROFILE_LISTEN_HTTP"}, Usage: "ip:port used to expose the profiling endpoints (implicitly enables profiling)"}),
|
||||
)
|
||||
|
||||
var cmdServe = &cli.Command{
|
||||
@@ -167,6 +168,7 @@ func execServe(c *cli.Context) error {
|
||||
billingContact := c.String("billing-contact")
|
||||
metricsListenHTTP := c.String("metrics-listen-http")
|
||||
enableMetrics := c.Bool("enable-metrics") || metricsListenHTTP != ""
|
||||
profileListenHTTP := c.String("profile-listen-http")
|
||||
|
||||
// Check values
|
||||
if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) {
|
||||
@@ -321,6 +323,7 @@ func execServe(c *cli.Context) error {
|
||||
conf.EnableReservations = enableReservations
|
||||
conf.EnableMetrics = enableMetrics
|
||||
conf.MetricsListenHTTP = metricsListenHTTP
|
||||
conf.ProfileListenHTTP = profileListenHTTP
|
||||
conf.Version = c.App.Version
|
||||
|
||||
// Set up hot-reloading of config
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
|
||||
@@ -1111,16 +1111,38 @@ doing, and/or secure access to the endpoint in your reverse proxy.
|
||||
- `metrics-listen-http` exposes the metrics endpoint via a dedicated `[IP]:port`. If set, this option implicitly
|
||||
enables metrics as well, e.g. "10.0.1.1:9090" or ":9090"
|
||||
|
||||
=== "Using default port"
|
||||
=== "server.yml (Using default port)"
|
||||
```yaml
|
||||
enable-metrics: true
|
||||
```
|
||||
|
||||
=== "Using dedicated IP/port"
|
||||
=== "server.yml (Using dedicated IP/port)"
|
||||
```yaml
|
||||
metrics-listen-http: "10.0.1.1:9090"
|
||||
```
|
||||
|
||||
In Prometheus, an example scrape config would look like this:
|
||||
|
||||
=== "prometheus.yml"
|
||||
```yaml
|
||||
scrape_configs:
|
||||
- job_name: "ntfy"
|
||||
static_configs:
|
||||
- targets: ["10.0.1.1:9090"]
|
||||
```
|
||||
|
||||
Here's an example Grafana dashboard built from the metrics (see [Grafana JSON on GitHub](https://raw.githubusercontent.com/binwiederhier/ntfy/main/examples/grafana-dashboard/ntfy-grafana.json)):
|
||||
|
||||
<figure markdown style="padding-left: 50px; padding-right: 50px">
|
||||
<a href="../../static/img/grafana-dashboard.png" target="_blank"><img src="../../static/img/grafana-dashboard.png"/></a>
|
||||
<figcaption>ntfy Grafana dashboard</figcaption>
|
||||
</figure>
|
||||
|
||||
## Profiling
|
||||
ntfy can expose Go's [net/http/pprof](https://pkg.go.dev/net/http/pprof) endpoints to support profiling of the ntfy server.
|
||||
If enabled, ntfy will listen on a dedicated listen IP/port, which can be accessed via the web browser on `http://<ip>:<port>/debug/pprof/`.
|
||||
This can be helpful to expose bottlenecks, and visualize call flows. To enable, simply set the `profile-listen-http` config option.
|
||||
|
||||
## Logging & debugging
|
||||
By default, ntfy logs to the console (stderr), with an `info` log level, and in a human-readable text format.
|
||||
|
||||
|
||||
3636
docs/emojis.md
3636
docs/emojis.md
File diff suppressed because it is too large
Load Diff
@@ -43,9 +43,9 @@ of the app and [self-host your own ntfy server](install.md).
|
||||
## How much battery does the Android app use?
|
||||
If you use the ntfy.sh server, and you don't use the [instant delivery](subscribe/phone.md#instant-delivery) feature,
|
||||
the Android/iOS app uses no additional battery, since Firebase Cloud Messaging (FCM) is used. If you use your own server,
|
||||
or you use *instant delivery* (Android only), the app has to maintain a constant connection to the server, which consumes
|
||||
about 0-1% of battery in 17h of use (on my phone). There has been a ton of testing and improvement around this. I think it's pretty
|
||||
decent now.
|
||||
or you use *instant delivery* (Android only), or install from F-droid ([which does not support FCM](https://f-droid.org/docs/Inclusion_Policy/)),
|
||||
the app has to maintain a constant connection to the server, which consumes about 0-1% of battery in 17h of use (on my phone).
|
||||
There has been a ton of testing and improvement around this. I think it's pretty decent now.
|
||||
|
||||
## Paid plans? I thought it was open source?
|
||||
All of ntfy will remain open source, with a free software license (Apache 2.0 and GPLv2). If you'd like to self-host, you
|
||||
|
||||
@@ -26,37 +26,37 @@ deb/rpm packages.
|
||||
|
||||
=== "x86_64/amd64"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.2.0/ntfy_2.2.0_linux_x86_64.tar.gz
|
||||
tar zxvf ntfy_2.2.0_linux_x86_64.tar.gz
|
||||
sudo cp -a ntfy_2.2.0_linux_x86_64/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.2.0_linux_x86_64/{client,server}/*.yml /etc/ntfy
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.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.2.0/ntfy_2.2.0_linux_armv6.tar.gz
|
||||
tar zxvf ntfy_2.2.0_linux_armv6.tar.gz
|
||||
sudo cp -a ntfy_2.2.0_linux_armv6/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.2.0_linux_armv6/{client,server}/*.yml /etc/ntfy
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.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.2.0/ntfy_2.2.0_linux_armv7.tar.gz
|
||||
tar zxvf ntfy_2.2.0_linux_armv7.tar.gz
|
||||
sudo cp -a ntfy_2.2.0_linux_armv7/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.2.0_linux_armv7/{client,server}/*.yml /etc/ntfy
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.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.2.0/ntfy_2.2.0_linux_arm64.tar.gz
|
||||
tar zxvf ntfy_2.2.0_linux_arm64.tar.gz
|
||||
sudo cp -a ntfy_2.2.0_linux_arm64/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.2.0_linux_arm64/{client,server}/*.yml /etc/ntfy
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.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.2.0/ntfy_2.2.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.2.0/ntfy_2.2.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.2.0/ntfy_2.2.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.2.0/ntfy_2.2.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.2.0/ntfy_2.2.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.2.0/ntfy_2.2.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.2.0/ntfy_2.2.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.2.0/ntfy_2.2.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.2.0/ntfy_2.2.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.2.0/ntfy_2.2.0_macOS_all.tar.gz > ntfy_2.2.0_macOS_all.tar.gz
|
||||
tar zxvf ntfy_2.2.0_macOS_all.tar.gz
|
||||
sudo cp -a ntfy_2.2.0_macOS_all/ntfy /usr/local/bin/ntfy
|
||||
curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.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.2.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.2.0/ntfy_2.2.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).
|
||||
|
||||
@@ -17,6 +17,7 @@ ntfy community. Thanks to everyone running a public server. **You guys rock!**
|
||||
| [ntfy.adminforge.de](https://ntfy.adminforge.de/) | 🇩🇪 Germany |
|
||||
| [ntfy.envs.net](https://ntfy.envs.net) | 🇩🇪 Germany |
|
||||
| [ntfy.mzte.de](https://ntfy.mzte.de/) | 🇩🇪 Germany |
|
||||
| [ntfy.hostux.net](https://ntfy.hostux.net/) | 🇫🇷 France |
|
||||
|
||||
Please be aware that **server operators can log your messages**. The project also cannot guarantee the reliability
|
||||
and uptime of third party servers, so use of each server is **at your own discretion**.
|
||||
@@ -34,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
|
||||
@@ -61,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
|
||||
|
||||
@@ -86,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)
|
||||
@@ -116,9 +121,20 @@ and uptime of third party servers, so use of each server is **at your own discre
|
||||
- [nodebb-plugin-ntfy](https://github.com/NodeBB/nodebb-plugin-ntfy) - Push notifications for NodeBB forums
|
||||
- [n8n-ntfy](https://github.com/raghavanand98/n8n-ntfy.sh) - n8n community node that lets you use ntfy in your workflows
|
||||
- [nlog-ntfy](https://github.com/MichelMichels/nlog-ntfy) - Send NLog messages over ntfy (C# / .NET / NLog)
|
||||
- [helm-charts](https://github.com/sarab97/helm-charts) - Helm charts of some of the selfhosted services, incl. ntfy
|
||||
- [ntfy_ansible_role](https://github.com/stevenengland/ntfy_ansible_role) (on [Ansible Galaxy](https://galaxy.ansible.com/stevenengland/ntfy)) - Ansible role to install ntfy
|
||||
- [easy2ntfy](https://github.com/chromoxdor/easy2ntfy) - Gateway for ESPeasy to receive commands through ntfy and using easyfetch (HTML/JS)
|
||||
- [ntfy_lite](https://github.com/MPI-IS/ntfy_lite) - Minimalist python API for pushing ntfy notifications (Python)
|
||||
- [notify](https://github.com/guanguans/notify) - 推送通知 (PHP)
|
||||
- [zpool-events](https://github.com/maglar0/zpool-events) - Notify on ZFS pool events (Python)
|
||||
- [ntfyd](https://github.com/joachimschmidt557/ntfyd) - ntfy desktop daemon (Zig)
|
||||
|
||||
## Blog + forum posts
|
||||
|
||||
- [ntfy.sh](https://neo-sahara.com/wp/2023/03/25/ntfy-sh/) - neo-sahara.com - 3/2023
|
||||
- [Using Ntfy to send and receive push notifications - Samuel Rosa de Oliveria - Delphicon 2023](https://www.youtube.com/watch?v=feu0skpI9QI) - youtube.com - 3/2023
|
||||
- [ntfy: własny darmowy system powiadomień](https://sprawdzone.it/ntfy-wlasny-darmowy-system-powiadomien/) - sprawdzone.it - 3/2023
|
||||
- [Deploying ntfy on railway](https://www.youtube.com/watch?v=auJICXtxoNA) - youtube.com - 3/2023
|
||||
- [Start-Job,Variables, and ntfy.sh](https://klingele.dev/2023/03/01/start-jobvariables-and-ntfy-sh/) - klingele.dev - 3/2023
|
||||
- [enviar notificaciones automáticas usando ntfy.sh](https://osiux.com/2023-02-15-send-automatic-notifications-using-ntfy.html) - osiux.com - 2/2023
|
||||
- [Carnet IP动态解析以及通过ntfy推送IP信息](https://blog.wslll.cn/index.php/archives/201/) - blog.wslll.cn - 2/2023
|
||||
@@ -131,10 +147,12 @@ and uptime of third party servers, so use of each server is **at your own discre
|
||||
- [UnifiedPush: a decentralized, open-source push notification protocol](https://f-droid.org/en/2022/12/18/unifiedpush.html) ⭐ - 12/2022
|
||||
- [ntfy setup instructions](https://docs.benjamin-altpeter.de/network/vms/1001029-ntfy/) - benjamin-altpeter.de - 12/2022
|
||||
- [Ntfy Self-Hosted Push Notifications](https://lachlanlife.net/posts/2022-12-ntfy/) - lachlanlife.net - 12/2022
|
||||
- [NTFY - système de notification hyper simple et complet](https://www.youtube.com/watch?v=UieZYWVVgA4) - youtube.com - 12/2022
|
||||
- [ntfy.sh](https://paramdeo.com/til/ntfy-sh) - paramdeo.com - 11/2022
|
||||
- [Using ntfy to warn me when my computer is discharging](https://ulysseszh.github.io/programming/2022/11/28/ntfy-warn-discharge.html) - ulysseszh.github.io - 11/2022
|
||||
- [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
|
||||
|
||||
@@ -8,7 +8,7 @@ For some (many?) users, the iOS app is not refreshing the view when new notifica
|
||||
swipe down, you do not see the newly arrived messages, even though the popup appeared before.
|
||||
|
||||
This is caused by some weirdness between the Notification Service Extension (NSE), SwiftUI and Core Data. I am entirely
|
||||
clueless on how to fix it, sadly, as it is ephemeral and now clear to me what is causing it.
|
||||
clueless on how to fix it, sadly, as it is ephemeral and not clear to me what is causing it.
|
||||
|
||||
Please send experienced iOS developers my way to help me figure this out.
|
||||
|
||||
|
||||
513
docs/publish.md
513
docs/publish.md
@@ -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 |
|
||||
|
||||
@@ -2,6 +2,34 @@
|
||||
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
|
||||
|
||||
This release primarily fixes an issue with delayed messages, and it adds support for Go's profiler (if enabled), which
|
||||
will allow investigating usage spikes in more detail. There will likely be a follow-up release this week to fix the
|
||||
actual spikes [caused by iOS devices](https://github.com/binwiederhier/ntfy/issues/677).
|
||||
|
||||
**Features:**
|
||||
|
||||
* ntfy now supports Go's `pprof` profiler, if enabled (relates to [#677](https://github.com/binwiederhier/ntfy/issues/677))
|
||||
|
||||
**Bug fixes + maintenance:**
|
||||
|
||||
* Fix delayed message sending from authenticated users ([#679](https://github.com/binwiederhier/ntfy/issues/679))
|
||||
* Fixed plural for Polish and other translations ([#678](https://github.com/binwiederhier/ntfy/pull/678), thanks to [@bmoczulski](https://github.com/bmoczulski))
|
||||
|
||||
## ntfy server v2.2.0
|
||||
Released March 17, 2023
|
||||
|
||||
@@ -1125,7 +1153,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
|
||||
|
||||
**Features:**
|
||||
|
||||
* You can now disable UnifiedPush so ntfy does not act as a UnifiedPush distributor ([#646](https://github.com/binwiederhier/ntfy/issues/646), thanks to [@ollien](https://github.com/ollien) for reporting and to [@wunter8](https://github.com/wunter8) for implementing)
|
||||
* You can now disable UnifiedPush so ntfy does not act as a UnifiedPush distributor ([#646](https://github.com/binwiederhier/ntfy/issues/646), thanks to [@ollien](https://github.com/ollien) for reporting and to [@wunter8](https://github.com/wunter8) for implementing)
|
||||
|
||||
**Bug fixes + maintenance:**
|
||||
|
||||
@@ -1135,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/))
|
||||
|
||||
13
docs/static/css/extra.css
vendored
13
docs/static/css/extra.css
vendored
@@ -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 */
|
||||
|
||||
BIN
docs/static/img/android-screenshot-logs.jpg
vendored
Normal file
BIN
docs/static/img/android-screenshot-logs.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
BIN
docs/static/img/grafana-dashboard.png
vendored
Normal file
BIN
docs/static/img/grafana-dashboard.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 334 KiB |
BIN
docs/static/img/web-logs.png
vendored
Normal file
BIN
docs/static/img/web-logs.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
131
docs/troubleshooting.md
Normal file
131
docs/troubleshooting.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# Troubleshooting
|
||||
This page lists a few suggestions of what to do when things don't work as expected. This is not a complete list.
|
||||
If this page does not help, feel free to drop by the [Discord](https://discord.gg/cT7ECsZj9w) or [Matrix](https://matrix.to/#/#ntfy:matrix.org)
|
||||
and ask there. We're happy to help.
|
||||
|
||||
## ntfy server
|
||||
If you host your own ntfy server, and you're having issues with any component, it is always helpful to enable debugging/tracing
|
||||
in the server. You can find detailed instructions in the [Logging & Debugging](config.md#logging-debugging) section, but it ultimately
|
||||
boils down to setting `log-level: debug` or `log-level: trace` in the `server.yml` file:
|
||||
|
||||
=== "server.yml (debug)"
|
||||
``` yaml
|
||||
log-level: debug
|
||||
```
|
||||
|
||||
=== "server.yml (trace)"
|
||||
``` yaml
|
||||
log-level: trace
|
||||
```
|
||||
|
||||
If you're using environment variables, set `NTFY_LOG_LEVEL=debug` (or `trace`) instead. You can also pass `--debug` or `--trace`
|
||||
to the `ntfy serve` command, e.g. `ntfy serve --trace`. If you're using systemd (i.e. `systemctl`) to run ntfy, you can look at
|
||||
the logs using `journalctl -u ntfy -f`. The logs will look something like this:
|
||||
|
||||
=== "Example logs (debug)"
|
||||
```
|
||||
$ ntfy serve --debug
|
||||
2023/03/20 14:45:38 INFO Listening on :2586[http] :1025[smtp], ntfy 2.1.2, log level is DEBUG (tag=startup)
|
||||
2023/03/20 14:45:38 DEBUG Waiting until 2023-03-21 00:00:00 +0000 UTC to reset visitor stats (tag=resetter)
|
||||
2023/03/20 14:45:39 DEBUG Rate limiters reset for visitor (visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=0, visitor_messages_limit=500, visitor_messages_remaining=500, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=60, visitor_seen=2023-03-20T14:45:39.7-04:00)
|
||||
2023/03/20 14:45:39 DEBUG HTTP request started (http_method=POST, http_path=/mytopic, tag=http, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=0, visitor_messages_limit=500, visitor_messages_remaining=500, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=60, visitor_seen=2023-03-20T14:45:39.7-04:00)
|
||||
2023/03/20 14:45:39 DEBUG Received message (http_method=POST, http_path=/mytopic, message_body_size=2, message_delayed=false, message_email=, message_event=message, message_firebase=true, message_id=EZu6i2WZjH0v, message_sender=127.0.0.1, message_time=1679337939, message_unifiedpush=false, tag=publish, topic=mytopic, topic_last_access=2023-03-20T14:45:38.319-04:00, topic_subscribers=0, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=1, visitor_messages_limit=500, visitor_messages_remaining=499, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=59.0002132248, visitor_seen=2023-03-20T14:45:39.7-04:00)
|
||||
2023/03/20 14:45:39 DEBUG Adding message to cache (http_method=POST, http_path=/mytopic, message_body_size=2, message_event=message, message_id=EZu6i2WZjH0v, message_sender=127.0.0.1, message_time=1679337939, tag=publish, topic=mytopic, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=1, visitor_messages_limit=500, visitor_messages_remaining=499, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=59.000259165, visitor_seen=2023-03-20T14:45:39.7-04:00)
|
||||
2023/03/20 14:45:39 DEBUG HTTP request finished (http_method=POST, http_path=/mytopic, tag=http, time_taken_ms=2, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=1, visitor_messages_limit=500, visitor_messages_remaining=499, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=59.0004147334, visitor_seen=2023-03-20T14:45:39.7-04:00)
|
||||
2023/03/20 14:45:39 DEBUG Wrote 1 message(s) in 8.285712ms (tag=message_cache)
|
||||
...
|
||||
```
|
||||
|
||||
=== "Example logs (trace)"
|
||||
```
|
||||
$ ntfy serve --trace
|
||||
2023/03/20 14:40:42 INFO Listening on :2586[http] :1025[smtp], ntfy 2.1.2, log level is TRACE (tag=startup)
|
||||
2023/03/20 14:40:42 DEBUG Waiting until 2023-03-21 00:00:00 +0000 UTC to reset visitor stats (tag=resetter)
|
||||
2023/03/20 14:40:59 DEBUG Rate limiters reset for visitor (visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=0, visitor_messages_limit=500, visitor_messages_remaining=500, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=60, visitor_seen=2023-03-20T14:40:59.893-04:00)
|
||||
2023/03/20 14:40:59 TRACE HTTP request started (http_method=POST, http_path=/mytopic, http_request=POST /mytopic HTTP/1.1
|
||||
User-Agent: curl/7.81.0
|
||||
Accept: */*
|
||||
Content-Length: 2
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
hi, tag=http, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=0, visitor_messages_limit=500, visitor_messages_remaining=500, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=60, visitor_seen=2023-03-20T14:40:59.893-04:00)
|
||||
2023/03/20 14:40:59 TRACE Received message (http_method=POST, http_path=/mytopic, message_body={
|
||||
"id": "Khaup1RVclU3",
|
||||
"time": 1679337659,
|
||||
"expires": 1679380859,
|
||||
"event": "message",
|
||||
"topic": "mytopic",
|
||||
"message": "hi"
|
||||
}, message_body_size=2, message_delayed=false, message_email=, message_event=message, message_firebase=true, message_id=Khaup1RVclU3, message_sender=127.0.0.1, message_time=1679337659, message_unifiedpush=false, tag=publish, topic=mytopic, topic_last_access=2023-03-20T14:40:59.893-04:00, topic_subscribers=0, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=1, visitor_messages_limit=500, visitor_messages_remaining=499, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=59.0001785048, visitor_seen=2023-03-20T14:40:59.893-04:00)
|
||||
2023/03/20 14:40:59 DEBUG Adding message to cache (http_method=POST, http_path=/mytopic, message_body_size=2, message_event=message, message_id=Khaup1RVclU3, message_sender=127.0.0.1, message_time=1679337659, tag=publish, topic=mytopic, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=1, visitor_messages_limit=500, visitor_messages_remaining=499, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=59.0002044368, visitor_seen=2023-03-20T14:40:59.893-04:00)
|
||||
2023/03/20 14:40:59 DEBUG HTTP request finished (http_method=POST, http_path=/mytopic, tag=http, time_taken_ms=1, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=1, visitor_messages_limit=500, visitor_messages_remaining=499, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=59.000220502, visitor_seen=2023-03-20T14:40:59.893-04:00)
|
||||
2023/03/20 14:40:59 TRACE No stream or WebSocket subscribers, not forwarding (message_body_size=2, message_event=message, message_id=Khaup1RVclU3, message_sender=127.0.0.1, message_time=1679337659, tag=publish, topic=mytopic, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=1, visitor_messages_limit=500, visitor_messages_remaining=499, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=59.0002369212, visitor_seen=2023-03-20T14:40:59.893-04:00)
|
||||
2023/03/20 14:41:00 DEBUG Wrote 1 message(s) in 9.529196ms (tag=message_cache)
|
||||
...
|
||||
```
|
||||
|
||||
## Android app
|
||||
On Android, you can turn on logging in the settings under **Settings → Record logs**. This will store up to 1,000 log
|
||||
entries, which you can then copy or upload.
|
||||
|
||||
<figure markdown>
|
||||
{ width=400 }
|
||||
<figcaption>Recording logs on Android</figcaption>
|
||||
</figure>
|
||||
|
||||
When you copy or upload the logs, you can censor them to make it easier to share them with others. ntfy will replace all
|
||||
topics and hostnames with fruits. Here's an example:
|
||||
|
||||
```
|
||||
This is a log of the ntfy Android app. The log shows up to 1,000 entries.
|
||||
Server URLs (aside from ntfy.sh) and topics have been replaced with fruits 🍌🥝🍋🥥🥑🍊🍎🍑.
|
||||
|
||||
Device info:
|
||||
--
|
||||
ntfy: 1.16.0 (play)
|
||||
OS: 4.19.157-perf+
|
||||
Android: 13 (SDK 33)
|
||||
...
|
||||
|
||||
Logs
|
||||
--
|
||||
|
||||
1679339199507 2023-03-20 15:06:39.507 D NtfyMainActivity Battery: ignoring optimizations = true (we want this to be true); instant subscriptions = true; remind time reached = true; banner = false
|
||||
1679339199507 2023-03-20 15:06:39.507 D NtfySubscriberMgr Enqueuing work to refresh subscriber service
|
||||
1679339199589 2023-03-20 15:06:39.589 D NtfySubscriberMgr ServiceStartWorker: Starting foreground service with action START (work ID: a7eeeae9-9356-40df-afbd-236e5ed10a0b)
|
||||
1679339199602 2023-03-20 15:06:39.602 D NtfySubscriberService onStartCommand executed with startId: 262
|
||||
1679339199602 2023-03-20 15:06:39.602 D NtfySubscriberService using an intent with action START
|
||||
1679339199629 2023-03-20 15:06:39.629 D NtfySubscriberService Refreshing subscriptions
|
||||
1679339199629 2023-03-20 15:06:39.629 D NtfySubscriberService - Desired connections: [ConnectionId(baseUrl=https://ntfy.sh, topicsToSubscriptionIds={avocado=23801492, lemon=49013182, banana=1309176509201171073, peach=573300885184666424, pineapple=-5956897229801209316, durian=81453333, starfruit=30489279, fruit12=82532869}), ConnectionId(baseUrl=https://orange.example.com, topicsToSubscriptionIds={apple=4971265, dragonfruit=66809328})]
|
||||
1679339199629 2023-03-20 15:06:39.629 D NtfySubscriberService - Active connections: [ConnectionId(baseUrl=https://orange.example.com, topicsToSubscriptionIds={apple=4971265, dragonfruit=66809328}), ConnectionId(baseUrl=https://ntfy.sh, topicsToSubscriptionIds={avocado=23801492, lemon=49013182, banana=1309176509201171073, peach=573300885184666424, pineapple=-5956897229801209316, durian=81453333, starfruit=30489279, fruit12=82532869})]
|
||||
...
|
||||
```
|
||||
|
||||
To get live logs, or to get more advanced access to an Android phone, you can use [adb](https://developer.android.com/studio/command-line/adb).
|
||||
After you install and [enable adb debugging](https://developer.android.com/studio/command-line/adb#Enabling), you can
|
||||
get detailed logs like so:
|
||||
|
||||
```
|
||||
# Connect to phone (enable Wireless debugging first)
|
||||
adb connect 192.168.1.137:39539
|
||||
|
||||
# Print all logs; you may have to pass the -s option
|
||||
adb logcat
|
||||
adb -s 192.168.1.137:39539 logcat
|
||||
|
||||
# Only list ntfy logs
|
||||
adb logcat --pid=$(adb shell pidof -s io.heckel.ntfy)
|
||||
adb -s 192.168.1.137:39539 logcat --pid=$(adb -s 192.168.1.137:39539 shell pidof -s io.heckel.ntfy)
|
||||
```
|
||||
|
||||
## Web app
|
||||
The web app logs everything to the **developer console**, which you can open by **pressing the F12 key** on your
|
||||
keyboard.
|
||||
|
||||
<figure markdown>
|
||||

|
||||
<figcaption>Web app logs in the developer console</figcaption>
|
||||
</figure>
|
||||
|
||||
## iOS app
|
||||
Sorry, there is no way to debug or get the logs from the iOS app (yet), outside of running the app in Xcode.
|
||||
2400
examples/grafana-dashboard/ntfy-grafana.json
Normal file
2400
examples/grafana-dashboard/ntfy-grafana.json
Normal file
File diff suppressed because it is too large
Load Diff
35
go.mod
35
go.mod
@@ -4,7 +4,7 @@ go 1.18
|
||||
|
||||
require (
|
||||
cloud.google.com/go/firestore v1.9.0 // indirect
|
||||
cloud.google.com/go/storage v1.30.0 // indirect
|
||||
cloud.google.com/go/storage v1.30.1 // indirect
|
||||
github.com/BurntSushi/toml v1.2.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/emersion/go-smtp v0.16.0
|
||||
@@ -13,29 +13,29 @@ require (
|
||||
github.com/mattn/go-sqlite3 v1.14.16
|
||||
github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/urfave/cli/v2 v2.25.0
|
||||
golang.org/x/crypto v0.7.0
|
||||
golang.org/x/oauth2 v0.6.0 // indirect
|
||||
github.com/urfave/cli/v2 v2.25.1
|
||||
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.12.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.18.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,14 +61,14 @@ 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-20230306155012-7f2fa6fef1f4 // indirect
|
||||
google.golang.org/grpc v1.53.0 // 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
|
||||
)
|
||||
|
||||
100
go.sum
100
go.sum
@@ -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.18.0 h1:FEigFqoDbys2cvFkZ9Fjq4gnHBP55anJ0yQyau2f9oY=
|
||||
cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs=
|
||||
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.0 h1:g1yrbxAWOrvg/594228pETWkOi00MLTrOWfh56veU5o=
|
||||
cloud.google.com/go/storage v1.30.0/go.mod h1:xAVretHSROm1BQX4IIsoVgJqw0LqOyX+I/O2GzRAzdE=
|
||||
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.12.0 h1:uakz8Ubngok3G6Pcwc1ssqI3msONE4tdeyi84UooLQk=
|
||||
github.com/stripe/stripe-go/v74 v74.12.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw=
|
||||
github.com/urfave/cli/v2 v2.25.0 h1:ykdZKuQey2zq0yin/l7JOm9Mh+pg72ngYMeB0ABn6q8=
|
||||
github.com/urfave/cli/v2 v2.25.0/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
||||
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,31 +246,43 @@ 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.113.0 h1:3zLZyS9hgne8yoXUFy871yWdQcA2tA6wp59aaCT6Cp4=
|
||||
google.golang.org/api v0.113.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
|
||||
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-20230306155012-7f2fa6fef1f4 h1:DdoeryqhaXp1LtT/emMP1BRJPHHKFi5akj/nbx/zNTA=
|
||||
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s=
|
||||
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.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
|
||||
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
|
||||
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=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@@ -228,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=
|
||||
|
||||
@@ -93,6 +93,7 @@ nav:
|
||||
- "Integrations + projects": integrations.md
|
||||
- "Release notes": releases.md
|
||||
- "Emojis 🥳 🎉": emojis.md
|
||||
- "Troubleshooting": troubleshooting.md
|
||||
- "Known issues": known-issues.md
|
||||
- "Deprecation notices": deprecations.md
|
||||
- "Development": develop.md
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -107,6 +107,7 @@ type Config struct {
|
||||
SMTPServerAddrPrefix string
|
||||
MetricsEnable bool
|
||||
MetricsListenHTTP string
|
||||
ProfileListenHTTP string
|
||||
MessageLimit int
|
||||
MinDelay time.Duration
|
||||
MaxDelay time.Duration
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
111
server/server.go
111
server/server.go
@@ -19,6 +19,7 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -39,6 +40,7 @@ type Server struct {
|
||||
httpServer *http.Server
|
||||
httpsServer *http.Server
|
||||
httpMetricsServer *http.Server
|
||||
httpProfileServer *http.Server
|
||||
unixListener net.Listener
|
||||
smtpServer *smtp.Server
|
||||
smtpServerBackend *smtpBackend
|
||||
@@ -46,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
|
||||
@@ -54,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
|
||||
@@ -77,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"
|
||||
@@ -114,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
|
||||
@@ -146,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)
|
||||
@@ -175,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
|
||||
@@ -217,6 +228,9 @@ func (s *Server) Run() error {
|
||||
if s.config.MetricsListenHTTP != "" {
|
||||
listenStr += fmt.Sprintf(" %s[http/metrics]", s.config.MetricsListenHTTP)
|
||||
}
|
||||
if s.config.ProfileListenHTTP != "" {
|
||||
listenStr += fmt.Sprintf(" %s[http/profile]", s.config.ProfileListenHTTP)
|
||||
}
|
||||
log.Tag(tagStartup).Info("Listening on%s, ntfy %s, log level is %s", listenStr, s.config.Version, log.CurrentLevel().String())
|
||||
if log.IsFile() {
|
||||
fmt.Fprintf(os.Stderr, "Listening on%s, ntfy %s\n", listenStr, s.config.Version)
|
||||
@@ -273,6 +287,18 @@ func (s *Server) Run() error {
|
||||
initMetrics()
|
||||
s.metricsHandler = promhttp.Handler()
|
||||
}
|
||||
if s.config.ProfileListenHTTP != "" {
|
||||
profileMux := http.NewServeMux()
|
||||
profileMux.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
profileMux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
||||
profileMux.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||
profileMux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
||||
profileMux.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
||||
s.httpProfileServer = &http.Server{Addr: s.config.ProfileListenHTTP, Handler: profileMux}
|
||||
go func() {
|
||||
errChan <- s.httpProfileServer.ListenAndServe()
|
||||
}()
|
||||
}
|
||||
if s.config.SMTPServerListen != "" {
|
||||
go func() {
|
||||
errChan <- s.runSMTPServer()
|
||||
@@ -424,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)
|
||||
@@ -529,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.
|
||||
@@ -798,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")
|
||||
@@ -839,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"))
|
||||
@@ -1512,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
|
||||
}
|
||||
@@ -1541,7 +1592,7 @@ func (s *Server) sendDelayedMessages() error {
|
||||
for _, m := range messages {
|
||||
var u *user.User
|
||||
if s.userManager != nil && m.User != "" {
|
||||
u, err = s.userManager.User(m.User)
|
||||
u, err = s.userManager.UserByID(m.User)
|
||||
if err != nil {
|
||||
log.With(m).Err(err).Warn("Error sending delayed message")
|
||||
continue
|
||||
@@ -1557,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
|
||||
@@ -1798,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")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -276,6 +276,14 @@
|
||||
# enable-metrics: false
|
||||
# metrics-listen-http:
|
||||
|
||||
# Profiling
|
||||
#
|
||||
# ntfy can expose Go's net/http/pprof endpoints to support profiling of the ntfy server. If enabled, ntfy will listen
|
||||
# on a dedicated listen IP/port, which can be accessed via the web browser on http://<ip>:<port>/debug/pprof/.
|
||||
# This can be helpful to expose bottlenecks, and visualize call flows. See https://pkg.go.dev/net/http/pprof for details.
|
||||
#
|
||||
# profile-listen-http:
|
||||
|
||||
# Logging options
|
||||
#
|
||||
# By default, ntfy logs to the console (stderr), with an "info" log level, and in a human-readable text format.
|
||||
|
||||
@@ -701,8 +701,7 @@ func TestAccount_Reservation_Delete_Messages_And_Attachments(t *testing.T) {
|
||||
require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, m2.ID))
|
||||
}
|
||||
|
||||
func TestAccount_Persist_UserStats_After_Tier_Change(t *testing.T) {
|
||||
t.Parallel()
|
||||
/*func TestAccount_Persist_UserStats_After_Tier_Change(t *testing.T) {
|
||||
conf := newTestConfigWithAuthFile(t)
|
||||
conf.AuthDefault = user.PermissionReadWrite
|
||||
conf.AuthStatsQueueWriterInterval = 300 * time.Millisecond
|
||||
@@ -763,4 +762,4 @@ func TestAccount_Persist_UserStats_After_Tier_Change(t *testing.T) {
|
||||
require.Equal(t, 200, rr.Code)
|
||||
account, _ = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body))
|
||||
require.Equal(t, int64(2), account.Stats.Messages) // Is not reset!
|
||||
}
|
||||
}*/
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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"
|
||||
@@ -327,13 +325,10 @@ func TestServer_PublishNoCache(t *testing.T) {
|
||||
|
||||
func TestServer_PublishAt(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := newTestConfig(t)
|
||||
c.MinDelay = time.Second
|
||||
c.DelayedSenderInterval = 100 * time.Millisecond
|
||||
s := newTestServer(t, c)
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
response := request(t, s, "PUT", "/mytopic", "a message", map[string]string{
|
||||
"In": "1s",
|
||||
"In": "1h",
|
||||
})
|
||||
require.Equal(t, 200, response.Code)
|
||||
|
||||
@@ -341,22 +336,62 @@ func TestServer_PublishAt(t *testing.T) {
|
||||
messages := toMessages(t, response.Body.String())
|
||||
require.Equal(t, 0, len(messages))
|
||||
|
||||
time.Sleep(time.Second)
|
||||
require.Nil(t, s.sendDelayedMessages())
|
||||
// Update message time to the past
|
||||
fakeTime := time.Now().Add(-10 * time.Second).Unix()
|
||||
_, err := s.messageCache.db.Exec(`UPDATE messages SET time=?`, fakeTime)
|
||||
require.Nil(t, err)
|
||||
|
||||
// Trigger delayed message sending
|
||||
require.Nil(t, s.sendDelayedMessages())
|
||||
response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
|
||||
messages = toMessages(t, response.Body.String())
|
||||
require.Equal(t, 1, len(messages))
|
||||
require.Equal(t, "a message", messages[0].Message)
|
||||
require.Equal(t, netip.Addr{}, messages[0].Sender) // Never return the sender!
|
||||
|
||||
messages, err := s.messageCache.Messages("mytopic", sinceAllMessages, true)
|
||||
messages, err = s.messageCache.Messages("mytopic", sinceAllMessages, true)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 1, len(messages))
|
||||
require.Equal(t, "a message", messages[0].Message)
|
||||
require.Equal(t, "9.9.9.9", messages[0].Sender.String()) // It's stored in the DB though!
|
||||
}
|
||||
|
||||
func TestServer_PublishAt_FromUser(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := newTestServer(t, newTestConfigWithAuthFile(t))
|
||||
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
|
||||
response := request(t, s, "PUT", "/mytopic", "a message", map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "phil"),
|
||||
"In": "1h",
|
||||
})
|
||||
require.Equal(t, 200, response.Code)
|
||||
|
||||
// Message doesn't show up immediately
|
||||
response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
|
||||
messages := toMessages(t, response.Body.String())
|
||||
require.Equal(t, 0, len(messages))
|
||||
|
||||
// Update message time to the past
|
||||
fakeTime := time.Now().Add(-10 * time.Second).Unix()
|
||||
_, err := s.messageCache.db.Exec(`UPDATE messages SET time=?`, fakeTime)
|
||||
require.Nil(t, err)
|
||||
|
||||
// Trigger delayed message sending
|
||||
require.Nil(t, s.sendDelayedMessages())
|
||||
response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
|
||||
messages = toMessages(t, response.Body.String())
|
||||
require.Equal(t, 1, len(messages))
|
||||
require.Equal(t, fakeTime, messages[0].Time)
|
||||
require.Equal(t, "a message", messages[0].Message)
|
||||
|
||||
messages, err = s.messageCache.Messages("mytopic", sinceAllMessages, true)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 1, len(messages))
|
||||
require.Equal(t, "a message", messages[0].Message)
|
||||
require.True(t, strings.HasPrefix(messages[0].User, "u_"))
|
||||
}
|
||||
|
||||
func TestServer_PublishAt_Expires(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
@@ -2069,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
|
||||
@@ -2362,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"
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
69
tools/loadgen/main.go
Normal file
69
tools/loadgen/main.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
baseURL := "https://staging.ntfy.sh"
|
||||
if len(os.Args) > 1 {
|
||||
baseURL = os.Args[1]
|
||||
}
|
||||
for i := 0; i < 2000; i++ {
|
||||
go subscribe(i, baseURL)
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
for i := 0; i < 2000; i++ {
|
||||
go func(worker int) {
|
||||
for {
|
||||
poll(worker, baseURL)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
time.Sleep(time.Hour)
|
||||
}
|
||||
|
||||
func subscribe(worker int, baseURL string) {
|
||||
fmt.Printf("[subscribe] worker=%d STARTING\n", worker)
|
||||
start := time.Now()
|
||||
topic, ip := fmt.Sprintf("subtopic%d", worker), fmt.Sprintf("1.2.%d.%d", (worker/255)%255, worker%255)
|
||||
req, _ := http.NewRequest("GET", fmt.Sprintf("%s/%s/json", baseURL, topic), nil)
|
||||
req.Header.Set("X-Forwarded-For", ip)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
fmt.Printf("[subscribe] worker=%d time=%d error=%s\n", worker, time.Since(start).Milliseconds(), err.Error())
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
for scanner.Scan() {
|
||||
// Do nothing
|
||||
}
|
||||
fmt.Printf("[subscribe] worker=%d status=%d time=%d EXITED\n", worker, resp.StatusCode, time.Since(start).Milliseconds())
|
||||
}
|
||||
|
||||
func poll(worker int, baseURL string) {
|
||||
fmt.Printf("[poll] worker=%d STARTING\n", worker)
|
||||
topic, ip := fmt.Sprintf("polltopic%d", worker), fmt.Sprintf("1.2.%d.%d", (worker/255)%255, worker%255)
|
||||
start := time.Now()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
|
||||
defer cancel()
|
||||
|
||||
//req, _ := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://staging.ntfy.sh/%s/json?poll=1&since=all", topic), nil)
|
||||
req, _ := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%s/json?poll=1&since=all", baseURL, topic), nil)
|
||||
req.Header.Set("X-Forwarded-For", ip)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
fmt.Printf("[poll] worker=%d time=%d status=- error=%s\n", worker, time.Since(start).Milliseconds(), err.Error())
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
fmt.Printf("[poll] worker=%d time=%d status=%s\n", worker, time.Since(start).Milliseconds(), resp.Status)
|
||||
}
|
||||
1204
web/package-lock.json
generated
1204
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -214,8 +214,8 @@
|
||||
"account_delete_description": "احذف حسابك نهائيا",
|
||||
"account_delete_dialog_label": "كلمة المرور",
|
||||
"account_upgrade_dialog_title": "تغيير فئة الحساب",
|
||||
"account_upgrade_dialog_tier_features_messages": "{{messages}} رسائل يومية",
|
||||
"account_upgrade_dialog_tier_features_emails": "{{emails}} من رسائل البريد الإلكتروني اليومية",
|
||||
"account_upgrade_dialog_tier_features_messages_other": "{{messages}} رسائل يومية",
|
||||
"account_upgrade_dialog_tier_features_emails_other": "{{emails}} من رسائل البريد الإلكتروني اليومية",
|
||||
"account_upgrade_dialog_button_cancel": "إلغاء",
|
||||
"account_upgrade_dialog_button_pay_now": "ادفع الآن واشترك",
|
||||
"account_upgrade_dialog_button_cancel_subscription": "إلغاء الاشتراك",
|
||||
@@ -314,7 +314,7 @@
|
||||
"publish_dialog_progress_uploading_detail": "تحميل {{loaded}}/{{total}} ({{percent}}٪) …",
|
||||
"account_basics_tier_interval_monthly": "شهريا",
|
||||
"account_basics_tier_interval_yearly": "سنويا",
|
||||
"account_upgrade_dialog_tier_features_reservations": "{{reservations}} مواضيع محجوزة",
|
||||
"account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} مواضيع محجوزة",
|
||||
"account_upgrade_dialog_billing_contact_website": "للأسئلة المتعلقة بالفوترة، يرجى الرجوع إلى <Link>موقعنا على الويب</Link>.",
|
||||
"prefs_notifications_min_priority_description_x_or_higher": "إظهار الإشعارات إذا كانت الأولوية {{number}} ({{name}}) أو أعلى",
|
||||
"account_upgrade_dialog_billing_contact_email": "للأسئلة المتعلقة بالفوترة، الرجاء <Link>الاتصال بنا</Link> مباشرة.",
|
||||
|
||||
@@ -252,7 +252,7 @@
|
||||
"account_usage_attachment_storage_title": "Хранилище за прикачени файлове",
|
||||
"account_delete_dialog_button_cancel": "Отказ",
|
||||
"account_upgrade_dialog_interval_monthly": "Месечно",
|
||||
"account_upgrade_dialog_tier_features_reservations": "{{reservations}} резервирани теми",
|
||||
"account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} резервирани теми",
|
||||
"account_upgrade_dialog_tier_features_no_reservations": "Няма резервирани теми",
|
||||
"account_tokens_dialog_button_cancel": "Отказ",
|
||||
"account_delete_title": "Премахване на профила",
|
||||
@@ -260,5 +260,32 @@
|
||||
"account_usage_emails_title": "Изпратени съобщения",
|
||||
"account_usage_reservations_title": "Резервирани теми",
|
||||
"account_usage_reservations_none": "Няма резервирани теми",
|
||||
"account_usage_cannot_create_portal_session": "Порталът за разплащане не може да бъде отворен"
|
||||
"account_usage_cannot_create_portal_session": "Порталът за разплащане не може да бъде отворен",
|
||||
"account_upgrade_dialog_interval_yearly": "Годишно",
|
||||
"account_delete_description": "Безвъзвратно премахване на профила",
|
||||
"account_delete_dialog_button_submit": "Безвъзвратно премахване на профила",
|
||||
"account_upgrade_dialog_interval_yearly_discount_save": "отстъпка {{discount}}%",
|
||||
"account_upgrade_dialog_button_cancel": "Отказ",
|
||||
"account_upgrade_dialog_button_redirect_signup": "Регистриране",
|
||||
"account_tokens_table_label_header": "Етикет",
|
||||
"prefs_reservations_edit_button": "Настройки на достъпа",
|
||||
"prefs_reservations_table_topic_header": "Тема",
|
||||
"prefs_reservations_table_access_header": "Достъп",
|
||||
"prefs_reservations_dialog_topic_label": "Тема",
|
||||
"prefs_reservations_dialog_access_label": "Достъп",
|
||||
"account_basics_password_dialog_current_password_incorrect": "Грешна парола",
|
||||
"account_basics_tier_description": "Ниво на профила",
|
||||
"account_basics_tier_upgrade_button": "Надграждане до Pro",
|
||||
"account_usage_messages_title": "Публикувани съобщения",
|
||||
"account_tokens_table_last_access_header": "Последен достъп",
|
||||
"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}}."
|
||||
}
|
||||
|
||||
@@ -287,9 +287,9 @@
|
||||
"account_upgrade_dialog_title": "Změna úrovně účtu",
|
||||
"account_upgrade_dialog_proration_info": "<strong>Prohlášení</strong>: Při přechodu mezi placenými úrovněmi bude rozdíl v ceně <strong>zaúčtován okamžitě</strong>. Při přechodu na nižší úroveň se zůstatek použije na platbu za budoucí zúčtovací období.",
|
||||
"account_upgrade_dialog_reservations_warning_one": "Vybraná úroveň umožňuje méně rezervovaných témat než vaše aktuální úroveň. Než změníte svou úroveň, <strong>odstraňte alespoň jednu rezervaci</strong>. Rezervace můžete odstranit v <Link>Nastavení</Link>.",
|
||||
"account_upgrade_dialog_tier_features_reservations": "{{reservations}} rezervovaných témat",
|
||||
"account_upgrade_dialog_tier_features_messages": "{{messages}} denních zpráv",
|
||||
"account_upgrade_dialog_tier_features_emails": "{{emails}} denních e-mailů",
|
||||
"account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} rezervovaných témat",
|
||||
"account_upgrade_dialog_tier_features_messages_other": "{{messages}} denních zpráv",
|
||||
"account_upgrade_dialog_tier_features_emails_other": "{{emails}} denních e-mailů",
|
||||
"account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} na soubor",
|
||||
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} celkový úložný prostor",
|
||||
"account_upgrade_dialog_tier_selected_label": "Vybráno",
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
43
web/public/static/langs/cy.json
Normal file
43
web/public/static/langs/cy.json
Normal 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"
|
||||
}
|
||||
@@ -201,18 +201,18 @@
|
||||
"account_basics_password_dialog_current_password_label": "Nuværende kodeord",
|
||||
"account_basics_password_dialog_new_password_label": "Nyt kodeord",
|
||||
"notifications_loading": "Indlæser notifikationer…",
|
||||
"account_upgrade_dialog_tier_features_emails": "{{emails}} daglige e-mails",
|
||||
"account_upgrade_dialog_tier_features_emails_other": "{{emails}} daglige e-mails",
|
||||
"account_tokens_table_create_token_button": "Opret adgangstoken",
|
||||
"account_tokens_dialog_title_delete": "Slet adgangstoken",
|
||||
"publish_dialog_chip_email_label": "Videresend til e-mail",
|
||||
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} samlet lagerplads",
|
||||
"subscribe_dialog_subscribe_use_another_label": "Brug en anden server",
|
||||
"account_basics_tier_upgrade_button": "Opgrader til Pro",
|
||||
"account_upgrade_dialog_tier_features_messages": "{{messages}} daglige beskeder",
|
||||
"account_upgrade_dialog_tier_features_messages_other": "{{messages}} daglige beskeder",
|
||||
"account_tokens_table_copy_to_clipboard": "Kopier til udklipsholder",
|
||||
"prefs_reservations_edit_button": "Rediger emneadgang",
|
||||
"account_upgrade_dialog_title": "Skift kontoniveau",
|
||||
"account_upgrade_dialog_tier_features_reservations": "{{reservations}} reserverede emner",
|
||||
"account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} reserverede emner",
|
||||
"account_tokens_dialog_expires_never": "Token udløber aldrig",
|
||||
"account_tokens_table_current_session": "Nuværende browsersession",
|
||||
"account_tokens_dialog_title_edit": "Rediger adgangstoken",
|
||||
|
||||
@@ -264,9 +264,9 @@
|
||||
"account_upgrade_dialog_proration_info": "<strong>Anrechnung</strong>: Wenn Du auf einen höheren kostenpflichtigen Level wechselst wird die Differenz <strong>sofort berechnet</strong>. Beim Wechsel auf ein kleineres Level verwenden wir Dein Guthaben für zukünftige Abrechnungsperioden.",
|
||||
"account_upgrade_dialog_reservations_warning_one": "Das gewählte Level erlaubt weniger reservierte Themen als Dein aktueller Level. <strong>Bitte löschen vor dem Wechsel Deines Levels mindestens eine Reservierung</strong>. Du kannst Reservierungen in den <Link>Einstellungen</Link> löschen.",
|
||||
"account_upgrade_dialog_reservations_warning_other": "Das gewählte Level erlaubt weniger reservierte Themen als Dein aktueller Level. <strong>Bitte löschen vor dem Wechsel Deines Levels mindestens {{count}} Reservierungen</strong>. Du kannst Reservierungen in den <Link>Einstellungen</Link> löschen.",
|
||||
"account_upgrade_dialog_tier_features_reservations": "{{reservations}} reservierte Themen",
|
||||
"account_upgrade_dialog_tier_features_messages": "{{messages}} Nachrichten pro Tag",
|
||||
"account_upgrade_dialog_tier_features_emails": "{{emails}} Emails pro Tag",
|
||||
"account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} reservierte Themen",
|
||||
"account_upgrade_dialog_tier_features_messages_other": "{{messages}} Nachrichten pro Tag",
|
||||
"account_upgrade_dialog_tier_features_emails_other": "{{emails}} Emails pro Tag",
|
||||
"account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} pro Datei",
|
||||
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} gesamter Speicherplatz",
|
||||
"account_upgrade_dialog_tier_selected_label": "Ausgewählt",
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -225,10 +225,13 @@
|
||||
"account_upgrade_dialog_proration_info": "<strong>Proration</strong>: When upgrading between paid plans, the price difference will be <strong>charged immediately</strong>. When downgrading to a lower tier, the balance will be used to pay for future billing periods.",
|
||||
"account_upgrade_dialog_reservations_warning_one": "The selected tier allows fewer reserved topics than your current tier. Before changing your tier, <strong>please delete at least one reservation</strong>. You can remove reservations in the <Link>Settings</Link>.",
|
||||
"account_upgrade_dialog_reservations_warning_other": "The selected tier allows fewer reserved topics than your current tier. Before changing your tier, <strong>please delete at least {{count}} reservations</strong>. You can remove reservations in the <Link>Settings</Link>.",
|
||||
"account_upgrade_dialog_tier_features_reservations": "{{reservations}} reserved topics",
|
||||
"account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} reserved topic",
|
||||
"account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} reserved topics",
|
||||
"account_upgrade_dialog_tier_features_no_reservations": "No reserved topics",
|
||||
"account_upgrade_dialog_tier_features_messages": "{{messages}} daily messages",
|
||||
"account_upgrade_dialog_tier_features_emails": "{{emails}} daily emails",
|
||||
"account_upgrade_dialog_tier_features_messages_one": "{{messages}} daily message",
|
||||
"account_upgrade_dialog_tier_features_messages_other": "{{messages}} daily messages",
|
||||
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} daily email",
|
||||
"account_upgrade_dialog_tier_features_emails_other": "{{emails}} daily emails",
|
||||
"account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} per file",
|
||||
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} total storage",
|
||||
"account_upgrade_dialog_tier_price_per_month": "month",
|
||||
|
||||
@@ -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",
|
||||
@@ -291,12 +291,12 @@
|
||||
"account_delete_dialog_description": "Esto borrará permanentemente su cuenta, incluyendo todos los datos almacenados en el servidor. Tras la eliminación, su nombre de usuario no estará disponible durante 7 días. Si realmente desea continuar, por favor confirme su contraseña en la casilla de abajo.",
|
||||
"account_delete_dialog_label": "Contraseña",
|
||||
"account_delete_dialog_button_submit": "Eliminar permanentemente la cuenta",
|
||||
"account_upgrade_dialog_tier_features_reservations": "{{reservations}} tópicos reservados",
|
||||
"account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} tópicos reservados",
|
||||
"account_upgrade_dialog_cancel_warning": "Esto <strong>cancelará su suscripción</strong> y degradará su cuenta en {{date}}. En esa fecha, sus tópicos reservados y sus mensajes almacenados en caché en el servidor <strong>serán eliminados</strong>.",
|
||||
"account_upgrade_dialog_proration_info": "<strong>Prorrateo</strong>: al actualizar entre planes pagos, la diferencia de precio se <strong>cobrará de inmediato</strong>. Al cambiar a un nivel inferior, el saldo se utilizará para pagar futuros períodos de facturación.",
|
||||
"account_upgrade_dialog_reservations_warning_other": "El nivel seleccionado permite menos tópicos reservados que su nivel actual. Antes de cambiar de nivel, <strong>por favor elimine al menos {{count}} reservaciones</strong>. Puede eliminar reservaciones en <Link>Configuración</Link>.",
|
||||
"account_upgrade_dialog_tier_features_messages": "{{messages}} mensajes diarios",
|
||||
"account_upgrade_dialog_tier_features_emails": "{{emails}} correos diarios",
|
||||
"account_upgrade_dialog_tier_features_messages_other": "{{messages}} mensajes diarios",
|
||||
"account_upgrade_dialog_tier_features_emails_other": "{{emails}} correos diarios",
|
||||
"account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} por archivo",
|
||||
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} almacenamiento total",
|
||||
"account_upgrade_dialog_tier_current_label": "Actual",
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -274,9 +274,9 @@
|
||||
"account_upgrade_dialog_title": "Changer le tarif du compte",
|
||||
"account_upgrade_dialog_proration_info": "<strong>Facturation</strong> : Lors d'un changement entre un plan payant et un autre, la différence de prix sera créditée ou remboursée sur la prochaine facture. Vous ne recevrez pas d'autre facture avant la fin de la prochaine période de facturation.",
|
||||
"account_upgrade_dialog_reservations_warning_other": "Le tarif sélectionné autorise moins de sujets réservés que votre tarif actuel. Avant de changer de tarif, <strong>veuillez supprimer au moins {{count}} sujets réservés</strong>. Vous pouvez supprimer des sujets réservés dans les <Link>Paramètres</Link>.",
|
||||
"account_upgrade_dialog_tier_features_reservations": "{{reservations}} sujets réservés",
|
||||
"account_upgrade_dialog_tier_features_messages": "{{messages}} messages journaliers",
|
||||
"account_upgrade_dialog_tier_features_emails": "{{emails}} emails journaliers",
|
||||
"account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} sujets réservés",
|
||||
"account_upgrade_dialog_tier_features_messages_other": "{{messages}} messages journaliers",
|
||||
"account_upgrade_dialog_tier_features_emails_other": "{{emails}} emails journaliers",
|
||||
"account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} par fichier",
|
||||
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} stockage total",
|
||||
"account_upgrade_dialog_tier_selected_label": "Sélectionné",
|
||||
|
||||
1
web/public/static/langs/gl.json
Normal file
1
web/public/static/langs/gl.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -258,9 +258,9 @@
|
||||
"account_upgrade_dialog_title": "Ubah peringkat akun",
|
||||
"account_upgrade_dialog_proration_info": "<strong>Prorasi</strong>: Saat melakukan upgrade antar paket berbayar, selisih harga akan <strong>langsung dibebankan ke</strong>. Saat menurunkan ke tingkat yang lebih rendah, saldo akan digunakan untuk membayar periode penagihan di masa mendatang.",
|
||||
"account_upgrade_dialog_reservations_warning_other": "Peringkat yang dipilih memperbolehkan lebih sedikit reservasi topik daripada peringkat Anda saat ini. Sebelum mengubah peringkat Anda, <strong>silakan menghapus setidaknya {{count}} reservasi</strong>. Anda dapat menghapus reservasi di <Link>Pengaturan</Link>.",
|
||||
"account_upgrade_dialog_tier_features_reservations": "{{reservations}} topik yang telah direservasi",
|
||||
"account_upgrade_dialog_tier_features_messages": "{{messages}} pesan harian",
|
||||
"account_upgrade_dialog_tier_features_emails": "{{emails}} surel harian",
|
||||
"account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} topik yang telah direservasi",
|
||||
"account_upgrade_dialog_tier_features_messages_other": "{{messages}} pesan harian",
|
||||
"account_upgrade_dialog_tier_features_emails_other": "{{emails}} surel harian",
|
||||
"account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} per berkas",
|
||||
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} jumlah penyimpanan",
|
||||
"account_upgrade_dialog_tier_selected_label": "Dipilih",
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -187,5 +187,74 @@
|
||||
"prefs_notifications_delete_after_one_week": "Dopo una settimana",
|
||||
"prefs_notifications_delete_after_one_month": "Dopo un mese",
|
||||
"prefs_notifications_delete_after_three_hours_description": "Le notifiche vengono eliminate automaticamente dopo tre ore",
|
||||
"error_boundary_unsupported_indexeddb_description": "L'app web ntfy ha bisogno di IndexedDB per funzionare e il tuo browser non supporta IndexedDB in modalità di navigazione privata.<br/><br/>Anche se questo è un peccato, non ha molto senso usare il web ntfy app in modalità di navigazione privata comunque, perché tutto è archiviato nella memoria del browser. Puoi leggere di più a riguardo <githubLink>in questo numero di GitHub</githubLink> o parlarci su <discordLink>Discord</discordLink> o <matrixLink>Matrix</matrixLink>."
|
||||
"error_boundary_unsupported_indexeddb_description": "L'app web ntfy ha bisogno di IndexedDB per funzionare e il tuo browser non supporta IndexedDB in modalità di navigazione privata.<br/><br/>Anche se questo è un peccato, non ha molto senso usare il web ntfy app in modalità di navigazione privata comunque, perché tutto è archiviato nella memoria del browser. Puoi leggere di più a riguardo <githubLink>in questo numero di GitHub</githubLink> o parlarci su <discordLink>Discord</discordLink> o <matrixLink>Matrix</matrixLink>.",
|
||||
"nav_upgrade_banner_label": "Passa alla versione Pro di ntfy",
|
||||
"alert_not_supported_context_description": "Le Notificche sono supportate solo tramite HTTPS. Questa è una limitazione delle <mdnLink>Notifications API</mdnLink>.",
|
||||
"account_basics_password_dialog_new_password_label": "Nuova password",
|
||||
"action_bar_profile_logout": "Esci",
|
||||
"account_basics_tier_interval_monthly": "mensile",
|
||||
"account_basics_tier_interval_yearly": "annuale",
|
||||
"account_basics_tier_upgrade_button": "Passa alla versione Pro",
|
||||
"account_basics_tier_change_button": "Cambia",
|
||||
"account_basics_tier_paid_until": "Abbonamento pagato fino a {{data}}, e si rinnoverà automaticamente",
|
||||
"account_basics_tier_payment_overdue": "Il pagamento è scaduto. La preghiamo di aggiornare il suo metodo di pagamento, altrimenti il suo account verrà presto declassato.",
|
||||
"account_basics_tier_canceled_subscription": "L'abbonamento è stato annullato e sarà declassato ad account gratuito a partire dalla {{data}}.",
|
||||
"account_basics_tier_manage_billing_button": "Gestire la fatturazione",
|
||||
"account_usage_messages_title": "Messaggi pubblicati",
|
||||
"account_usage_reservations_title": "Argomenti riservati",
|
||||
"account_usage_reservations_none": "Non ci sono argomenti riservati per questo account",
|
||||
"signup_form_toggle_password_visibility": "Imposta la visibilità della password",
|
||||
"signup_already_have_account": "Hai già un account? Accedi!",
|
||||
"signup_disabled": "Registrazione disabilitata",
|
||||
"signup_title": "Crea un account ntfy",
|
||||
"signup_form_username": "Nome utente",
|
||||
"signup_form_password": "Password",
|
||||
"signup_form_confirm_password": "Conferma password",
|
||||
"signup_form_button_submit": "Registrazione",
|
||||
"signup_error_username_taken": "Il nome utente {{username}} è già utilizzato",
|
||||
"signup_error_creation_limit_reached": "Il limite per la creazione di account è stato raggiunto",
|
||||
"login_title": "Accedi al tuo account ntfy",
|
||||
"login_form_button_submit": "Accedi",
|
||||
"login_link_signup": "Registrati",
|
||||
"login_disabled": "L'accesso è disabilitato",
|
||||
"action_bar_account": "Account",
|
||||
"action_bar_change_display_name": "Cambia il nome da visualizzare",
|
||||
"action_bar_reservation_limit_reached": "Limite raggiunto",
|
||||
"action_bar_profile_title": "Profilo",
|
||||
"action_bar_profile_settings": "Impostazioni",
|
||||
"action_bar_reservation_add": "Riserva un argomento",
|
||||
"action_bar_reservation_edit": "Modifica l'argomento riservato",
|
||||
"action_bar_reservation_delete": "Rimuovi l'argomento riservato",
|
||||
"action_bar_sign_in": "Accedi",
|
||||
"action_bar_sign_up": "Registrati",
|
||||
"nav_button_account": "Account",
|
||||
"nav_upgrade_banner_description": "Riserva argomenti, più messaggi ed e-mail e allegati più grandi",
|
||||
"display_name_dialog_description": "Imposta un nome alternativo per un argomento che viene visualizzato nell'elenco delle sottoscrizioni. Questo aiuta a identificare più facilmente gli argomenti con nomi complicati.",
|
||||
"display_name_dialog_title": "Cambia il nome visualizzato",
|
||||
"display_name_dialog_placeholder": "Nome visualizzato",
|
||||
"reserve_dialog_checkbox_label": "Riserva un argomento e configura l'accesso",
|
||||
"subscribe_dialog_subscribe_button_generate_topic_name": "Genera un nome",
|
||||
"subscribe_dialog_error_topic_already_reserved": "Argomento già in uso",
|
||||
"account_basics_title": "Account",
|
||||
"account_basics_username_title": "Nome utente",
|
||||
"account_basics_username_admin_tooltip": "Sei Amministratore",
|
||||
"account_basics_password_title": "Password",
|
||||
"account_basics_password_description": "Cambia la password del tuo account",
|
||||
"account_basics_password_dialog_title": "Cambia la password",
|
||||
"account_basics_password_dialog_current_password_label": "Password attuale",
|
||||
"account_basics_password_dialog_confirm_password_label": "Conferma la password",
|
||||
"account_basics_password_dialog_button_submit": "Cambia la password",
|
||||
"account_basics_password_dialog_current_password_incorrect": "Password errata",
|
||||
"account_usage_title": "Utilizzo",
|
||||
"account_usage_of_limit": "di {{limit}}",
|
||||
"account_usage_unlimited": "Illimitato",
|
||||
"account_usage_limits_reset_daily": "I limiti di utilizzo vengono azzerati ogni giorno a mezzanotte (orario UTC)",
|
||||
"account_basics_tier_title": "Tipo di account",
|
||||
"account_basics_tier_description": "Permessi del tuo account",
|
||||
"account_basics_tier_admin": "Amministratore",
|
||||
"account_basics_tier_admin_suffix_with_tier": "(con livello {{tier}})",
|
||||
"account_basics_tier_admin_suffix_no_tier": "(nessun livello)",
|
||||
"account_basics_tier_basic": "Base",
|
||||
"account_basics_tier_free": "Gratuito",
|
||||
"account_usage_emails_title": "Email inviate"
|
||||
}
|
||||
|
||||
@@ -241,9 +241,9 @@
|
||||
"account_upgrade_dialog_title": "アカウントティアを変更",
|
||||
"account_upgrade_dialog_cancel_warning": "これにより<strong>サブスクリプションをキャンセルし</strong>{{date}}にアカウントをダウングレードします。同日、トピック予約およびサーバーにキャッシュされたメッセージは<strong>削除されます</strong>。",
|
||||
"account_upgrade_dialog_proration_info": "<strong>追記</strong>。有料プランをアップグレードする場合、価格差は<strong>即座に請求されます</strong>。ダウングレードする場合、差額は次の請求期間の支払いに利用されます。",
|
||||
"account_upgrade_dialog_tier_features_reservations": "予約のトピック{{reservations}}件",
|
||||
"account_upgrade_dialog_tier_features_emails": "日次メール{{emails}}件",
|
||||
"account_upgrade_dialog_tier_features_messages": "日次メッセージ{{messages}}件",
|
||||
"account_upgrade_dialog_tier_features_reservations_other": "予約のトピック{{reservations}}件",
|
||||
"account_upgrade_dialog_tier_features_emails_other": "日次メール{{emails}}件",
|
||||
"account_upgrade_dialog_tier_features_messages_other": "日次メッセージ{{messages}}件",
|
||||
"account_upgrade_dialog_tier_selected_label": "選択",
|
||||
"account_upgrade_dialog_tier_current_label": "現在",
|
||||
"account_upgrade_dialog_button_cancel": "キャンセル",
|
||||
@@ -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}} 件"
|
||||
}
|
||||
|
||||
@@ -308,5 +308,14 @@
|
||||
"account_upgrade_dialog_button_pay_now": "Zapłać i aktywuj subskrypcję",
|
||||
"account_tokens_dialog_button_cancel": "Anuluj",
|
||||
"account_tokens_dialog_expires_label": "Token dostępowy wygasa po",
|
||||
"account_tokens_dialog_expires_unchanged": "Pozostaw termin ważności bez zmian"
|
||||
"account_tokens_dialog_expires_unchanged": "Pozostaw termin ważności bez zmian",
|
||||
"account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} rezerwacja tematu",
|
||||
"account_upgrade_dialog_tier_features_reservations_few": "{{reservations}} rezerwacje tematów",
|
||||
"account_upgrade_dialog_tier_features_reservations_many": "{{reservations}} rezerwacji tematów",
|
||||
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} mail dziennie",
|
||||
"account_upgrade_dialog_tier_features_emails_few": "{{emails}} maile dziennie",
|
||||
"account_upgrade_dialog_tier_features_emails_many": "{{emails}} maili dziennie",
|
||||
"account_upgrade_dialog_tier_features_messages_one": "{{messages}} wiadomość dziennie",
|
||||
"account_upgrade_dialog_tier_features_messages_few": "{{messages}} wiadomości dziennie",
|
||||
"account_upgrade_dialog_tier_features_messages_many": "{{messages}} wiadomości dziennie"
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"notifications_attachment_copy_url_title": "Copiar URL do anexo para a área de transferência",
|
||||
"notifications_attachment_copy_url_button": "Copiar URL",
|
||||
"notifications_attachment_open_title": "Ir para {{url}}",
|
||||
"notifications_attachment_link_expired": "a ligação de transferência expirou",
|
||||
"notifications_attachment_link_expired": "a ligação de descarga expirou",
|
||||
"notifications_attachment_open_button": "Abrir anexo",
|
||||
"notifications_attachment_link_expires": "a ligação expira em {{date}}",
|
||||
"notifications_attachment_file_image": "ficheiro de imagem",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"publish_dialog_priority_min": "Наименьший приоритет",
|
||||
"publish_dialog_priority_min": "Минимальный приоритет",
|
||||
"action_bar_settings": "Настройки",
|
||||
"action_bar_send_test_notification": "Отправить тестовое уведомление",
|
||||
"action_bar_clear_notifications": "Удалить все уведомления",
|
||||
@@ -24,7 +24,7 @@
|
||||
"publish_dialog_priority_low": "Низкий приоритет",
|
||||
"publish_dialog_priority_default": "Стандартный приоритет",
|
||||
"publish_dialog_priority_high": "Высокий приоритет",
|
||||
"publish_dialog_priority_max": "Наивысший приоритет",
|
||||
"publish_dialog_priority_max": "Максимальный приоритет",
|
||||
"publish_dialog_base_url_label": "URL-адрес сервиса",
|
||||
"publish_dialog_base_url_placeholder": "URL-адрес сервиса, например https://example.com",
|
||||
"publish_dialog_topic_label": "Название темы",
|
||||
@@ -106,13 +106,13 @@
|
||||
"prefs_notifications_sound_title": "Звук уведомления",
|
||||
"prefs_notifications_sound_description_none": "Уведомления не воспроизводят никаких звуков при получении",
|
||||
"prefs_notifications_sound_no_sound": "Без звука",
|
||||
"prefs_notifications_min_priority_title": "Наименьший приоритет",
|
||||
"prefs_notifications_min_priority_description_any": "Показать все уведомления, независимо от приоритета",
|
||||
"prefs_notifications_min_priority_title": "Минимальный приоритет",
|
||||
"prefs_notifications_min_priority_description_any": "Показывать все уведомления, независимо от приоритета",
|
||||
"prefs_notifications_min_priority_description_x_or_higher": "Показывать уведомления, если приоритет {{number}} ({{name}}) или выше",
|
||||
"prefs_notifications_min_priority_description_max": "Показывать уведомления, если приоритет равен 5 (наивысший)",
|
||||
"prefs_notifications_min_priority_description_max": "Показывать уведомления, если приоритет равен 5 (максимальный)",
|
||||
"prefs_notifications_min_priority_any": "Любой приоритет",
|
||||
"prefs_notifications_min_priority_low_and_higher": "Низкий приоритет и выше",
|
||||
"prefs_notifications_min_priority_max_only": "Только наивысший приоритет",
|
||||
"prefs_notifications_min_priority_max_only": "Только максимальный приоритет",
|
||||
"prefs_notifications_delete_after_title": "Удалить уведомления",
|
||||
"prefs_notifications_delete_after_never": "Никогда",
|
||||
"prefs_notifications_delete_after_three_hours": "Через три часа",
|
||||
@@ -140,11 +140,11 @@
|
||||
"common_save": "Сохранить",
|
||||
"prefs_appearance_title": "Внешний вид",
|
||||
"prefs_appearance_language_title": "Язык",
|
||||
"priority_min": "наименьший",
|
||||
"priority_min": "минимальный",
|
||||
"priority_low": "низкий",
|
||||
"priority_default": "стандартный",
|
||||
"priority_high": "высокий",
|
||||
"priority_max": "наивысший",
|
||||
"priority_max": "максимальный",
|
||||
"error_boundary_title": "О нет, ntfy сломался",
|
||||
"error_boundary_button_copy_stack_trace": "Скопировать трассировку стека",
|
||||
"error_boundary_stack_trace": "Трассировка стека",
|
||||
@@ -192,7 +192,7 @@
|
||||
"account_tokens_dialog_button_create": "Создать токен",
|
||||
"account_tokens_delete_dialog_submit_button": "Безвозвратно удалить токен",
|
||||
"account_upgrade_dialog_reservations_warning_other": "Выбранная подписка разрешает меньше зарезервированных тем, чем есть у Вас на данный момент. Перед сменой подписки, <strong>пожалуйста удалите хотя бы {{count}} зарезервированных тем</strong>. Вы можете это сделать в <Link>Настройках</Link>.",
|
||||
"account_upgrade_dialog_tier_features_messages": "{{messages}} сообщений в день",
|
||||
"account_upgrade_dialog_tier_features_messages_other": "{{messages}} сообщений в день",
|
||||
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} суммарный объем",
|
||||
"account_upgrade_dialog_tier_selected_label": "Выбранная",
|
||||
"account_tokens_table_current_session": "Текущий сеанс браузера",
|
||||
@@ -201,8 +201,8 @@
|
||||
"account_tokens_dialog_expires_x_hours": "Токен истекает через {{hours}} часов",
|
||||
"account_tokens_dialog_expires_never": "Токен никогда не истекает",
|
||||
"prefs_notifications_sound_play": "Воспроизводить выбранный звук",
|
||||
"account_upgrade_dialog_tier_features_reservations": "{{reservations}} зарезервированных тем",
|
||||
"account_upgrade_dialog_tier_features_emails": "{{emails}} эл. сообщений в день",
|
||||
"account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} зарезервированных тем",
|
||||
"account_upgrade_dialog_tier_features_emails_other": "{{emails}} эл. сообщений в день",
|
||||
"account_basics_tier_free": "Бесплатный",
|
||||
"account_tokens_dialog_title_create": "Создать токен доступа",
|
||||
"account_tokens_dialog_title_delete": "Удалить токен доступа",
|
||||
@@ -303,7 +303,7 @@
|
||||
"account_usage_reservations_title": "Зарезервированные темы",
|
||||
"account_usage_reservations_none": "Нет зарезервированных тем",
|
||||
"account_usage_attachment_storage_title": "Хранение вложений",
|
||||
"account_usage_attachment_storage_description": "{{filesize}} за файл, удаляются после {{expiry}}",
|
||||
"account_usage_attachment_storage_description": "{{filesize}} за файл, удаляются спустя {{expiry}}",
|
||||
"account_usage_cannot_create_portal_session": "Невозможно открыть портал оплаты",
|
||||
"account_delete_title": "Удалить учетную запись",
|
||||
"account_delete_description": "Безвозвратно удалить Вашу учетную запись",
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -253,9 +253,9 @@
|
||||
"account_upgrade_dialog_title": "Hesap seviyesini değiştir",
|
||||
"account_upgrade_dialog_proration_info": "<strong>Fiyatlandırma</strong>: Ücretli planlar arasında yükseltme yaparken, fiyat farkı <strong>hemen tahsil edilecektir</strong>. Daha düşük bir seviyeye inildiğinde, bakiye gelecek faturalandırma dönemleri için ödeme yapmak üzere kullanılacaktır.",
|
||||
"account_upgrade_dialog_reservations_warning_other": "Seçilen seviye, geçerli seviyenizden daha az konu ayırtmaya izin veriyor. Seviyenizi değiştirmeden önce <strong>lütfen en az {{count}} ayırtmayı silin</strong>. Ayırtmaları <Link>Ayarlar</Link> sayfasından kaldırabilirsiniz.",
|
||||
"account_upgrade_dialog_tier_features_reservations": "{{reservations}} konu ayırtıldı",
|
||||
"account_upgrade_dialog_tier_features_messages": "{{messages}} günlük mesaj",
|
||||
"account_upgrade_dialog_tier_features_emails": "{{emails}} günlük e-posta",
|
||||
"account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} konu ayırtıldı",
|
||||
"account_upgrade_dialog_tier_features_messages_other": "{{messages}} günlük mesaj",
|
||||
"account_upgrade_dialog_tier_features_emails_other": "{{emails}} günlük e-posta",
|
||||
"account_upgrade_dialog_tier_features_attachment_file_size": "dosya başına {{filesize}}",
|
||||
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} toplam depolama",
|
||||
"account_upgrade_dialog_tier_selected_label": "Seçilen",
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -293,12 +293,12 @@
|
||||
"account_delete_dialog_billing_warning": "删除您的帐户也会立即取消您的计费订阅。您将无法再访问计费仪表板。",
|
||||
"account_upgrade_dialog_title": "更改帐户等级",
|
||||
"account_upgrade_dialog_cancel_warning": "这将<strong>取消您的订阅</strong>,并在 {{date}} 降级您的帐户。在那一天,主题保留以及缓存在服务器上的消息<strong>将被删除</strong>。",
|
||||
"account_upgrade_dialog_proration_info": "<strong>按比例分配</strong>:在付费计划之间切换时,差价将在下一次计费时收取或退还。在下一个计费周期结束之前,您不会收到另一张收据。",
|
||||
"account_upgrade_dialog_proration_info": "<strong>按比例分配</strong>:在付费计划之间升级时,差价将被<strong>立刻收取</strong>。在降级到较低级别时,余额将被用于支付未来的账单周期。",
|
||||
"account_upgrade_dialog_reservations_warning_one": "所选等级允许的保留主题少于当前等级。在更改您的等级之前,<strong>请至少删除 1 项保留</strong>。您可以在<Link>设置</Link>中删除保留。",
|
||||
"account_upgrade_dialog_reservations_warning_other": "所选等级允许的保留主题少于当前等级。在更改您的等级之前,<strong>请至少删除 {{count}} 项保留</strong>。您可以在<Link>设置</Link>中删除保留。",
|
||||
"account_upgrade_dialog_tier_features_reservations": "{{reservations}} 条保留主题",
|
||||
"account_upgrade_dialog_tier_features_messages": "{{messages}} 条每日消息",
|
||||
"account_upgrade_dialog_tier_features_emails": "{{emails}} 条每日邮件",
|
||||
"account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} 条保留主题",
|
||||
"account_upgrade_dialog_tier_features_messages_other": "{{messages}} 条每日消息",
|
||||
"account_upgrade_dialog_tier_features_emails_other": "{{emails}} 条每日邮件",
|
||||
"account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} 每个文件",
|
||||
"signup_form_confirm_password": "确认密码",
|
||||
"signup_form_button_submit": "注册",
|
||||
@@ -340,5 +340,17 @@
|
||||
"account_tokens_table_last_origin_tooltip": "于IP地址 {{ip}},点击查找",
|
||||
"account_tokens_dialog_label": "标签,例如:Radarr 通知",
|
||||
"account_tokens_dialog_button_create": "创建令牌",
|
||||
"account_tokens_dialog_button_update": "更新令牌"
|
||||
"account_tokens_dialog_button_update": "更新令牌",
|
||||
"account_basics_tier_interval_monthly": "每月",
|
||||
"account_basics_tier_interval_yearly": "每年",
|
||||
"account_upgrade_dialog_interval_monthly": "每月",
|
||||
"account_upgrade_dialog_interval_yearly": "每年",
|
||||
"account_upgrade_dialog_interval_yearly_discount_save": "节省 {{discount}}%",
|
||||
"account_upgrade_dialog_interval_yearly_discount_save_up_to": "节省高达 {{discount}}%",
|
||||
"account_upgrade_dialog_tier_features_no_reservations": "无保留主题",
|
||||
"account_upgrade_dialog_tier_price_per_month": "月",
|
||||
"account_upgrade_dialog_tier_price_billed_monthly": "{{price}} 每年。按月计费。",
|
||||
"account_upgrade_dialog_tier_price_billed_yearly": "{{价格}} 按年计费。节省 {{save}}。",
|
||||
"account_upgrade_dialog_billing_contact_email": "有关账单问题,请直接<Link>联系我们 </Link>。",
|
||||
"account_upgrade_dialog_billing_contact_website": "有关账单问题,请参考我们的<Link>网站 </Link>。"
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -297,10 +297,10 @@ const TierCard = (props) => {
|
||||
{monthlyPrice > 0 && <>/ {t("account_upgrade_dialog_tier_price_per_month")}</>}
|
||||
</div>
|
||||
<List dense>
|
||||
{tier.limits.reservations > 0 && <Feature>{t("account_upgrade_dialog_tier_features_reservations", { reservations: tier.limits.reservations })}</Feature>}
|
||||
{tier.limits.reservations > 0 && <Feature>{t("account_upgrade_dialog_tier_features_reservations", { reservations: tier.limits.reservations, count: tier.limits.reservations })}</Feature>}
|
||||
{tier.limits.reservations === 0 && <NoFeature>{t("account_upgrade_dialog_tier_features_no_reservations")}</NoFeature>}
|
||||
<Feature>{t("account_upgrade_dialog_tier_features_messages", { messages: formatNumber(tier.limits.messages) })}</Feature>
|
||||
<Feature>{t("account_upgrade_dialog_tier_features_emails", { emails: formatNumber(tier.limits.emails) })}</Feature>
|
||||
<Feature>{t("account_upgrade_dialog_tier_features_messages", { messages: formatNumber(tier.limits.messages), count: tier.limits.messages })}</Feature>
|
||||
<Feature>{t("account_upgrade_dialog_tier_features_emails", { emails: formatNumber(tier.limits.emails), count: tier.limits.emails })}</Feature>
|
||||
<Feature>{t("account_upgrade_dialog_tier_features_attachment_file_size", { filesize: formatBytes(tier.limits.attachment_file_size, 0) })}</Feature>
|
||||
<Feature>{t("account_upgrade_dialog_tier_features_attachment_total_size", { totalsize: formatBytes(tier.limits.attachment_total_size, 0) })}</Feature>
|
||||
</List>
|
||||
|
||||
Reference in New Issue
Block a user