Compare commits

..

78 Commits

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

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

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

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

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

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

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

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

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

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

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

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2023-04-01 13:39:48 +02:00
josé m
286ae43d1a Added translation using Weblate (Galician) 2023-03-31 11:51:46 +02:00
binwiederhier
a75fb08ef1 Tidy 2023-03-30 21:06:22 -04:00
binwiederhier
58a0c2a6c6 Bump 2023-03-30 21:04:03 -04:00
binwiederhier
d050956007 Added Ansible role 2023-03-30 14:56:14 -04:00
binwiederhier
bdae48afba Disable iOS polling entirely 2023-03-30 14:48:52 -04:00
binwiederhier
cb5c4c5483 Thank you @R-Gld for your sponsorship 2023-03-30 12:56:47 -04:00
binwiederhier
e91f07a081 I still don't understand 2023-03-29 21:20:43 -04:00
binwiederhier
7d96be6fb3 Deps 2023-03-29 21:18:17 -04:00
binwiederhier
46c798c71a Just comment the test for now 2023-03-29 15:03:41 -04:00
binwiederhier
037a51a9d0 Bump 2023-03-29 14:56:16 -04:00
binwiederhier
4596e4bcab Blog posts, fix lint 2023-03-29 00:23:08 -04:00
Philipp C. Heckel
9b30ada880 Merge pull request #688 from Raistlingru/patch-1
add hostux server
2023-03-29 00:13:34 -04:00
Raistlingru
96d711e19e add hostux server 2023-03-29 06:12:19 +02:00
binwiederhier
5af5565fb1 Thank you @johman10 for your donation 2023-03-28 14:42:15 -04:00
binwiederhier
29c9551548 Profiling support 2023-03-28 14:41:16 -04:00
binwiederhier
23c5d4e345 Adjust battery FAQ 2023-03-26 17:01:08 -04:00
binwiederhier
ff5bf4acd0 Thank you @samliebow for your sponsorship 2023-03-25 14:11:58 -04:00
binwiederhier
34c42c55f6 Changelog 2023-03-25 14:11:23 -04:00
binwiederhier
07e5b28868 Fix other languages 2023-03-25 14:09:51 -04:00
binwiederhier
06a0654a5a Merge branch 'main' into i18n-plural-forms 2023-03-25 14:03:09 -04:00
binwiederhier
8cc23117fe Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web into i18n-plural-forms 2023-03-25 14:02:50 -04:00
Nick
f8c4f20a8f Translated using Weblate (Russian)
Currently translated at 100.0% (354 of 354 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ru/
2023-03-24 07:37:58 +01:00
109247019824
8053e992e4 Translated using Weblate (Bulgarian)
Currently translated at 79.0% (280 of 354 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/bg/
2023-03-24 07:37:58 +01:00
binwiederhier
9db96140e2 Bump 2023-03-22 16:26:00 -04:00
binwiederhier
502d0a0abd Fix delayed message sending from authenticated users, closes #679 2023-03-22 15:30:20 -04:00
Bartosz Moczulski
80b0a94f7e i18n-pl: Provide translations for plural forms of reservations. emails, messages
Following up on the previous commit this one introduces Polish
translations for plural forms of reservations. emails, messages in
upgrade modal.
2023-03-21 10:14:39 +01:00
Bartosz Moczulski
338cab1660 i18n: Introduce plural forms for reservations, emails, messages
In many languages there is more than one plural form of nouns and rules
for choosing the correct one are often far more complex than in English.
Luckily both react-i18next and Weblate provide built-in support for
translating and selecting plural forms in accordance with grammatical
rules of any given language.

In order to enable plural forms `{count: n}` option is added to relevant
`t()` calls. In translations files "_one" and "_other" suffix is added
to English labels such that Weblate can detect which entries represent a
set of plural forms and show appropriate language-specific form on the
translation page. E.g. in Polish there are 2 plural forms and hence 3
resulting suffixes: "_one", "_few", "_many".

Note on transition period: in the absence of expected suffixed variants
react-i18next will use non-suffixed one (if present) so existing
translations will continue to work just fine even if they happen to be
grammatically imperfect. Translators can provide proper plural forms in
once this change is merged and Weblate will then replace non-suffixed
labels with the suffixed ones.
2023-03-21 10:03:36 +01:00
binwiederhier
b8836d674a Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web 2023-03-20 21:55:35 -04:00
binwiederhier
c6a96d19e2 Troubleshooting doc update 2023-03-20 21:50:54 -04:00
binwiederhier
bcb24aecd3 Troubleshooting docs page 2023-03-20 15:34:10 -04:00
ssantos
d72ae47d1f Translated using Weblate (Portuguese)
Currently translated at 61.0% (216 of 354 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/pt/
2023-03-20 10:37:29 +01:00
Poesty Li
a5d2fc172b Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (354 of 354 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/zh_Hans/
2023-03-20 10:37:29 +01:00
Emanuele Cisbani
bbab81a1a2 Translated using Weblate (Italian)
Currently translated at 72.8% (258 of 354 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/it/
2023-03-20 10:37:28 +01:00
109247019824
78a1ca81e3 Translated using Weblate (Bulgarian)
Currently translated at 78.5% (278 of 354 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/bg/
2023-03-20 10:37:28 +01:00
binwiederhier
f090d1313e Merge branch 'main' of github.com:binwiederhier/ntfy 2023-03-19 15:46:56 -04:00
binwiederhier
afa4efa140 Add Grafana dashboard to docs 2023-03-19 15:46:37 -04:00
Philipp C. Heckel
d2b88005f0 Merge pull request #674 from caseodilla/main
fix misc typos
2023-03-19 10:03:53 -04:00
caseodilla
9eb1f6a186 fix typo 2023-03-19 09:59:52 -04:00
caseodilla
2d8d5b3b95 Update README.md
fix contributor logo
2023-03-19 09:45:18 -04:00
58 changed files with 6558 additions and 2782 deletions

View File

@@ -26,7 +26,7 @@ jobs:
~/go/bin ~/go/bin
~/.npm ~/.npm
web/node_modules 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- restore-keys: ${{ runner.os }}-ntfy-
- -
name: Install dependencies name: Install dependencies

View File

@@ -29,7 +29,7 @@ jobs:
~/go/bin ~/go/bin
~/.npm ~/.npm
web/node_modules 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- restore-keys: ${{ runner.os }}-ntfy-
- -
name: Docker login name: Docker login

View File

@@ -26,7 +26,7 @@ jobs:
~/go/bin ~/go/bin
~/.npm ~/.npm
web/node_modules 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- restore-keys: ${{ runner.os }}-ntfy-
- -
name: Install dependencies name: Install dependencies

View File

@@ -141,25 +141,25 @@ web-deps-update:
# Main server/client build # Main server/client build
cli: cli-deps cli: cli-deps
goreleaser build --snapshot --rm-dist goreleaser build --snapshot --clean
cli-linux-amd64: cli-deps-static-sites 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 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 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 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 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 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 cli-linux-server: cli-deps-static-sites
# This is a target to build the CLI (including the server) manually. # This is a target to build the CLI (including the server) manually.

View File

@@ -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/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/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/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, 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: and [DigitalOcean](https://m.do.co/c/442b929528db) (*referral link*) for supporting the project:

View File

@@ -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.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.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: "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{ var cmdServe = &cli.Command{
@@ -167,6 +168,7 @@ func execServe(c *cli.Context) error {
billingContact := c.String("billing-contact") billingContact := c.String("billing-contact")
metricsListenHTTP := c.String("metrics-listen-http") metricsListenHTTP := c.String("metrics-listen-http")
enableMetrics := c.Bool("enable-metrics") || metricsListenHTTP != "" enableMetrics := c.Bool("enable-metrics") || metricsListenHTTP != ""
profileListenHTTP := c.String("profile-listen-http")
// Check values // Check values
if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) { if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) {
@@ -321,6 +323,7 @@ func execServe(c *cli.Context) error {
conf.EnableReservations = enableReservations conf.EnableReservations = enableReservations
conf.EnableMetrics = enableMetrics conf.EnableMetrics = enableMetrics
conf.MetricsListenHTTP = metricsListenHTTP conf.MetricsListenHTTP = metricsListenHTTP
conf.ProfileListenHTTP = profileListenHTTP
conf.Version = c.App.Version conf.Version = c.App.Version
// Set up hot-reloading of config // Set up hot-reloading of config

View File

@@ -119,8 +119,7 @@ func execSubscribe(c *cli.Context) error {
} }
if token != "" { if token != "" {
options = append(options, client.WithBearerAuth(token)) options = append(options, client.WithBearerAuth(token))
} } else if user != "" {
if user != "" {
var pass string var pass string
parts := strings.SplitN(user, ":", 2) parts := strings.SplitN(user, ":", 2)
if len(parts) == 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)) fmt.Fprintf(c.App.ErrWriter, "\r%s\r", strings.Repeat(" ", 20))
} }
options = append(options, client.WithBasicAuth(user, pass)) 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 { if scheduled {
options = append(options, client.WithScheduled()) options = append(options, client.WithScheduled())

View File

@@ -310,3 +310,52 @@ func TestCLI_Subscribe_Token_And_UserPass(t *testing.T) {
require.Error(t, err) require.Error(t, err)
require.Equal(t, "cannot set both --user and --token", err.Error()) 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()))
}

View File

@@ -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 - `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" enables metrics as well, e.g. "10.0.1.1:9090" or ":9090"
=== "Using default port" === "server.yml (Using default port)"
```yaml ```yaml
enable-metrics: true enable-metrics: true
``` ```
=== "Using dedicated IP/port" === "server.yml (Using dedicated IP/port)"
```yaml ```yaml
metrics-listen-http: "10.0.1.1:9090" 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 ## Logging & debugging
By default, ntfy logs to the console (stderr), with an `info` log level, and in a human-readable text format. By default, ntfy logs to the console (stderr), with an `info` log level, and in a human-readable text format.

File diff suppressed because it is too large Load Diff

View File

@@ -43,9 +43,9 @@ of the app and [self-host your own ntfy server](install.md).
## How much battery does the Android app use? ## 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, 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, 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 or you use *instant delivery* (Android only), or install from F-droid ([which does not support FCM](https://f-droid.org/docs/Inclusion_Policy/)),
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 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).
decent now. 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? ## 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 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

View File

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

View File

@@ -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.adminforge.de](https://ntfy.adminforge.de/) | 🇩🇪 Germany |
| [ntfy.envs.net](https://ntfy.envs.net) | 🇩🇪 Germany | | [ntfy.envs.net](https://ntfy.envs.net) | 🇩🇪 Germany |
| [ntfy.mzte.de](https://ntfy.mzte.de/) | 🇩🇪 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 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**. 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 - [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 - [FlexGet](https://flexget.com/Plugins/Notifiers/ntfysh) ⭐ - Multipurpose automation tool for all of your media
- [Shoutrrr](https://containrrr.dev/shoutrrr/v0.7/services/ntfy/) ⭐ - Notification library for gophers and their furry friends. - [Shoutrrr](https://containrrr.dev/shoutrrr/v0.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 - [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 - [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 - [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](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-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](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 ## 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) - [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) - [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) - [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) - [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) - [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) - [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 - [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 - [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) - [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 ## 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 - [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 - [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 - [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 - [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 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 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 - [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 - [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 - [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 - [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 - [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 - [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 - [Pointer | Issue #367](https://www.pointer.io/archives/a9495a2a6f/) - pointer.io - 11/2022

View File

@@ -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. 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 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. Please send experienced iOS developers my way to help me figure this out.

View File

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

View File

@@ -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) Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases)
and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases). and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).
### ntfy server v2.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 ## ntfy server v2.2.0
Released March 17, 2023 Released March 17, 2023
@@ -1135,3 +1163,24 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
**Additional languages:** **Additional languages:**
* Swedish (thanks to [@hellbown](https://hosted.weblate.org/user/hellbown/)) * Swedish (thanks to [@hellbown](https://hosted.weblate.org/user/hellbown/))
### ntfy server v2.4.0 (UNRELEASED)
**Features:**
* [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) can now be installed via Homebrew (thanks to [@Moulick](https://github.com/Moulick))
* Added `v1/stats` endpoint to expose messages stats (no ticket)
* Support [RFC 2047](https://datatracker.ietf.org/doc/html/rfc2047#section-2) encoded headers (no ticket, honorable mention to [mqttwarn](https://github.com/jpmens/mqttwarn/pull/638) and [@amotl](https://github.com/amotl))
**Bug fixes + maintenance:**
* Hide country flags on Windows ([#606](https://github.com/binwiederhier/ntfy/issues/606), thanks to [@cmeis](https://github.com/cmeis) for reporting, and to [@pokej6](https://github.com/pokej6) for fixing it)
* `ntfy sub` now uses default auth credentials as defined in `client.yml` ([#698](https://github.com/binwiederhier/ntfy/issues/698), thanks to [@CrimsonFez](https://github.com/CrimsonFez) for reporting, and to [@wunter8](https://github.com/wunter8) for fixing it)
**Documentation:**
* Updated PowerShell examples ([#697](https://github.com/binwiederhier/ntfy/pull/697), thanks to [@Natfan](https://github.com/Natfan))
**Additional languages:**
* Swedish (thanks to [@hellbown](https://hosted.weblate.org/user/Shjosan/))

View File

@@ -71,7 +71,18 @@ figure video {
} }
.remove-md-box td { .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 */ /* Lightbox; thanks to https://yossiabramov.com/blog/vanilla-js-lightbox */

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

131
docs/troubleshooting.md Normal file
View 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>
![Recording logs on Android](static/img/android-screenshot-logs.jpg){ 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>
![Web app logs](static/img/web-logs.png)
<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.

File diff suppressed because it is too large Load Diff

35
go.mod
View File

@@ -4,7 +4,7 @@ go 1.18
require ( require (
cloud.google.com/go/firestore v1.9.0 // indirect 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/BurntSushi/toml v1.2.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/emersion/go-smtp v0.16.0 github.com/emersion/go-smtp v0.16.0
@@ -13,29 +13,29 @@ require (
github.com/mattn/go-sqlite3 v1.14.16 github.com/mattn/go-sqlite3 v1.14.16
github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8 github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8
github.com/stretchr/testify v1.8.1 github.com/stretchr/testify v1.8.1
github.com/urfave/cli/v2 v2.25.0 github.com/urfave/cli/v2 v2.25.1
golang.org/x/crypto v0.7.0 golang.org/x/crypto v0.8.0
golang.org/x/oauth2 v0.6.0 // indirect golang.org/x/oauth2 v0.7.0 // indirect
golang.org/x/sync v0.1.0 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 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 gopkg.in/yaml.v2 v2.4.0
) )
require github.com/pkg/errors v0.9.1 // indirect require github.com/pkg/errors v0.9.1 // indirect
require ( require (
firebase.google.com/go/v4 v4.10.0 firebase.google.com/go/v4 v4.11.0
github.com/prometheus/client_golang v1.14.0 github.com/prometheus/client_golang v1.15.0
github.com/stripe/stripe-go/v74 v74.12.0 github.com/stripe/stripe-go/v74 v74.15.0
) )
require ( require (
cloud.google.com/go v0.110.0 // indirect 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/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 cloud.google.com/go/longrunning v0.4.1 // indirect
github.com/AlekSi/pointer v1.2.0 // indirect github.com/AlekSi/pointer v1.2.0 // indirect
github.com/MicahParks/keyfunc v1.9.0 // indirect github.com/MicahParks/keyfunc v1.9.0 // indirect
@@ -47,6 +47,7 @@ require (
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.5.9 // 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/google/uuid v1.3.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
github.com/googleapis/gax-go/v2 v2.8.0 // 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/stretchr/objx v0.5.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
go.opencensus.io v0.24.0 // indirect go.opencensus.io v0.24.0 // indirect
golang.org/x/net v0.8.0 // indirect golang.org/x/net v0.9.0 // indirect
golang.org/x/sys v0.6.0 // indirect golang.org/x/sys v0.7.0 // indirect
golang.org/x/text v0.8.0 // indirect golang.org/x/text v0.9.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect
google.golang.org/appengine/v2 v2.0.2 // indirect google.golang.org/appengine/v2 v2.0.3 // indirect
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
google.golang.org/grpc v1.53.0 // indirect google.golang.org/grpc v1.54.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

100
go.sum
View File

@@ -1,20 +1,27 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.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 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys=
cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= 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.19.0 h1:+9zda3WGgW1ZSTlVppLCYFIr48Pa35q1uG2N1itbCEQ=
cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= 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 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/firestore v1.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA= 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/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 h1:+CmB+K0J/33d0zSQ9SlFWUeCCEn5XJA0ZMZ3pHE9u8k=
cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= 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 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM=
cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= 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.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM=
cloud.google.com/go/storage v1.30.0/go.mod h1:xAVretHSROm1BQX4IIsoVgJqw0LqOyX+I/O2GzRAzdE= 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 h1:dgK/8uwfJbzc5LZK/GyRRfIkZEDObN9q0kgEXsjlXN4=
firebase.google.com/go/v4 v4.10.0/go.mod h1:m0gLwPY9fxKggizzglgCNWOGnFnVPifLpqZzo5u3e/A= 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 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml 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/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 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= 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 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 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/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 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/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 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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= 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.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.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.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/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 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= 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.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 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 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.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.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.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.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/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.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-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.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 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.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 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 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 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 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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/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.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 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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/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 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 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 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 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/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 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= 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.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 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= 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/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= 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/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 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 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= 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.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 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 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.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.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.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 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.14.0 h1:hB1Ocu/m3BUZ+PrTePsPSv8TKcXTrleCL5Y5JfB8zCo=
github.com/stripe/stripe-go/v74 v74.12.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw= github.com/stripe/stripe-go/v74 v74.14.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw=
github.com/urfave/cli/v2 v2.25.0 h1:ykdZKuQey2zq0yin/l7JOm9Mh+pg72ngYMeB0ABn6q8= github.com/stripe/stripe-go/v74 v74.15.0 h1:P3ZYrY4CdZeV8Pc/205utqjur+5gcTef+9hgtj8P8IY=
github.com/urfave/cli/v2 v2.25.0/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= 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 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 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 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
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-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-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 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 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/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-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-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 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-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-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-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-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-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-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-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-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-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 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 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-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 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= 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-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-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-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-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 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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-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-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-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-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-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-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-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-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 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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-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.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 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 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.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.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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/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.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 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 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 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 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= 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-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-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-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-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 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= 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 h1:1xQPji6cO2E2vLiI+C/XiFAnsn1WV3mjaEwGLhi3grE=
google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= 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.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 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 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 h1:MSqyWy2shDLwG7chbwBJ5uMyw6SNqJzhJHNDwYB0Akk=
google.golang.org/appengine/v2 v2.0.2/go.mod h1:PkgRUWz4o1XOvbqtWTkBtCitEJ5Tp4HoVEdMMYQR/8E= 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-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-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-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-20230330200707-38013875ee22 h1:n3ThVoQnHbCbnkhZZ1fx3+3fBAisViSwrpbtLV7vydY=
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= 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.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 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.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= 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-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-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf 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= 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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c 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 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -93,6 +93,7 @@ nav:
- "Integrations + projects": integrations.md - "Integrations + projects": integrations.md
- "Release notes": releases.md - "Release notes": releases.md
- "Emojis 🥳 🎉": emojis.md - "Emojis 🥳 🎉": emojis.md
- "Troubleshooting": troubleshooting.md
- "Known issues": known-issues.md - "Known issues": known-issues.md
- "Deprecation notices": deprecations.md - "Deprecation notices": deprecations.md
- "Development": develop.md - "Development": develop.md

View File

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

View File

@@ -107,6 +107,7 @@ type Config struct {
SMTPServerAddrPrefix string SMTPServerAddrPrefix string
MetricsEnable bool MetricsEnable bool
MetricsListenHTTP string MetricsListenHTTP string
ProfileListenHTTP string
MessageLimit int MessageLimit int
MinDelay time.Duration MinDelay time.Duration
MaxDelay time.Duration MaxDelay time.Duration

View File

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

View File

@@ -19,6 +19,7 @@ import (
"io" "io"
"net" "net"
"net/http" "net/http"
"net/http/pprof"
"net/netip" "net/netip"
"net/url" "net/url"
"os" "os"
@@ -39,6 +40,7 @@ type Server struct {
httpServer *http.Server httpServer *http.Server
httpsServer *http.Server httpsServer *http.Server
httpMetricsServer *http.Server httpMetricsServer *http.Server
httpProfileServer *http.Server
unixListener net.Listener unixListener net.Listener
smtpServer *smtp.Server smtpServer *smtp.Server
smtpServerBackend *smtpBackend smtpServerBackend *smtpBackend
@@ -46,7 +48,8 @@ type Server struct {
topics map[string]*topic topics map[string]*topic
visitors map[string]*visitor // ip:<ip> or user:<user> visitors map[string]*visitor // ip:<ip> or user:<user>
firebaseClient *firebaseClient 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! userManager *user.Manager // Might be nil!
messageCache *messageCache // Database that stores the messages messageCache *messageCache // Database that stores the messages
fileCache *fileCache // File system based cache that stores attachments 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!) 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 metricsHandler http.Handler // Handles /metrics if enable-metrics set, and listen-metrics-http not set
closeChan chan bool closeChan chan bool
mu sync.Mutex mu sync.RWMutex
} }
// handleFunc extends the normal http.HandlerFunc to be able to easily return errors // handleFunc extends the normal http.HandlerFunc to be able to easily return errors
@@ -77,7 +80,8 @@ var (
matrixPushPath = "/_matrix/push/v1/notify" matrixPushPath = "/_matrix/push/v1/notify"
metricsPath = "/metrics" metricsPath = "/metrics"
apiHealthPath = "/v1/health" apiHealthPath = "/v1/health"
apiTiers = "/v1/tiers" apiStatsPath = "/v1/stats"
apiTiersPath = "/v1/tiers"
apiAccountPath = "/v1/account" apiAccountPath = "/v1/account"
apiAccountTokenPath = "/v1/account/token" apiAccountTokenPath = "/v1/account/token"
apiAccountPasswordPath = "/v1/account/password" apiAccountPasswordPath = "/v1/account/password"
@@ -114,9 +118,10 @@ const (
newMessageBody = "New message" // Used in poll requests as generic message newMessageBody = "New message" // Used in poll requests as generic message
defaultAttachmentMessage = "You received a file: %s" // Used if message body is empty, and there is an attachment defaultAttachmentMessage = "You received a file: %s" // Used if message body is empty, and there is an attachment
encodingBase64 = "base64" // Used mainly for binary UnifiedPush messages encodingBase64 = "base64" // Used mainly for binary UnifiedPush messages
jsonBodyBytesLimit = 16384 jsonBodyBytesLimit = 16384 // Max number of bytes for a JSON request body
unifiedPushTopicPrefix = "up" // Temporarily, we rate limit all "up*" topics based on the subscriber unifiedPushTopicPrefix = "up" // Temporarily, we rate limit all "up*" topics based on the subscriber
unifiedPushTopicLength = 14 unifiedPushTopicLength = 14 // Length of UnifiedPush topics, including the "up" part
messagesHistoryMax = 10 // Number of message count values to keep in memory
) )
// WebSocket constants // WebSocket constants
@@ -146,6 +151,10 @@ func New(conf *Config) (*Server, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
messages, err := messageCache.Stats()
if err != nil {
return nil, err
}
var fileCache *fileCache var fileCache *fileCache
if conf.AttachmentCacheDir != "" { if conf.AttachmentCacheDir != "" {
fileCache, err = newFileCache(conf.AttachmentCacheDir, conf.AttachmentTotalSizeLimit) fileCache, err = newFileCache(conf.AttachmentCacheDir, conf.AttachmentTotalSizeLimit)
@@ -175,15 +184,17 @@ func New(conf *Config) (*Server, error) {
firebaseClient = newFirebaseClient(sender, auther) firebaseClient = newFirebaseClient(sender, auther)
} }
s := &Server{ s := &Server{
config: conf, config: conf,
messageCache: messageCache, messageCache: messageCache,
fileCache: fileCache, fileCache: fileCache,
firebaseClient: firebaseClient, firebaseClient: firebaseClient,
smtpSender: mailer, smtpSender: mailer,
topics: topics, topics: topics,
userManager: userManager, userManager: userManager,
visitors: make(map[string]*visitor), messages: messages,
stripe: stripe, messagesHistory: []int64{messages},
visitors: make(map[string]*visitor),
stripe: stripe,
} }
s.priceCache = util.NewLookupCache(s.fetchStripePrices, conf.StripePriceCacheDuration) s.priceCache = util.NewLookupCache(s.fetchStripePrices, conf.StripePriceCacheDuration)
return s, nil return s, nil
@@ -217,6 +228,9 @@ func (s *Server) Run() error {
if s.config.MetricsListenHTTP != "" { if s.config.MetricsListenHTTP != "" {
listenStr += fmt.Sprintf(" %s[http/metrics]", 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()) log.Tag(tagStartup).Info("Listening on%s, ntfy %s, log level is %s", listenStr, s.config.Version, log.CurrentLevel().String())
if log.IsFile() { if log.IsFile() {
fmt.Fprintf(os.Stderr, "Listening on%s, ntfy %s\n", listenStr, s.config.Version) fmt.Fprintf(os.Stderr, "Listening on%s, ntfy %s\n", listenStr, s.config.Version)
@@ -273,6 +287,18 @@ func (s *Server) Run() error {
initMetrics() initMetrics()
s.metricsHandler = promhttp.Handler() 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 != "" { if s.config.SMTPServerListen != "" {
go func() { go func() {
errChan <- s.runSMTPServer() 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) return s.ensurePaymentsEnabled(s.ensureStripeCustomer(s.handleAccountBillingPortalSessionCreate))(w, r, v)
} else if r.Method == http.MethodPost && r.URL.Path == apiAccountBillingWebhookPath { } 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! 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) return s.ensurePaymentsEnabled(s.handleBillingTiersGet)(w, r, v)
} else if r.Method == http.MethodGet && r.URL.Path == matrixPushPath { } else if r.Method == http.MethodGet && r.URL.Path == matrixPushPath {
return s.handleMatrixDiscovery(w) return s.handleMatrixDiscovery(w)
@@ -529,17 +557,34 @@ func (s *Server) handleMetrics(w http.ResponseWriter, r *http.Request, _ *visito
return nil 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 { func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request, _ *visitor) error {
r.URL.Path = webSiteDir + r.URL.Path r.URL.Path = webSiteDir + r.URL.Path
util.Gzip(http.FileServer(http.FS(webFsCached))).ServeHTTP(w, r) util.Gzip(http.FileServer(http.FS(webFsCached))).ServeHTTP(w, r)
return nil return nil
} }
// handleDocs returns static resources related to the docs
func (s *Server) handleDocs(w http.ResponseWriter, r *http.Request, _ *visitor) error { func (s *Server) handleDocs(w http.ResponseWriter, r *http.Request, _ *visitor) error {
util.Gzip(http.FileServer(http.FS(docsStaticCached))).ServeHTTP(w, r) util.Gzip(http.FileServer(http.FS(docsStaticCached))).ServeHTTP(w, r)
return nil 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. // 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 // 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. // 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) { 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") cache = readBoolParam(r, true, "x-cache", "cache")
firebase = readBoolParam(r, true, "x-firebase", "firebase") firebase = readBoolParam(r, true, "x-firebase", "firebase")
m.Title = readParam(r, "x-title", "title", "t") m.Title = maybeDecodeHeader(readParam(r, "x-title", "title", "t"))
m.Click = readParam(r, "x-click", "click") m.Click = readParam(r, "x-click", "click")
icon := readParam(r, "x-icon", "icon") icon := readParam(r, "x-icon", "icon")
filename := readParam(r, "x-filename", "filename", "file", "f") 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") messageStr := strings.ReplaceAll(readParam(r, "x-message", "message", "m"), "\\n", "\n")
if messageStr != "" { if messageStr != "" {
m.Message = messageStr m.Message = maybeDecodeHeader(messageStr)
} }
var e error var e error
m.Priority, e = util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p")) m.Priority, e = util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p"))
@@ -1512,8 +1557,14 @@ func (s *Server) runFirebaseKeepaliver() {
select { select {
case <-time.After(s.config.FirebaseKeepaliveInterval): case <-time.After(s.config.FirebaseKeepaliveInterval):
s.sendToFirebase(v, newKeepaliveMessage(firebaseControlTopic)) 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: case <-s.closeChan:
return return
} }
@@ -1541,7 +1592,7 @@ func (s *Server) sendDelayedMessages() error {
for _, m := range messages { for _, m := range messages {
var u *user.User var u *user.User
if s.userManager != nil && m.User != "" { if s.userManager != nil && m.User != "" {
u, err = s.userManager.User(m.User) u, err = s.userManager.UserByID(m.User)
if err != nil { if err != nil {
log.With(m).Err(err).Warn("Error sending delayed message") log.With(m).Err(err).Warn("Error sending delayed message")
continue continue
@@ -1557,9 +1608,9 @@ func (s *Server) sendDelayedMessages() error {
func (s *Server) sendDelayedMessage(v *visitor, m *message) error { func (s *Server) sendDelayedMessage(v *visitor, m *message) error {
logvm(v, m).Debug("Sending delayed message") 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 t, ok := s.topics[m.Topic] // If no subscribers, just mark message as published
s.mu.Unlock() s.mu.RUnlock()
if ok { if ok {
go func() { go func() {
// We do not rate-limit messages here, since we've rate limited them in the PUT/POST handler // 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 return nil
} }
func (s *Server) updateAndWriteStats(messagesCount int64) {
s.mu.Lock()
s.messagesHistory = append(s.messagesHistory, messagesCount)
if len(s.messagesHistory) > messagesHistoryMax {
s.messagesHistory = s.messagesHistory[1:]
}
s.mu.Unlock()
go func() {
if err := s.messageCache.UpdateStats(messagesCount); err != nil {
log.Tag(tagManager).Err(err).Warn("Cannot write messages stats")
}
}()
}

View File

@@ -276,6 +276,14 @@
# enable-metrics: false # enable-metrics: false
# metrics-listen-http: # 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 # Logging options
# #
# By default, ntfy logs to the console (stderr), with an "info" log level, and in a human-readable text format. # By default, ntfy logs to the console (stderr), with an "info" log level, and in a human-readable text format.

View File

@@ -701,8 +701,7 @@ func TestAccount_Reservation_Delete_Messages_And_Attachments(t *testing.T) {
require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, m2.ID)) require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, m2.ID))
} }
func TestAccount_Persist_UserStats_After_Tier_Change(t *testing.T) { /*func TestAccount_Persist_UserStats_After_Tier_Change(t *testing.T) {
t.Parallel()
conf := newTestConfigWithAuthFile(t) conf := newTestConfigWithAuthFile(t)
conf.AuthDefault = user.PermissionReadWrite conf.AuthDefault = user.PermissionReadWrite
conf.AuthStatsQueueWriterInterval = 300 * time.Millisecond 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) require.Equal(t, 200, rr.Code)
account, _ = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) account, _ = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body))
require.Equal(t, int64(2), account.Stats.Messages) // Is not reset! require.Equal(t, int64(2), account.Stats.Messages) // Is not reset!
} }*/

View File

@@ -73,9 +73,14 @@ func (s *Server) execManager() {
} }
// Print stats // Print stats
s.mu.Lock() s.mu.RLock()
messagesCount, topicsCount, visitorsCount := s.messages, len(s.topics), len(s.visitors) 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. log.
Tag(tagManager). Tag(tagManager).
Fields(log.Context{ Fields(log.Context{

View File

@@ -21,8 +21,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"heckel.io/ntfy/log" "heckel.io/ntfy/log"
"heckel.io/ntfy/util" "heckel.io/ntfy/util"
@@ -327,13 +325,10 @@ func TestServer_PublishNoCache(t *testing.T) {
func TestServer_PublishAt(t *testing.T) { func TestServer_PublishAt(t *testing.T) {
t.Parallel() t.Parallel()
c := newTestConfig(t) s := newTestServer(t, newTestConfig(t))
c.MinDelay = time.Second
c.DelayedSenderInterval = 100 * time.Millisecond
s := newTestServer(t, c)
response := request(t, s, "PUT", "/mytopic", "a message", map[string]string{ response := request(t, s, "PUT", "/mytopic", "a message", map[string]string{
"In": "1s", "In": "1h",
}) })
require.Equal(t, 200, response.Code) require.Equal(t, 200, response.Code)
@@ -341,22 +336,62 @@ func TestServer_PublishAt(t *testing.T) {
messages := toMessages(t, response.Body.String()) messages := toMessages(t, response.Body.String())
require.Equal(t, 0, len(messages)) require.Equal(t, 0, len(messages))
time.Sleep(time.Second) // Update message time to the past
require.Nil(t, s.sendDelayedMessages()) 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) response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
messages = toMessages(t, response.Body.String()) messages = toMessages(t, response.Body.String())
require.Equal(t, 1, len(messages)) require.Equal(t, 1, len(messages))
require.Equal(t, "a message", messages[0].Message) require.Equal(t, "a message", messages[0].Message)
require.Equal(t, netip.Addr{}, messages[0].Sender) // Never return the sender! 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.Nil(t, err)
require.Equal(t, 1, len(messages)) require.Equal(t, 1, len(messages))
require.Equal(t, "a message", messages[0].Message) 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! 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) { func TestServer_PublishAt_Expires(t *testing.T) {
s := newTestServer(t, newTestConfig(t)) s := newTestServer(t, newTestConfig(t))
@@ -2069,8 +2104,8 @@ func TestServer_PublishWhileUpdatingStatsWithLotsOfMessages(t *testing.T) {
start = time.Now() start = time.Now()
response := request(t, s, "PUT", "/mytopic", "some body", nil) response := request(t, s, "PUT", "/mytopic", "some body", nil)
m := toMessage(t, response.Body.String()) m := toMessage(t, response.Body.String())
assert.Equal(t, "some body", m.Message) require.Equal(t, "some body", m.Message)
assert.True(t, time.Since(start) < 100*time.Millisecond) require.True(t, time.Since(start) < 100*time.Millisecond)
log.Info("Done: Publishing message; took %s", time.Since(start).Round(time.Millisecond)) log.Info("Done: Publishing message; took %s", time.Since(start).Round(time.Millisecond))
// Wait for all goroutines // Wait for all goroutines
@@ -2362,6 +2397,91 @@ func TestServer_SubscriberRateLimiting_ProtectedTopics_WithDefaultReadWrite(t *t
require.Nil(t, s.topics["announcements"].rateVisitor) 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 { func newTestConfig(t *testing.T) *Config {
conf := NewConfig() conf := NewConfig()
conf.BaseURL = "http://127.0.0.1:12345" conf.BaseURL = "http://127.0.0.1:12345"

View File

@@ -239,6 +239,11 @@ type apiHealthResponse struct {
Healthy bool `json:"healthy"` 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 { type apiAccountCreateRequest struct {
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -214,8 +214,8 @@
"account_delete_description": "احذف حسابك نهائيا", "account_delete_description": "احذف حسابك نهائيا",
"account_delete_dialog_label": "كلمة المرور", "account_delete_dialog_label": "كلمة المرور",
"account_upgrade_dialog_title": "تغيير فئة الحساب", "account_upgrade_dialog_title": "تغيير فئة الحساب",
"account_upgrade_dialog_tier_features_messages": "{{messages}} رسائل يومية", "account_upgrade_dialog_tier_features_messages_other": "{{messages}} رسائل يومية",
"account_upgrade_dialog_tier_features_emails": "{{emails}} من رسائل البريد الإلكتروني اليومية", "account_upgrade_dialog_tier_features_emails_other": "{{emails}} من رسائل البريد الإلكتروني اليومية",
"account_upgrade_dialog_button_cancel": "إلغاء", "account_upgrade_dialog_button_cancel": "إلغاء",
"account_upgrade_dialog_button_pay_now": "ادفع الآن واشترك", "account_upgrade_dialog_button_pay_now": "ادفع الآن واشترك",
"account_upgrade_dialog_button_cancel_subscription": "إلغاء الاشتراك", "account_upgrade_dialog_button_cancel_subscription": "إلغاء الاشتراك",
@@ -314,7 +314,7 @@
"publish_dialog_progress_uploading_detail": "تحميل {{loaded}}/{{total}} ({{percent}}٪) …", "publish_dialog_progress_uploading_detail": "تحميل {{loaded}}/{{total}} ({{percent}}٪) …",
"account_basics_tier_interval_monthly": "شهريا", "account_basics_tier_interval_monthly": "شهريا",
"account_basics_tier_interval_yearly": "سنويا", "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>.", "account_upgrade_dialog_billing_contact_website": "للأسئلة المتعلقة بالفوترة، يرجى الرجوع إلى <Link>موقعنا على الويب</Link>.",
"prefs_notifications_min_priority_description_x_or_higher": "إظهار الإشعارات إذا كانت الأولوية {{number}} ({{name}}) أو أعلى", "prefs_notifications_min_priority_description_x_or_higher": "إظهار الإشعارات إذا كانت الأولوية {{number}} ({{name}}) أو أعلى",
"account_upgrade_dialog_billing_contact_email": "للأسئلة المتعلقة بالفوترة، الرجاء <Link>الاتصال بنا</Link> مباشرة.", "account_upgrade_dialog_billing_contact_email": "للأسئلة المتعلقة بالفوترة، الرجاء <Link>الاتصال بنا</Link> مباشرة.",

View File

@@ -252,7 +252,7 @@
"account_usage_attachment_storage_title": "Хранилище за прикачени файлове", "account_usage_attachment_storage_title": "Хранилище за прикачени файлове",
"account_delete_dialog_button_cancel": "Отказ", "account_delete_dialog_button_cancel": "Отказ",
"account_upgrade_dialog_interval_monthly": "Месечно", "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_upgrade_dialog_tier_features_no_reservations": "Няма резервирани теми",
"account_tokens_dialog_button_cancel": "Отказ", "account_tokens_dialog_button_cancel": "Отказ",
"account_delete_title": "Премахване на профила", "account_delete_title": "Премахване на профила",
@@ -260,5 +260,32 @@
"account_usage_emails_title": "Изпратени съобщения", "account_usage_emails_title": "Изпратени съобщения",
"account_usage_reservations_title": "Резервирани теми", "account_usage_reservations_title": "Резервирани теми",
"account_usage_reservations_none": "Няма резервирани теми", "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}}."
} }

View File

@@ -287,9 +287,9 @@
"account_upgrade_dialog_title": "Změna úrovně účtu", "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_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_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_reservations_other": "{{reservations}} rezervovaných témat",
"account_upgrade_dialog_tier_features_messages": "{{messages}} denních zpráv", "account_upgrade_dialog_tier_features_messages_other": "{{messages}} denních zpráv",
"account_upgrade_dialog_tier_features_emails": "{{emails}} denních e-mailů", "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_file_size": "{{filesize}} na soubor",
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} celkový úložný prostor", "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} celkový úložný prostor",
"account_upgrade_dialog_tier_selected_label": "Vybráno", "account_upgrade_dialog_tier_selected_label": "Vybráno",
@@ -352,5 +352,8 @@
"account_upgrade_dialog_interval_yearly": "Roční", "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_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_email": "V případě dotazů týkajících se fakturace nás prosím <Link>kontaktujte</Link> přímo.",
"account_upgrade_dialog_billing_contact_website": "Otázky týkající se fakturace naleznete na našich <Link>webových stránkách</Link>." "account_upgrade_dialog_billing_contact_website": "Otázky týkající se fakturace naleznete na našich <Link>webových stránkách</Link>.",
"account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} rezervované téma",
"account_upgrade_dialog_tier_features_messages_one": "{{messages}} denní zpráva",
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} denní e-mail"
} }

View File

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

View File

@@ -201,18 +201,18 @@
"account_basics_password_dialog_current_password_label": "Nuværende kodeord", "account_basics_password_dialog_current_password_label": "Nuværende kodeord",
"account_basics_password_dialog_new_password_label": "Nyt kodeord", "account_basics_password_dialog_new_password_label": "Nyt kodeord",
"notifications_loading": "Indlæser notifikationer…", "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_table_create_token_button": "Opret adgangstoken",
"account_tokens_dialog_title_delete": "Slet adgangstoken", "account_tokens_dialog_title_delete": "Slet adgangstoken",
"publish_dialog_chip_email_label": "Videresend til e-mail", "publish_dialog_chip_email_label": "Videresend til e-mail",
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} samlet lagerplads", "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} samlet lagerplads",
"subscribe_dialog_subscribe_use_another_label": "Brug en anden server", "subscribe_dialog_subscribe_use_another_label": "Brug en anden server",
"account_basics_tier_upgrade_button": "Opgrader til Pro", "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", "account_tokens_table_copy_to_clipboard": "Kopier til udklipsholder",
"prefs_reservations_edit_button": "Rediger emneadgang", "prefs_reservations_edit_button": "Rediger emneadgang",
"account_upgrade_dialog_title": "Skift kontoniveau", "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_dialog_expires_never": "Token udløber aldrig",
"account_tokens_table_current_session": "Nuværende browsersession", "account_tokens_table_current_session": "Nuværende browsersession",
"account_tokens_dialog_title_edit": "Rediger adgangstoken", "account_tokens_dialog_title_edit": "Rediger adgangstoken",

View File

@@ -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_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_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_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_reservations_other": "{{reservations}} reservierte Themen",
"account_upgrade_dialog_tier_features_messages": "{{messages}} Nachrichten pro Tag", "account_upgrade_dialog_tier_features_messages_other": "{{messages}} Nachrichten pro Tag",
"account_upgrade_dialog_tier_features_emails": "{{emails}} Emails 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_file_size": "{{filesize}} pro Datei",
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} gesamter Speicherplatz", "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} gesamter Speicherplatz",
"account_upgrade_dialog_tier_selected_label": "Ausgewählt", "account_upgrade_dialog_tier_selected_label": "Ausgewählt",
@@ -352,5 +352,8 @@
"account_basics_tier_interval_monthly": "monatlich", "account_basics_tier_interval_monthly": "monatlich",
"account_upgrade_dialog_interval_monthly": "Monatlich", "account_upgrade_dialog_interval_monthly": "Monatlich",
"account_upgrade_dialog_tier_price_billed_monthly": "{{price}} pro Jahr. Monatlich abgerechnet.", "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} pro Jahr. Monatlich abgerechnet.",
"account_upgrade_dialog_interval_yearly": "Jährlich" "account_upgrade_dialog_interval_yearly": "Jährlich",
"account_upgrade_dialog_tier_features_messages_one": "{{messages}} tägliche Nachricht",
"account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} reserviertes Thema",
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} tägliche E-Mail"
} }

View File

@@ -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_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_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_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_no_reservations": "No reserved topics",
"account_upgrade_dialog_tier_features_messages": "{{messages}} daily messages", "account_upgrade_dialog_tier_features_messages_one": "{{messages}} daily message",
"account_upgrade_dialog_tier_features_emails": "{{emails}} daily emails", "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_file_size": "{{filesize}} per file",
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} total storage", "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} total storage",
"account_upgrade_dialog_tier_price_per_month": "month", "account_upgrade_dialog_tier_price_per_month": "month",

View File

@@ -107,7 +107,7 @@
"prefs_appearance_language_title": "Idioma", "prefs_appearance_language_title": "Idioma",
"error_boundary_title": "Oh no, ntfy tuvo un error", "error_boundary_title": "Oh no, ntfy tuvo un error",
"error_boundary_button_copy_stack_trace": "Copiar el stack trace", "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 …", "error_boundary_gathering_info": "Reunir más información …",
"notifications_example": "Ejemplo", "notifications_example": "Ejemplo",
"prefs_notifications_min_priority_title": "Prioridad mínima", "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_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_label": "Contraseña",
"account_delete_dialog_button_submit": "Eliminar permanentemente la cuenta", "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_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_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_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_messages_other": "{{messages}} mensajes diarios",
"account_upgrade_dialog_tier_features_emails": "{{emails}} correos 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_file_size": "{{filesize}} por archivo",
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} almacenamiento total", "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} almacenamiento total",
"account_upgrade_dialog_tier_current_label": "Actual", "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_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_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_tier_price_billed_monthly": "{{price}} al año. Facturación mensual.",
"account_upgrade_dialog_billing_contact_email": "Para preguntas sobre facturación, por favor <Link>contáctenos</Link> directamente." "account_upgrade_dialog_billing_contact_email": "Para preguntas sobre facturación, por favor <Link>contáctenos</Link> directamente.",
"account_upgrade_dialog_tier_features_messages_one": "{{messages}} mensaje diario",
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} correo electrónico diario",
"account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} tema reservado"
} }

View File

@@ -274,9 +274,9 @@
"account_upgrade_dialog_title": "Changer le tarif du compte", "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_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_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_reservations_other": "{{reservations}} sujets réservés",
"account_upgrade_dialog_tier_features_messages": "{{messages}} messages journaliers", "account_upgrade_dialog_tier_features_messages_other": "{{messages}} messages journaliers",
"account_upgrade_dialog_tier_features_emails": "{{emails}} emails 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_file_size": "{{filesize}} par fichier",
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} stockage total", "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} stockage total",
"account_upgrade_dialog_tier_selected_label": "Sélectionné", "account_upgrade_dialog_tier_selected_label": "Sélectionné",

View File

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

View File

@@ -258,9 +258,9 @@
"account_upgrade_dialog_title": "Ubah peringkat akun", "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_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_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_reservations_other": "{{reservations}} topik yang telah direservasi",
"account_upgrade_dialog_tier_features_messages": "{{messages}} pesan harian", "account_upgrade_dialog_tier_features_messages_other": "{{messages}} pesan harian",
"account_upgrade_dialog_tier_features_emails": "{{emails}} surel 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_file_size": "{{filesize}} per berkas",
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} jumlah penyimpanan", "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} jumlah penyimpanan",
"account_upgrade_dialog_tier_selected_label": "Dipilih", "account_upgrade_dialog_tier_selected_label": "Dipilih",
@@ -352,5 +352,8 @@
"account_upgrade_dialog_tier_price_per_month": "bulan", "account_upgrade_dialog_tier_price_per_month": "bulan",
"account_upgrade_dialog_tier_price_billed_monthly": "{{price}} per bulan. Ditagih setiap 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_email": "Untuk pertanyaan penagihan, silakan <Link>hubungi kami</Link> secara langsung.",
"account_upgrade_dialog_billing_contact_website": "Untuk pertanyaan penagihan, silakan menuju ke <Link>situs web</Link> kami." "account_upgrade_dialog_billing_contact_website": "Untuk pertanyaan penagihan, silakan menuju ke <Link>situs web</Link> kami.",
"account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} topik yang direservasi",
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} surel harian",
"account_upgrade_dialog_tier_features_messages_one": "{{messages}} pesan harian"
} }

View File

@@ -187,5 +187,74 @@
"prefs_notifications_delete_after_one_week": "Dopo una settimana", "prefs_notifications_delete_after_one_week": "Dopo una settimana",
"prefs_notifications_delete_after_one_month": "Dopo un mese", "prefs_notifications_delete_after_one_month": "Dopo un mese",
"prefs_notifications_delete_after_three_hours_description": "Le notifiche vengono eliminate automaticamente dopo tre ore", "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"
} }

View File

@@ -241,9 +241,9 @@
"account_upgrade_dialog_title": "アカウントティアを変更", "account_upgrade_dialog_title": "アカウントティアを変更",
"account_upgrade_dialog_cancel_warning": "これにより<strong>サブスクリプションをキャンセルし</strong>{{date}}にアカウントをダウングレードします。同日、トピック予約およびサーバーにキャッシュされたメッセージは<strong>削除されます</strong>。", "account_upgrade_dialog_cancel_warning": "これにより<strong>サブスクリプションをキャンセルし</strong>{{date}}にアカウントをダウングレードします。同日、トピック予約およびサーバーにキャッシュされたメッセージは<strong>削除されます</strong>。",
"account_upgrade_dialog_proration_info": "<strong>追記</strong>。有料プランをアップグレードする場合、価格差は<strong>即座に請求されます</strong>。ダウングレードする場合、差額は次の請求期間の支払いに利用されます。", "account_upgrade_dialog_proration_info": "<strong>追記</strong>。有料プランをアップグレードする場合、価格差は<strong>即座に請求されます</strong>。ダウングレードする場合、差額は次の請求期間の支払いに利用されます。",
"account_upgrade_dialog_tier_features_reservations": "予約のトピック{{reservations}}件", "account_upgrade_dialog_tier_features_reservations_other": "予約のトピック{{reservations}}件",
"account_upgrade_dialog_tier_features_emails": "日次メール{{emails}}件", "account_upgrade_dialog_tier_features_emails_other": "日次メール{{emails}}件",
"account_upgrade_dialog_tier_features_messages": "日次メッセージ{{messages}}件", "account_upgrade_dialog_tier_features_messages_other": "日次メッセージ{{messages}}件",
"account_upgrade_dialog_tier_selected_label": "選択", "account_upgrade_dialog_tier_selected_label": "選択",
"account_upgrade_dialog_tier_current_label": "現在", "account_upgrade_dialog_tier_current_label": "現在",
"account_upgrade_dialog_button_cancel": "キャンセル", "account_upgrade_dialog_button_cancel": "キャンセル",
@@ -352,5 +352,8 @@
"account_upgrade_dialog_tier_price_per_month": "月", "account_upgrade_dialog_tier_price_per_month": "月",
"account_upgrade_dialog_tier_price_billed_monthly": "年間{{price}}。月毎の支払い。", "account_upgrade_dialog_tier_price_billed_monthly": "年間{{price}}。月毎の支払い。",
"account_upgrade_dialog_tier_price_billed_yearly": "年間{{price}}の支払い。{{save}}節約。", "account_upgrade_dialog_tier_price_billed_yearly": "年間{{price}}の支払い。{{save}}節約。",
"account_upgrade_dialog_billing_contact_website": "支払いに関する質問は、<Link>ウェブサイト</Link>を参照して下さい。" "account_upgrade_dialog_billing_contact_website": "支払いに関する質問は、<Link>ウェブサイト</Link>を参照して下さい。",
"account_upgrade_dialog_tier_features_messages_one": "毎日 {{messages}} メッセージ",
"account_upgrade_dialog_tier_features_reservations_one": "予約済みトピック {{reservations}} 件",
"account_upgrade_dialog_tier_features_emails_one": "毎日メール {{emails}} 件"
} }

View File

@@ -308,5 +308,14 @@
"account_upgrade_dialog_button_pay_now": "Zapłać i aktywuj subskrypcję", "account_upgrade_dialog_button_pay_now": "Zapłać i aktywuj subskrypcję",
"account_tokens_dialog_button_cancel": "Anuluj", "account_tokens_dialog_button_cancel": "Anuluj",
"account_tokens_dialog_expires_label": "Token dostępowy wygasa po", "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"
} }

View File

@@ -31,7 +31,7 @@
"notifications_attachment_copy_url_title": "Copiar URL do anexo para a área de transferência", "notifications_attachment_copy_url_title": "Copiar URL do anexo para a área de transferência",
"notifications_attachment_copy_url_button": "Copiar URL", "notifications_attachment_copy_url_button": "Copiar URL",
"notifications_attachment_open_title": "Ir para {{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_open_button": "Abrir anexo",
"notifications_attachment_link_expires": "a ligação expira em {{date}}", "notifications_attachment_link_expires": "a ligação expira em {{date}}",
"notifications_attachment_file_image": "ficheiro de imagem", "notifications_attachment_file_image": "ficheiro de imagem",

View File

@@ -1,5 +1,5 @@
{ {
"publish_dialog_priority_min": "Наименьший приоритет", "publish_dialog_priority_min": "Минимальный приоритет",
"action_bar_settings": "Настройки", "action_bar_settings": "Настройки",
"action_bar_send_test_notification": "Отправить тестовое уведомление", "action_bar_send_test_notification": "Отправить тестовое уведомление",
"action_bar_clear_notifications": "Удалить все уведомления", "action_bar_clear_notifications": "Удалить все уведомления",
@@ -24,7 +24,7 @@
"publish_dialog_priority_low": "Низкий приоритет", "publish_dialog_priority_low": "Низкий приоритет",
"publish_dialog_priority_default": "Стандартный приоритет", "publish_dialog_priority_default": "Стандартный приоритет",
"publish_dialog_priority_high": "Высокий приоритет", "publish_dialog_priority_high": "Высокий приоритет",
"publish_dialog_priority_max": "Наивысший приоритет", "publish_dialog_priority_max": "Максимальный приоритет",
"publish_dialog_base_url_label": "URL-адрес сервиса", "publish_dialog_base_url_label": "URL-адрес сервиса",
"publish_dialog_base_url_placeholder": "URL-адрес сервиса, например https://example.com", "publish_dialog_base_url_placeholder": "URL-адрес сервиса, например https://example.com",
"publish_dialog_topic_label": "Название темы", "publish_dialog_topic_label": "Название темы",
@@ -106,13 +106,13 @@
"prefs_notifications_sound_title": "Звук уведомления", "prefs_notifications_sound_title": "Звук уведомления",
"prefs_notifications_sound_description_none": "Уведомления не воспроизводят никаких звуков при получении", "prefs_notifications_sound_description_none": "Уведомления не воспроизводят никаких звуков при получении",
"prefs_notifications_sound_no_sound": "Без звука", "prefs_notifications_sound_no_sound": "Без звука",
"prefs_notifications_min_priority_title": "Наименьший приоритет", "prefs_notifications_min_priority_title": "Минимальный приоритет",
"prefs_notifications_min_priority_description_any": "Показать все уведомления, независимо от приоритета", "prefs_notifications_min_priority_description_any": "Показывать все уведомления, независимо от приоритета",
"prefs_notifications_min_priority_description_x_or_higher": "Показывать уведомления, если приоритет {{number}} ({{name}}) или выше", "prefs_notifications_min_priority_description_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_any": "Любой приоритет",
"prefs_notifications_min_priority_low_and_higher": "Низкий приоритет и выше", "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_title": "Удалить уведомления",
"prefs_notifications_delete_after_never": "Никогда", "prefs_notifications_delete_after_never": "Никогда",
"prefs_notifications_delete_after_three_hours": "Через три часа", "prefs_notifications_delete_after_three_hours": "Через три часа",
@@ -140,11 +140,11 @@
"common_save": "Сохранить", "common_save": "Сохранить",
"prefs_appearance_title": "Внешний вид", "prefs_appearance_title": "Внешний вид",
"prefs_appearance_language_title": "Язык", "prefs_appearance_language_title": "Язык",
"priority_min": "наименьший", "priority_min": "минимальный",
"priority_low": "низкий", "priority_low": "низкий",
"priority_default": "стандартный", "priority_default": "стандартный",
"priority_high": "высокий", "priority_high": "высокий",
"priority_max": "наивысший", "priority_max": "максимальный",
"error_boundary_title": "О нет, ntfy сломался", "error_boundary_title": "О нет, ntfy сломался",
"error_boundary_button_copy_stack_trace": "Скопировать трассировку стека", "error_boundary_button_copy_stack_trace": "Скопировать трассировку стека",
"error_boundary_stack_trace": "Трассировка стека", "error_boundary_stack_trace": "Трассировка стека",
@@ -192,7 +192,7 @@
"account_tokens_dialog_button_create": "Создать токен", "account_tokens_dialog_button_create": "Создать токен",
"account_tokens_delete_dialog_submit_button": "Безвозвратно удалить токен", "account_tokens_delete_dialog_submit_button": "Безвозвратно удалить токен",
"account_upgrade_dialog_reservations_warning_other": "Выбранная подписка разрешает меньше зарезервированных тем, чем есть у Вас на данный момент. Перед сменой подписки, <strong>пожалуйста удалите хотя бы {{count}} зарезервированных тем</strong>. Вы можете это сделать в <Link>Настройках</Link>.", "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_features_attachment_total_size": "{{totalsize}} суммарный объем",
"account_upgrade_dialog_tier_selected_label": "Выбранная", "account_upgrade_dialog_tier_selected_label": "Выбранная",
"account_tokens_table_current_session": "Текущий сеанс браузера", "account_tokens_table_current_session": "Текущий сеанс браузера",
@@ -201,8 +201,8 @@
"account_tokens_dialog_expires_x_hours": "Токен истекает через {{hours}} часов", "account_tokens_dialog_expires_x_hours": "Токен истекает через {{hours}} часов",
"account_tokens_dialog_expires_never": "Токен никогда не истекает", "account_tokens_dialog_expires_never": "Токен никогда не истекает",
"prefs_notifications_sound_play": "Воспроизводить выбранный звук", "prefs_notifications_sound_play": "Воспроизводить выбранный звук",
"account_upgrade_dialog_tier_features_reservations": "{{reservations}} зарезервированных тем", "account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} зарезервированных тем",
"account_upgrade_dialog_tier_features_emails": "{{emails}} эл. сообщений в день", "account_upgrade_dialog_tier_features_emails_other": "{{emails}} эл. сообщений в день",
"account_basics_tier_free": "Бесплатный", "account_basics_tier_free": "Бесплатный",
"account_tokens_dialog_title_create": "Создать токен доступа", "account_tokens_dialog_title_create": "Создать токен доступа",
"account_tokens_dialog_title_delete": "Удалить токен доступа", "account_tokens_dialog_title_delete": "Удалить токен доступа",
@@ -303,7 +303,7 @@
"account_usage_reservations_title": "Зарезервированные темы", "account_usage_reservations_title": "Зарезервированные темы",
"account_usage_reservations_none": "Нет зарезервированных тем", "account_usage_reservations_none": "Нет зарезервированных тем",
"account_usage_attachment_storage_title": "Хранение вложений", "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_usage_cannot_create_portal_session": "Невозможно открыть портал оплаты",
"account_delete_title": "Удалить учетную запись", "account_delete_title": "Удалить учетную запись",
"account_delete_description": "Безвозвратно удалить Вашу учетную запись", "account_delete_description": "Безвозвратно удалить Вашу учетную запись",

View File

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

View File

@@ -253,9 +253,9 @@
"account_upgrade_dialog_title": "Hesap seviyesini değiştir", "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_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_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_reservations_other": "{{reservations}} konu ayırtıldı",
"account_upgrade_dialog_tier_features_messages": "{{messages}} günlük mesaj", "account_upgrade_dialog_tier_features_messages_other": "{{messages}} günlük mesaj",
"account_upgrade_dialog_tier_features_emails": "{{emails}} günlük e-posta", "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_file_size": "dosya başına {{filesize}}",
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} toplam depolama", "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} toplam depolama",
"account_upgrade_dialog_tier_selected_label": "Seçilen", "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_yearly_discount_save_up_to": "%{{discount}} kadar tasarruf edin",
"account_upgrade_dialog_interval_monthly": "Aylık", "account_upgrade_dialog_interval_monthly": "Aylık",
"account_basics_tier_interval_monthly": "aylık", "account_basics_tier_interval_monthly": "aylık",
"account_upgrade_dialog_billing_contact_website": "Faturalama ile ilgili sorularınız için lütfen <Link>web sitemizi ziyaret edin</Link>." "account_upgrade_dialog_billing_contact_website": "Faturalama ile ilgili sorularınız için lütfen <Link>web sitemizi ziyaret edin</Link>.",
"account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} ayırtılan konu",
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} günlük e-posta",
"account_upgrade_dialog_tier_features_messages_one": "{{messages}} günlük mesaj"
} }

View File

@@ -293,12 +293,12 @@
"account_delete_dialog_billing_warning": "删除您的帐户也会立即取消您的计费订阅。您将无法再访问计费仪表板。", "account_delete_dialog_billing_warning": "删除您的帐户也会立即取消您的计费订阅。您将无法再访问计费仪表板。",
"account_upgrade_dialog_title": "更改帐户等级", "account_upgrade_dialog_title": "更改帐户等级",
"account_upgrade_dialog_cancel_warning": "这将<strong>取消您的订阅</strong>,并在 {{date}} 降级您的帐户。在那一天,主题保留以及缓存在服务器上的消息<strong>将被删除</strong>。", "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_one": "所选等级允许的保留主题少于当前等级。在更改您的等级之前,<strong>请至少删除 1 项保留</strong>。您可以在<Link>设置</Link>中删除保留。",
"account_upgrade_dialog_reservations_warning_other": "所选等级允许的保留主题少于当前等级。在更改您的等级之前,<strong>请至少删除 {{count}} 项保留</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_reservations_other": "{{reservations}} 条保留主题",
"account_upgrade_dialog_tier_features_messages": "{{messages}} 条每日消息", "account_upgrade_dialog_tier_features_messages_other": "{{messages}} 条每日消息",
"account_upgrade_dialog_tier_features_emails": "{{emails}} 条每日邮件", "account_upgrade_dialog_tier_features_emails_other": "{{emails}} 条每日邮件",
"account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} 每个文件", "account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} 每个文件",
"signup_form_confirm_password": "确认密码", "signup_form_confirm_password": "确认密码",
"signup_form_button_submit": "注册", "signup_form_button_submit": "注册",
@@ -340,5 +340,17 @@
"account_tokens_table_last_origin_tooltip": "于IP地址 {{ip}},点击查找", "account_tokens_table_last_origin_tooltip": "于IP地址 {{ip}},点击查找",
"account_tokens_dialog_label": "标签例如Radarr 通知", "account_tokens_dialog_label": "标签例如Radarr 通知",
"account_tokens_dialog_button_create": "创建令牌", "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>。"
} }

View File

@@ -436,10 +436,17 @@ const Appearance = () => {
const Language = () => { const Language = () => {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const labelId = "prefLanguage"; const labelId = "prefLanguage";
const randomFlags = shuffle(["🇬🇧", "🇺🇸", "🇪🇸", "🇫🇷", "🇧🇬", "🇨🇿", "🇩🇪", "🇵🇱", "🇺🇦", "🇨🇳", "🇮🇹", "🇭🇺", "🇧🇷", "🇳🇱", "🇮🇩", "🇯🇵", "🇷🇺", "🇹🇷"]).slice(0, 3);
const title = t("prefs_appearance_language_title") + " " + randomFlags.join(" ");
const lang = i18n.language ?? "en"; 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) => { const handleChange = async (ev) => {
await i18n.changeLanguage(ev.target.value); await i18n.changeLanguage(ev.target.value);
await maybeUpdateAccountSettings({ await maybeUpdateAccountSettings({
@@ -476,6 +483,7 @@ const Language = () => {
<MenuItem value="pt_BR">Português (Brasil)</MenuItem> <MenuItem value="pt_BR">Português (Brasil)</MenuItem>
<MenuItem value="pl">Polski</MenuItem> <MenuItem value="pl">Polski</MenuItem>
<MenuItem value="ru">Русский</MenuItem> <MenuItem value="ru">Русский</MenuItem>
<MenuItem value="sv">Svenska</MenuItem>
<MenuItem value="tr">Türkçe</MenuItem> <MenuItem value="tr">Türkçe</MenuItem>
</Select> </Select>
</FormControl> </FormControl>

View File

@@ -297,10 +297,10 @@ const TierCard = (props) => {
{monthlyPrice > 0 && <>/ {t("account_upgrade_dialog_tier_price_per_month")}</>} {monthlyPrice > 0 && <>/ {t("account_upgrade_dialog_tier_price_per_month")}</>}
</div> </div>
<List dense> <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>} {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_messages", { messages: formatNumber(tier.limits.messages), count: 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_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_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> <Feature>{t("account_upgrade_dialog_tier_features_attachment_total_size", { totalsize: formatBytes(tier.limits.attachment_total_size, 0) })}</Feature>
</List> </List>