Merge branch 'main' into 303-update-notifications

This commit is contained in:
binwiederhier
2026-01-05 15:34:42 -05:00
80 changed files with 2948 additions and 1146 deletions

View File

@@ -1,3 +1,16 @@
<div align="center" markdown="1">
<sup>Special thanks to:</sup>
<br>
<br>
<a href="https://go.warp.dev/ntfy">
<img alt="Warp sponsorship" width="400" src="https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Github/Sponsor/Warp-Github-LG-02.png">
</a>
### [Warp, built for coding with multiple AI agents.](https://go.warp.dev/ntfy)
[Available for MacOS, Linux, & Windows](https://go.warp.dev/ntfy)<br>
</div>
<hr>
![ntfy](web/public/static/images/ntfy.png) ![ntfy](web/public/static/images/ntfy.png)
# ntfy.sh | Send push notifications to your phone or desktop via PUT/POST # ntfy.sh | Send push notifications to your phone or desktop via PUT/POST
@@ -67,6 +80,8 @@ Thank you to our commercial sponsors, who help keep the service running and the
<a href="https://www.magicbell.com/?utm_source=ntfy"><img src="assets/sponsors/magicbell.png" width="180px"></a> <a href="https://www.magicbell.com/?utm_source=ntfy"><img src="assets/sponsors/magicbell.png" width="180px"></a>
<a href="https://go.warp.dev/ntfy"><img src="https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Logos/Warp-Wordmark-Black.png" width="160px"></a>
And a big fat **Thank You** to the individuals who have sponsored ntfy in the past, or are still sponsoring ntfy: And a big fat **Thank You** to the individuals who have sponsored ntfy in the past, or are still sponsoring ntfy:
<a href="https://github.com/neutralinsomniac"><img src="https://github.com/neutralinsomniac.png" width="40px" /></a> <a href="https://github.com/neutralinsomniac"><img src="https://github.com/neutralinsomniac.png" width="40px" /></a>

View File

@@ -6,5 +6,7 @@ As of today, I only support the latest version of ntfy. Please make sure you sta
## Reporting a Vulnerability ## Reporting a Vulnerability
Please report severe security issues privately via ntfy@heckel.io, [Discord](https://discord.gg/cT7ECsZj9w), Please report security vulnerabilities privately via email to [security@mail.ntfy.sh](mailto:security@mail.ntfy.sh).
or [Matrix](https://matrix.to/#/#ntfy:matrix.org) (my username is `binwiederhier`).
You can also reach me on [Discord](https://discord.gg/cT7ECsZj9w) or [Matrix](https://matrix.to/#/#ntfy:matrix.org)
(my username is `binwiederhier`).

View File

@@ -21,7 +21,7 @@
# default-command: # default-command:
# Subscriptions to topics and their actions. This option is primarily used by the systemd service, # Subscriptions to topics and their actions. This option is primarily used by the systemd service,
# or if you cann "ntfy subscribe --from-config" directly. # or if you can "ntfy subscribe --from-config" directly.
# #
# Example: # Example:
# subscribe: # subscribe:

View File

@@ -1,5 +1,4 @@
//go:build darwin || linux || dragonfly || freebsd || netbsd || openbsd //go:build darwin || linux || dragonfly || freebsd || netbsd || openbsd
// +build darwin linux dragonfly freebsd netbsd openbsd
package cmd package cmd

View File

@@ -16,10 +16,10 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/stripe/stripe-go/v74"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc" "github.com/urfave/cli/v2/altsrc"
"heckel.io/ntfy/v2/log" "heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/payments"
"heckel.io/ntfy/v2/server" "heckel.io/ntfy/v2/server"
"heckel.io/ntfy/v2/user" "heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util" "heckel.io/ntfy/v2/util"
@@ -63,6 +63,7 @@ var flagsServe = append(
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-signup", Aliases: []string{"enable_signup"}, EnvVars: []string{"NTFY_ENABLE_SIGNUP"}, Value: false, Usage: "allows users to sign up via the web app, or API"}), altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-signup", Aliases: []string{"enable_signup"}, EnvVars: []string{"NTFY_ENABLE_SIGNUP"}, Value: false, Usage: "allows users to sign up via the web app, or API"}),
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-login", Aliases: []string{"enable_login"}, EnvVars: []string{"NTFY_ENABLE_LOGIN"}, Value: false, Usage: "allows users to log in via the web app, or API"}), altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-login", Aliases: []string{"enable_login"}, EnvVars: []string{"NTFY_ENABLE_LOGIN"}, Value: false, Usage: "allows users to log in via the web app, or API"}),
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-reservations", Aliases: []string{"enable_reservations"}, EnvVars: []string{"NTFY_ENABLE_RESERVATIONS"}, Value: false, Usage: "allows users to reserve topics (if their tier allows it)"}), altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-reservations", Aliases: []string{"enable_reservations"}, EnvVars: []string{"NTFY_ENABLE_RESERVATIONS"}, Value: false, Usage: "allows users to reserve topics (if their tier allows it)"}),
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "require-login", Aliases: []string{"require_login"}, EnvVars: []string{"NTFY_REQUIRE_LOGIN"}, Value: false, Usage: "all actions via the web app requires a login"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "upstream-base-url", Aliases: []string{"upstream_base_url"}, EnvVars: []string{"NTFY_UPSTREAM_BASE_URL"}, Value: "", Usage: "forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "upstream-base-url", Aliases: []string{"upstream_base_url"}, EnvVars: []string{"NTFY_UPSTREAM_BASE_URL"}, Value: "", Usage: "forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "upstream-access-token", Aliases: []string{"upstream_access_token"}, EnvVars: []string{"NTFY_UPSTREAM_ACCESS_TOKEN"}, Value: "", Usage: "access token to use for the upstream server; needed only if upstream rate limits are exceeded or upstream server requires auth"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "upstream-access-token", Aliases: []string{"upstream_access_token"}, EnvVars: []string{"NTFY_UPSTREAM_ACCESS_TOKEN"}, Value: "", Usage: "access token to use for the upstream server; needed only if upstream rate limits are exceeded or upstream server requires auth"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-addr", Aliases: []string{"smtp_sender_addr"}, EnvVars: []string{"NTFY_SMTP_SENDER_ADDR"}, Usage: "SMTP server address (host:port) for outgoing emails"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-addr", Aliases: []string{"smtp_sender_addr"}, EnvVars: []string{"NTFY_SMTP_SENDER_ADDR"}, Usage: "SMTP server address (host:port) for outgoing emails"}),
@@ -171,6 +172,7 @@ func execServe(c *cli.Context) error {
webRoot := c.String("web-root") webRoot := c.String("web-root")
enableSignup := c.Bool("enable-signup") enableSignup := c.Bool("enable-signup")
enableLogin := c.Bool("enable-login") enableLogin := c.Bool("enable-login")
requireLogin := c.Bool("require-login")
enableReservations := c.Bool("enable-reservations") enableReservations := c.Bool("enable-reservations")
upstreamBaseURL := c.String("upstream-base-url") upstreamBaseURL := c.String("upstream-base-url")
upstreamAccessToken := c.String("upstream-access-token") upstreamAccessToken := c.String("upstream-access-token")
@@ -279,6 +281,8 @@ func execServe(c *cli.Context) error {
// Check values // Check values
if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) { if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) {
return errors.New("if set, FCM key file must exist") return errors.New("if set, FCM key file must exist")
} else if firebaseKeyFile != "" && !server.FirebaseAvailable {
return errors.New("cannot set firebase-key-file, support for Firebase is not available (nofirebase)")
} else if webPushPublicKey != "" && (webPushPrivateKey == "" || webPushFile == "" || webPushEmailAddress == "" || baseURL == "") { } else if webPushPublicKey != "" && (webPushPrivateKey == "" || webPushFile == "" || webPushEmailAddress == "" || baseURL == "") {
return errors.New("if web push is enabled, web-push-private-key, web-push-public-key, web-push-file, web-push-email-address, and base-url should be set. run 'ntfy webpush keys' to generate keys") return errors.New("if web push is enabled, web-push-private-key, web-push-public-key, web-push-file, web-push-email-address, and base-url should be set. run 'ntfy webpush keys' to generate keys")
} else if keepaliveInterval < 5*time.Second { } else if keepaliveInterval < 5*time.Second {
@@ -316,10 +320,14 @@ func execServe(c *cli.Context) error {
return errors.New("if upstream-base-url is set, base-url must also be set") return errors.New("if upstream-base-url is set, base-url must also be set")
} else if upstreamBaseURL != "" && baseURL != "" && baseURL == upstreamBaseURL { } else if upstreamBaseURL != "" && baseURL != "" && baseURL == upstreamBaseURL {
return errors.New("base-url and upstream-base-url cannot be identical, you'll likely want to set upstream-base-url to https://ntfy.sh, see https://ntfy.sh/docs/config/#ios-instant-notifications") return errors.New("base-url and upstream-base-url cannot be identical, you'll likely want to set upstream-base-url to https://ntfy.sh, see https://ntfy.sh/docs/config/#ios-instant-notifications")
} else if authFile == "" && (enableSignup || enableLogin || enableReservations || stripeSecretKey != "") { } else if authFile == "" && (enableSignup || enableLogin || requireLogin || enableReservations || stripeSecretKey != "") {
return errors.New("cannot set enable-signup, enable-login, enable-reserve-topics, or stripe-secret-key if auth-file is not set") return errors.New("cannot set enable-signup, enable-login, require-login, enable-reserve-topics, or stripe-secret-key if auth-file is not set")
} else if enableSignup && !enableLogin { } else if enableSignup && !enableLogin {
return errors.New("cannot set enable-signup without also setting enable-login") return errors.New("cannot set enable-signup without also setting enable-login")
} else if requireLogin && !enableLogin {
return errors.New("cannot set require-login without also setting enable-login")
} else if !payments.Available && (stripeSecretKey != "" || stripeWebhookKey != "") {
return errors.New("cannot set stripe-secret-key or stripe-webhook-key, support for payments is not available in this build (nopayments)")
} else if stripeSecretKey != "" && (stripeWebhookKey == "" || baseURL == "") { } else if stripeSecretKey != "" && (stripeWebhookKey == "" || baseURL == "") {
return errors.New("if stripe-secret-key is set, stripe-webhook-key and base-url must also be set") return errors.New("if stripe-secret-key is set, stripe-webhook-key and base-url must also be set")
} else if twilioAccount != "" && (twilioAuthToken == "" || twilioPhoneNumber == "" || twilioVerifyService == "" || baseURL == "" || authFile == "") { } else if twilioAccount != "" && (twilioAuthToken == "" || twilioPhoneNumber == "" || twilioVerifyService == "" || baseURL == "" || authFile == "") {
@@ -329,6 +337,8 @@ func execServe(c *cli.Context) error {
if messageSizeLimit > 5*1024*1024 { if messageSizeLimit > 5*1024*1024 {
return errors.New("message-size-limit cannot be higher than 5M") return errors.New("message-size-limit cannot be higher than 5M")
} }
} else if !server.WebPushAvailable && (webPushPrivateKey != "" || webPushPublicKey != "" || webPushFile != "") {
return errors.New("cannot enable WebPush, support is not available in this build (nowebpush)")
} else if webPushExpiryWarningDuration > 0 && webPushExpiryWarningDuration > webPushExpiryDuration { } else if webPushExpiryWarningDuration > 0 && webPushExpiryWarningDuration > webPushExpiryDuration {
return errors.New("web push expiry warning duration cannot be higher than web push expiry duration") return errors.New("web push expiry warning duration cannot be higher than web push expiry duration")
} else if behindProxy && proxyForwardedHeader == "" { } else if behindProxy && proxyForwardedHeader == "" {
@@ -396,8 +406,7 @@ func execServe(c *cli.Context) error {
// Stripe things // Stripe things
if stripeSecretKey != "" { if stripeSecretKey != "" {
stripe.EnableTelemetry = false // Whoa! payments.Setup(stripeSecretKey)
stripe.Key = stripeSecretKey
} }
// Add default forbidden topics // Add default forbidden topics
@@ -470,6 +479,7 @@ func execServe(c *cli.Context) error {
conf.BillingContact = billingContact conf.BillingContact = billingContact
conf.EnableSignup = enableSignup conf.EnableSignup = enableSignup
conf.EnableLogin = enableLogin conf.EnableLogin = enableLogin
conf.RequireLogin = requireLogin
conf.EnableReservations = enableReservations conf.EnableReservations = enableReservations
conf.EnableMetrics = enableMetrics conf.EnableMetrics = enableMetrics
conf.MetricsListenHTTP = metricsListenHTTP conf.MetricsListenHTTP = metricsListenHTTP
@@ -550,8 +560,8 @@ func parseUsers(usersRaw []string) ([]*user.User, error) {
role := user.Role(strings.TrimSpace(parts[2])) role := user.Role(strings.TrimSpace(parts[2]))
if !user.AllowedUsername(username) { if !user.AllowedUsername(username) {
return nil, fmt.Errorf("invalid auth-users: %s, username invalid", userLine) return nil, fmt.Errorf("invalid auth-users: %s, username invalid", userLine)
} else if err := user.ValidPasswordHash(passwordHash); err != nil { } else if err := user.ValidPasswordHash(passwordHash, user.DefaultUserPasswordBcryptCost); err != nil {
return nil, fmt.Errorf("invalid auth-users: %s, %s", userLine, err.Error()) return nil, fmt.Errorf("invalid auth-users: %s, password hash invalid, %s", userLine, err.Error())
} else if !user.AllowedRole(role) { } else if !user.AllowedRole(role) {
return nil, fmt.Errorf("invalid auth-users: %s, role %s is not allowed, allowed roles are 'admin' or 'user'", userLine, role) return nil, fmt.Errorf("invalid auth-users: %s, role %s is not allowed, allowed roles are 'admin' or 'user'", userLine, role)
} }

View File

@@ -26,11 +26,11 @@ func TestParseUsers_Success(t *testing.T) {
}{ }{
{ {
name: "single user", name: "single user",
input: []string{"alice:$2a$10$abcdefghijklmnopqrstuvwxyz:user"}, input: []string{"alice:$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:user"},
expected: []*user.User{ expected: []*user.User{
{ {
Name: "alice", Name: "alice",
Hash: "$2a$10$abcdefghijklmnopqrstuvwxyz", Hash: "$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S",
Role: user.RoleUser, Role: user.RoleUser,
Provisioned: true, Provisioned: true,
}, },
@@ -39,19 +39,19 @@ func TestParseUsers_Success(t *testing.T) {
{ {
name: "multiple users with different roles", name: "multiple users with different roles",
input: []string{ input: []string{
"alice:$2a$10$abcdefghijklmnopqrstuvwxyz:user", "alice:$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:user",
"bob:$2b$10$abcdefghijklmnopqrstuvwxyz:admin", "bob:$2a$10$jIcuBWcbxd6oW1aPvoJ5iOShzu3/UJ2kSxKbTZtDypG06nBflQagq:admin",
}, },
expected: []*user.User{ expected: []*user.User{
{ {
Name: "alice", Name: "alice",
Hash: "$2a$10$abcdefghijklmnopqrstuvwxyz", Hash: "$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S",
Role: user.RoleUser, Role: user.RoleUser,
Provisioned: true, Provisioned: true,
}, },
{ {
Name: "bob", Name: "bob",
Hash: "$2b$10$abcdefghijklmnopqrstuvwxyz", Hash: "$2a$10$jIcuBWcbxd6oW1aPvoJ5iOShzu3/UJ2kSxKbTZtDypG06nBflQagq",
Role: user.RoleAdmin, Role: user.RoleAdmin,
Provisioned: true, Provisioned: true,
}, },
@@ -64,11 +64,11 @@ func TestParseUsers_Success(t *testing.T) {
}, },
{ {
name: "user with special characters in name", name: "user with special characters in name",
input: []string{"alice.test+123@example.com:$2y$10$abcdefghijklmnopqrstuvwxyz:user"}, input: []string{"alice.test+123@example.com:$2a$10$RYUYAsl5zOnAIp6fH7BPX.Eug0rUfEUk92r8WiVusb0VK.vGojWBe:user"},
expected: []*user.User{ expected: []*user.User{
{ {
Name: "alice.test+123@example.com", Name: "alice.test+123@example.com",
Hash: "$2y$10$abcdefghijklmnopqrstuvwxyz", Hash: "$2a$10$RYUYAsl5zOnAIp6fH7BPX.Eug0rUfEUk92r8WiVusb0VK.vGojWBe",
Role: user.RoleUser, Role: user.RoleUser,
Provisioned: true, Provisioned: true,
}, },
@@ -110,23 +110,23 @@ func TestParseUsers_Errors(t *testing.T) {
}, },
{ {
name: "invalid username", name: "invalid username",
input: []string{"alice@#$%:$2a$10$abcdefghijklmnopqrstuvwxyz:user"}, input: []string{"alice@#$%:$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:user"},
error: "invalid auth-users: alice@#$%:$2a$10$abcdefghijklmnopqrstuvwxyz:user, username invalid", error: "invalid auth-users: alice@#$%:$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:user, username invalid",
}, },
{ {
name: "invalid password hash - wrong prefix", name: "invalid password hash - wrong prefix",
input: []string{"alice:plaintext:user"}, input: []string{"alice:plaintext:user"},
error: "invalid auth-users: alice:plaintext:user, password hash but be a bcrypt hash, use 'ntfy user hash' to generate", error: "invalid auth-users: alice:plaintext:user, password hash invalid, password hash must be a bcrypt hash, use 'ntfy user hash' to generate",
}, },
{ {
name: "invalid role", name: "invalid role",
input: []string{"alice:$2a$10$abcdefghijklmnopqrstuvwxyz:invalid"}, input: []string{"alice:$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:invalid"},
error: "invalid auth-users: alice:$2a$10$abcdefghijklmnopqrstuvwxyz:invalid, role invalid is not allowed, allowed roles are 'admin' or 'user'", error: "invalid auth-users: alice:$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:invalid, role invalid is not allowed, allowed roles are 'admin' or 'user'",
}, },
{ {
name: "empty username", name: "empty username",
input: []string{":$2a$10$abcdefghijklmnopqrstuvwxyz:user"}, input: []string{":$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:user"},
error: "invalid auth-users: :$2a$10$abcdefghijklmnopqrstuvwxyz:user, username invalid", error: "invalid auth-users: :$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:user, username invalid",
}, },
} }

View File

@@ -60,6 +60,9 @@ func TestCLI_User_Add_Password_Mismatch(t *testing.T) {
func TestCLI_User_ChangePass(t *testing.T) { func TestCLI_User_ChangePass(t *testing.T) {
s, conf, port := newTestServerWithAuth(t) s, conf, port := newTestServerWithAuth(t)
conf.AuthUsers = []*user.User{
{Name: "philuser", Hash: "$2a$10$U4WSIYY6evyGmZaraavM2e2JeVG6EMGUKN1uUwufUeeRd4Jpg6cGC", Role: user.RoleUser}, // philuser:philpass
}
defer test.StopServer(t, s, port) defer test.StopServer(t, s, port)
// Add user // Add user
@@ -73,6 +76,11 @@ func TestCLI_User_ChangePass(t *testing.T) {
stdin.WriteString("newpass\nnewpass") stdin.WriteString("newpass\nnewpass")
require.Nil(t, runUserCommand(app, conf, "change-pass", "phil")) require.Nil(t, runUserCommand(app, conf, "change-pass", "phil"))
require.Contains(t, stdout.String(), "changed password for user phil") require.Contains(t, stdout.String(), "changed password for user phil")
// Cannot change provisioned user's pass
app, stdin, _, _ = newTestApp()
stdin.WriteString("newpass\nnewpass")
require.Error(t, runUserCommand(app, conf, "change-pass", "philuser"))
} }
func TestCLI_User_ChangeRole(t *testing.T) { func TestCLI_User_ChangeRole(t *testing.T) {

View File

@@ -1,4 +1,4 @@
//go:build !noserver //go:build !noserver && !nowebpush
package cmd package cmd

View File

@@ -88,7 +88,7 @@ using Docker Compose (i.e. `docker-compose.yml`):
NTFY_CACHE_FILE: /var/lib/ntfy/cache.db NTFY_CACHE_FILE: /var/lib/ntfy/cache.db
NTFY_AUTH_FILE: /var/lib/ntfy/auth.db NTFY_AUTH_FILE: /var/lib/ntfy/auth.db
NTFY_AUTH_DEFAULT_ACCESS: deny-all NTFY_AUTH_DEFAULT_ACCESS: deny-all
NTFY_AUTH_USERS: 'phil:$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:admin' NTFY_AUTH_USERS: 'phil:$$2a$$10$$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:admin' # Must escape '$' as '$$'
NTFY_BEHIND_PROXY: true NTFY_BEHIND_PROXY: true
NTFY_ATTACHMENT_CACHE_DIR: /var/lib/ntfy/attachments NTFY_ATTACHMENT_CACHE_DIR: /var/lib/ntfy/attachments
NTFY_ENABLE_LOGIN: true NTFY_ENABLE_LOGIN: true
@@ -1013,7 +1013,7 @@ or the root domain:
=== "caddy" === "caddy"
``` ```
# Note that this config is most certainly incomplete. Please help out and let me know what's missing # Note that this config is most certainly incomplete. Please help out and let me know what's missing
# via Discord/Matrix or in a GitHub issue. # via the contact page (https://ntfy.sh/docs/contact/) or in a GitHub issue.
# Note: Caddy automatically handles both HTTP and WebSockets with reverse_proxy # Note: Caddy automatically handles both HTTP and WebSockets with reverse_proxy
ntfy.sh, http://nfty.sh { ntfy.sh, http://nfty.sh {
@@ -1029,6 +1029,36 @@ or the root domain:
redir @httpget https://{host}{uri} redir @httpget https://{host}{uri}
} }
``` ```
=== "ferron"
``` kdl
// /etc/ferron.kdl
// Note that this config is most certainly incomplete. Please help out and let me know what's missing
// via the contact page (https://ntfy.sh/docs/contact/) or in a GitHub issue.
// Note: Ferron automatically handles both HTTP and WebSockets with proxy
ntfy.sh {
auto_tls
auto_tls_letsencrypt_production
protocols "h1" "h2" "h3"
proxy "http://127.0.0.1:2586"
// Redirect HTTP to HTTPS, but only for GET topic addresses, since we want
// it to work with curl without the annoying https:// prefix
no_redirect_to_https #true
condition "is_get_topic" {
is_equal "{method}" "GET"
is_regex "{path}" "^/([-_a-z0-9]{0,64}$|docs/|static/)"
}
if "is_get_topic" {
no_redirect_to_https #false
}
}
```
## Firebase (FCM) ## Firebase (FCM)
!!! info !!! info
@@ -1531,7 +1561,7 @@ See [Installation for Docker](install.md#docker) for an example of how this coul
If configured, ntfy can expose a `/metrics` endpoint for [Prometheus](https://prometheus.io/), which can then be used to If configured, ntfy can expose a `/metrics` endpoint for [Prometheus](https://prometheus.io/), which can then be used to
create dashboards and alerts (e.g. via [Grafana](https://grafana.com/)). create dashboards and alerts (e.g. via [Grafana](https://grafana.com/)).
To configure the metrics endpoint, either set `enable-metrics` and/or set the `listen-metrics-http` option to a dedicated To configure the metrics endpoint, either set `enable-metrics` and/or set the `metrics-listen-http` option to a dedicated
listen address. Metrics may be considered sensitive information, so before you enable them, be sure you know what you are listen address. Metrics may be considered sensitive information, so before you enable them, be sure you know what you are
doing, and/or secure access to the endpoint in your reverse proxy. doing, and/or secure access to the endpoint in your reverse proxy.
@@ -1698,6 +1728,7 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
| `enable-signup` | `NTFY_ENABLE_SIGNUP` | *boolean* (`true` or `false`) | `false` | Allows users to sign up via the web app, or API | | `enable-signup` | `NTFY_ENABLE_SIGNUP` | *boolean* (`true` or `false`) | `false` | Allows users to sign up via the web app, or API |
| `enable-login` | `NTFY_ENABLE_LOGIN` | *boolean* (`true` or `false`) | `false` | Allows users to log in via the web app, or API | | `enable-login` | `NTFY_ENABLE_LOGIN` | *boolean* (`true` or `false`) | `false` | Allows users to log in via the web app, or API |
| `enable-reservations` | `NTFY_ENABLE_RESERVATIONS` | *boolean* (`true` or `false`) | `false` | Allows users to reserve topics (if their tier allows it) | | `enable-reservations` | `NTFY_ENABLE_RESERVATIONS` | *boolean* (`true` or `false`) | `false` | Allows users to reserve topics (if their tier allows it) |
| `require-login` | `NTFY_REQUIRE_LOGIN` | *boolean* (`true` or `false`) | `false` | All actions via the web app require a login |
| `stripe-secret-key` | `NTFY_STRIPE_SECRET_KEY` | *string* | - | Payments: Key used for the Stripe API communication, this enables payments | | `stripe-secret-key` | `NTFY_STRIPE_SECRET_KEY` | *string* | - | Payments: Key used for the Stripe API communication, this enables payments |
| `stripe-webhook-key` | `NTFY_STRIPE_WEBHOOK_KEY` | *string* | - | Payments: Key required to validate the authenticity of incoming webhooks from Stripe | | `stripe-webhook-key` | `NTFY_STRIPE_WEBHOOK_KEY` | *string* | - | Payments: Key required to validate the authenticity of incoming webhooks from Stripe |
| `billing-contact` | `NTFY_BILLING_CONTACT` | *email address* or *website* | - | Payments: Email or website displayed in Upgrade dialog as a billing contact | | `billing-contact` | `NTFY_BILLING_CONTACT` | *email address* or *website* | - | Payments: Email or website displayed in Upgrade dialog as a billing contact |

46
docs/contact.md Normal file
View File

@@ -0,0 +1,46 @@
# Contact
This service is run by [Philipp C. Heckel](https://heckel.io). There are several ways to get in touch with me and the
ntfy community. Please choose the appropriate channel based on your needs.
## Support
### Community support
For general questions, feature discussions, and community help, please use one of these public channels:
| Channel | Link | Description |
|-------------------|--------------------------------------------------------------------------------------|------------------------------------------------------------|
| **Discord** | [discord.gg/cT7ECsZj9w](https://discord.gg/cT7ECsZj9w) | Real-time chat with the community (I'm `binwiederhier`) |
| **Matrix** | [#ntfy:matrix.org](https://matrix.to/#/#ntfy:matrix.org) | Bridged from Discord, same community (I'm `binwiederhier`) |
| **Matrix Space** | [#ntfy-space:matrix.org](https://matrix.to/#/#ntfy-space:matrix.org) | Matrix space with all ntfy rooms |
| **GitHub Issues** | [github.com/binwiederhier/ntfy/issues](https://github.com/binwiederhier/ntfy/issues) | Bug reports and feature requests |
!!! info "Why public channels?"
Answering questions in public channels benefits the entire community. Other users can learn from the
discussion, and answers can be referenced later. This is much more scalable than 1-on-1 support.
### Paid support
If you are subscribed to a [ntfy Pro](https://ntfy.sh/#pricing) plan, you are entitled to priority support
via the following channels:
| Channel | Contact | Description |
|-----------------------|-----------------------------------------------------|------------------------------------------|
| **General Support** | [support@mail.ntfy.sh](mailto:support@mail.ntfy.sh) | Direct email support for Pro subscribers |
| **Billing Inquiries** | [billing@mail.ntfy.sh](mailto:support@mail.ntfy.sh) | Inquire about billing issues |
| **Discord/Matrix** | Mention your Pro status | Priority responses in community channels |
Please include your ntfy.sh username when contacting support so we can verify your subscription status.
## Security issues
If you discover a security vulnerability, please report it responsibly via [security@mail.ntfy.sh](mailto:security@mail.ntfy.sh). See also: [SECURITY.md](https://github.com/binwiederhier/ntfy/blob/main/SECURITY.md).
## Other inquiries
For questions about our [privacy policy](privacy.md), data handling, or to exercise your data rights
(access, deletion, etc.), please email [privacy@mail.ntfy.sh](mailto:privacy@mail.ntfy.sh).
For business inquiries, partnerships, press, or other general questions that don't fit the categories above, please
use [contact@mail.ntfy.sh](mailto:contact@mail.ntfy.sh).

43
docs/contributing.md Normal file
View File

@@ -0,0 +1,43 @@
# Contributing
Thank you for your interest in contributing to ntfy! There are many ways to help, whether you're a developer,
translator, or just an enthusiastic user.
## Code contributions
If you'd like to contribute code to ntfy:
1. Check out the [development guide](develop.md) to set up your environment
2. Look at [open issues](https://github.com/binwiederhier/ntfy/issues) for ideas, or propose your own
3. For larger features or architectural changes, please reach out on [Discord/Matrix](contact.md) first to discuss
before investing significant time
4. Submit a pull request on GitHub
All contributions are welcome, from small bug fixes to major features.
## Translations
Help make ntfy accessible to users around the world! We use Hosted Weblate for translations:
- **Weblate**: [hosted.weblate.org/projects/ntfy](https://hosted.weblate.org/projects/ntfy/)
You can start translating immediately without any coding knowledge.
## Documentation
Found a typo? Want to improve the docs? Documentation contributions are very welcome:
- Edit any page directly on GitHub using the edit button
- Submit a pull request with your improvements
## Bug reports and feature requests
- **GitHub Issues**: [github.com/binwiederhier/ntfy/issues](https://github.com/binwiederhier/ntfy/issues)
Please search existing issues before creating a new one to avoid duplicates.
## Code of Conduct
Please be respectful and constructive in all interactions. See the
[Code of Conduct](https://github.com/binwiederhier/ntfy/blob/main/CODE_OF_CONDUCT.md) for details.

View File

@@ -2,7 +2,7 @@
Hurray 🥳 🎉, you are interested in writing code for ntfy! **That's awesome.** 😎 Hurray 🥳 🎉, you are interested in writing code for ntfy! **That's awesome.** 😎
I tried my very best to write up detailed instructions, but if at any point in time you run into issues, don't I tried my very best to write up detailed instructions, but if at any point in time you run into issues, don't
hesitate to **contact me on [Discord](https://discord.gg/cT7ECsZj9w) or [Matrix](https://matrix.to/#/#ntfy:matrix.org)**. hesitate to reach out via one of the channels listed on the [contact page](contact.md).
## ntfy server ## ntfy server
The ntfy server source code is available [on GitHub](https://github.com/binwiederhier/ntfy). The codebase for the The ntfy server source code is available [on GitHub](https://github.com/binwiederhier/ntfy). The codebase for the

View File

@@ -94,11 +94,11 @@ I would be humbled if you helped me carry the server and developer account costs
appreciated. appreciated.
## Can I email you? Can I DM you on Discord/Matrix? ## Can I email you? Can I DM you on Discord/Matrix?
While I love chatting on [Discord](https://discord.gg/cT7ECsZj9w), [Matrix](https://matrix.to/#/#ntfy-space:matrix.org), For community support, please use the public channels listed on the [contact page](contact.md). I generally
[Lemmy](https://discuss.ntfy.sh/c/ntfy), or [GitHub](https://github.com/binwiederhier/ntfy/issues), I generally **do not respond to direct messages** about ntfy, unless you are paying for a [ntfy Pro](https://ntfy.sh/#pricing)
**do not respond to emails about ntfy or direct messages** about ntfy, unless you are paying for a plan (see [paid support](contact.md#paid-support-ntfy-pro-subscribers)), or you are inquiring about business
[ntfy Pro](https://ntfy.sh/#pricing) plan, or you are inquiring about business opportunities. opportunities (see [general inquiries](contact.md#general-inquiries)).
I am sorry, but answering individual questions about ntfy on a 1-on-1 basis is not scalable. Answering your questions I am sorry, but answering individual questions about ntfy on a 1-on-1 basis is not scalable. Answering your questions
in the above-mentioned forums benefits others, since I can link to the discussion at a later point in time, or other users in public forums benefits others, since I can link to the discussion at a later point in time, or other users
may be able to help out. I hope you understand. may be able to help out. I hope you understand.

View File

@@ -30,50 +30,56 @@ deb/rpm packages.
=== "x86_64/amd64" === "x86_64/amd64"
```bash ```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_amd64.tar.gz wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_amd64.tar.gz
tar zxvf ntfy_2.13.0_linux_amd64.tar.gz tar zxvf ntfy_2.15.0_linux_amd64.tar.gz
sudo cp -a ntfy_2.13.0_linux_amd64/ntfy /usr/local/bin/ntfy sudo cp -a ntfy_2.15.0_linux_amd64/ntfy /usr/local/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.13.0_linux_amd64/{client,server}/*.yml /etc/ntfy sudo mkdir /etc/ntfy && sudo cp ntfy_2.15.0_linux_amd64/{client,server}/*.yml /etc/ntfy
sudo ntfy serve sudo ntfy serve
``` ```
=== "armv6" === "armv6"
```bash ```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_armv6.tar.gz wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_armv6.tar.gz
tar zxvf ntfy_2.13.0_linux_armv6.tar.gz tar zxvf ntfy_2.15.0_linux_armv6.tar.gz
sudo cp -a ntfy_2.13.0_linux_armv6/ntfy /usr/bin/ntfy sudo cp -a ntfy_2.15.0_linux_armv6/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.13.0_linux_armv6/{client,server}/*.yml /etc/ntfy sudo mkdir /etc/ntfy && sudo cp ntfy_2.15.0_linux_armv6/{client,server}/*.yml /etc/ntfy
sudo ntfy serve sudo ntfy serve
``` ```
=== "armv7/armhf" === "armv7/armhf"
```bash ```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_armv7.tar.gz wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_armv7.tar.gz
tar zxvf ntfy_2.13.0_linux_armv7.tar.gz tar zxvf ntfy_2.15.0_linux_armv7.tar.gz
sudo cp -a ntfy_2.13.0_linux_armv7/ntfy /usr/bin/ntfy sudo cp -a ntfy_2.15.0_linux_armv7/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.13.0_linux_armv7/{client,server}/*.yml /etc/ntfy sudo mkdir /etc/ntfy && sudo cp ntfy_2.15.0_linux_armv7/{client,server}/*.yml /etc/ntfy
sudo ntfy serve sudo ntfy serve
``` ```
=== "arm64" === "arm64"
```bash ```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_arm64.tar.gz wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_arm64.tar.gz
tar zxvf ntfy_2.13.0_linux_arm64.tar.gz tar zxvf ntfy_2.15.0_linux_arm64.tar.gz
sudo cp -a ntfy_2.13.0_linux_arm64/ntfy /usr/bin/ntfy sudo cp -a ntfy_2.15.0_linux_arm64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.13.0_linux_arm64/{client,server}/*.yml /etc/ntfy sudo mkdir /etc/ntfy && sudo cp ntfy_2.15.0_linux_arm64/{client,server}/*.yml /etc/ntfy
sudo ntfy serve sudo ntfy serve
``` ```
## Debian/Ubuntu repository ## Debian/Ubuntu repository
Installation via Debian repository:
!!! info
As of September 2025, **the official ntfy.sh Debian/Ubuntu repository has moved to [archive.ntfy.sh](https://archive.ntfy.sh/apt)**.
The old repository [archive.heckel.io](https://archive.heckel.io/apt) is still available for now, but will likely
go away soon. I suspect I will phase it out some time in early 2026.
Installation via Debian/Ubuntu repository (fingerprint `55BA 774A 6F5E E674 31E4 6B7C CFDB 962D 4F1E C4AF`):
=== "x86_64/amd64" === "x86_64/amd64"
```bash ```bash
sudo mkdir -p /etc/apt/keyrings sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://archive.heckel.io/apt/pubkey.txt | sudo gpg --dearmor -o /etc/apt/keyrings/archive.heckel.io.gpg sudo curl -L -o /etc/apt/keyrings/ntfy.gpg https://archive.ntfy.sh/apt/keyring.gpg
sudo apt install apt-transport-https sudo apt install apt-transport-https
sudo sh -c "echo 'deb [arch=amd64 signed-by=/etc/apt/keyrings/archive.heckel.io.gpg] https://archive.heckel.io/apt debian main' \ echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/ntfy.gpg] https://archive.ntfy.sh/apt stable main" \
> /etc/apt/sources.list.d/archive.heckel.io.list" | sudo tee /etc/apt/sources.list.d/ntfy.list
sudo apt update sudo apt update
sudo apt install ntfy sudo apt install ntfy
sudo systemctl enable ntfy sudo systemctl enable ntfy
@@ -83,10 +89,10 @@ Installation via Debian repository:
=== "armv7/armhf" === "armv7/armhf"
```bash ```bash
sudo mkdir -p /etc/apt/keyrings sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://archive.heckel.io/apt/pubkey.txt | sudo gpg --dearmor -o /etc/apt/keyrings/archive.heckel.io.gpg sudo curl -L -o /etc/apt/keyrings/ntfy.gpg https://archive.ntfy.sh/apt/keyring.gpg
sudo apt install apt-transport-https sudo apt install apt-transport-https
sudo sh -c "echo 'deb [arch=armhf signed-by=/etc/apt/keyrings/archive.heckel.io.gpg] https://archive.heckel.io/apt debian main' \ echo "deb [arch=armhf signed-by=/etc/apt/keyrings/ntfy.gpg] https://archive.ntfy.sh/apt stable main" \
> /etc/apt/sources.list.d/archive.heckel.io.list" | sudo tee /etc/apt/sources.list.d/ntfy.list
sudo apt update sudo apt update
sudo apt install ntfy sudo apt install ntfy
sudo systemctl enable ntfy sudo systemctl enable ntfy
@@ -96,10 +102,10 @@ Installation via Debian repository:
=== "arm64" === "arm64"
```bash ```bash
sudo mkdir -p /etc/apt/keyrings sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://archive.heckel.io/apt/pubkey.txt | sudo gpg --dearmor -o /etc/apt/keyrings/archive.heckel.io.gpg sudo curl -L -o /etc/apt/keyrings/ntfy.gpg https://archive.ntfy.sh/apt/keyring.gpg
sudo apt install apt-transport-https sudo apt install apt-transport-https
sudo sh -c "echo 'deb [arch=arm64 signed-by=/etc/apt/keyrings/archive.heckel.io.gpg] https://archive.heckel.io/apt debian main' \ echo "deb [arch=arm64 signed-by=/etc/apt/keyrings/ntfy.gpg] https://archive.ntfy.sh/apt stable main" \
> /etc/apt/sources.list.d/archive.heckel.io.list" | sudo tee /etc/apt/sources.list.d/ntfy.list
sudo apt update sudo apt update
sudo apt install ntfy sudo apt install ntfy
sudo systemctl enable ntfy sudo systemctl enable ntfy
@@ -110,7 +116,7 @@ Manually installing the .deb file:
=== "x86_64/amd64" === "x86_64/amd64"
```bash ```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_amd64.deb wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_amd64.deb
sudo dpkg -i ntfy_*.deb sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy sudo systemctl enable ntfy
sudo systemctl start ntfy sudo systemctl start ntfy
@@ -118,7 +124,7 @@ Manually installing the .deb file:
=== "armv6" === "armv6"
```bash ```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_armv6.deb wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_armv6.deb
sudo dpkg -i ntfy_*.deb sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy sudo systemctl enable ntfy
sudo systemctl start ntfy sudo systemctl start ntfy
@@ -126,7 +132,7 @@ Manually installing the .deb file:
=== "armv7/armhf" === "armv7/armhf"
```bash ```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_armv7.deb wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_armv7.deb
sudo dpkg -i ntfy_*.deb sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy sudo systemctl enable ntfy
sudo systemctl start ntfy sudo systemctl start ntfy
@@ -134,7 +140,7 @@ Manually installing the .deb file:
=== "arm64" === "arm64"
```bash ```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_linux_arm64.deb wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_arm64.deb
sudo dpkg -i ntfy_*.deb sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy sudo systemctl enable ntfy
sudo systemctl start ntfy sudo systemctl start ntfy
@@ -144,28 +150,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.13.0/ntfy_2.13.0_linux_amd64.rpm sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_amd64.rpm
sudo systemctl enable ntfy sudo systemctl 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.13.0/ntfy_2.13.0_linux_armv6.rpm sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_armv6.rpm
sudo systemctl enable ntfy sudo systemctl 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.13.0/ntfy_2.13.0_linux_armv7.rpm sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_armv7.rpm
sudo systemctl enable ntfy sudo systemctl 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.13.0/ntfy_2.13.0_linux_arm64.rpm sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_arm64.rpm
sudo systemctl enable ntfy sudo systemctl enable ntfy
sudo systemctl start ntfy sudo systemctl start ntfy
``` ```
@@ -195,18 +201,18 @@ NixOS also supports [declarative setup of the ntfy server](https://search.nixos.
## macOS ## macOS
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on macOS as well. The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on macOS as well.
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.13.0/ntfy_2.13.0_darwin_all.tar.gz), To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_darwin_all.tar.gz),
extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`). 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.13.0/ntfy_2.13.0_darwin_all.tar.gz > ntfy_2.13.0_darwin_all.tar.gz curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_darwin_all.tar.gz > ntfy_2.15.0_darwin_all.tar.gz
tar zxvf ntfy_2.13.0_darwin_all.tar.gz tar zxvf ntfy_2.15.0_darwin_all.tar.gz
sudo cp -a ntfy_2.13.0_darwin_all/ntfy /usr/local/bin/ntfy sudo cp -a ntfy_2.15.0_darwin_all/ntfy /usr/local/bin/ntfy
mkdir ~/Library/Application\ Support/ntfy mkdir ~/Library/Application\ Support/ntfy
cp ntfy_2.13.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml cp ntfy_2.15.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
ntfy --help ntfy --help
``` ```
@@ -221,10 +227,9 @@ simply run:
brew install ntfy 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.13.0/ntfy_2.13.0_windows_amd64.zip), To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_windows_amd64.zip),
extract it and place the `ntfy.exe` binary somewhere in your `%Path%`. 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).
@@ -301,6 +306,7 @@ services:
retries: 3 retries: 3
start_period: 40s start_period: 40s
restart: unless-stopped restart: unless-stopped
init: true # needed, if healthcheck is used. Prevents zombie processes
``` ```
If using a non-root user when running the docker version, be sure to chown the server.yml, user.db, and cache.db files and attachments directory to the same uid/gid. If using a non-root user when running the docker version, be sure to chown the server.yml, user.db, and cache.db files and attachments directory to the same uid/gid.
@@ -319,7 +325,6 @@ The setup for Kubernetes is very similar to that for Docker, and requires a fair
are a few options to mix and match, including a deployment without a cache file, a stateful set with a persistent cache, and a standalone are a few options to mix and match, including a deployment without a cache file, a stateful set with a persistent cache, and a standalone
unmanned pod. unmanned pod.
=== "deployment" === "deployment"
```yaml ```yaml
apiVersion: apps/v1 apiVersion: apps/v1

View File

@@ -42,6 +42,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had
- [Monibot](https://monibot.io/) - Monibot monitors your websites, servers and applications and notifies you if something goes wrong. - [Monibot](https://monibot.io/) - Monibot monitors your websites, servers and applications and notifies you if something goes wrong.
- [Miniflux](https://miniflux.app/docs/ntfy.html) - Minimalist and opinionated feed reader - [Miniflux](https://miniflux.app/docs/ntfy.html) - Minimalist and opinionated feed reader
- [Beszel](https://beszel.dev/guide/notifications/ntfy) - Server monitoring platform - [Beszel](https://beszel.dev/guide/notifications/ntfy) - Server monitoring platform
- [Simple Observability](https://simpleobservability.com/docs/alerts/ntfy) - Server monitoring and observability platform
## Integration via HTTP/SMTP/etc. ## Integration via HTTP/SMTP/etc.
@@ -175,7 +176,12 @@ I've added a ⭐ to projects or posts that have a significant following, or had
- [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp) - An ntfy MCP server for sending/fetching ntfy notifications to your self-hosted ntfy server from AI Agents (supports secure token auth & more - use with npx or docker!) (Node/Typescript) - [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp) - An ntfy MCP server for sending/fetching ntfy notifications to your self-hosted ntfy server from AI Agents (supports secure token auth & more - use with npx or docker!) (Node/Typescript)
- [InvaderInformant](https://github.com/patricksthannon/InvaderInformant) - Script for Mac OS systems that monitors new or dropped connections to your network using ntfy (Shell) - [InvaderInformant](https://github.com/patricksthannon/InvaderInformant) - Script for Mac OS systems that monitors new or dropped connections to your network using ntfy (Shell)
- [NtfyPwsh](https://github.com/ptmorris1/NtfyPwsh) - PowerShell module to help send messages to ntfy (PowerShell) - [NtfyPwsh](https://github.com/ptmorris1/NtfyPwsh) - PowerShell module to help send messages to ntfy (PowerShell)
- [ntfyrr](https://github.com/leukosaima/ntfyrr) - Currently an Overseerr webhook notification to ntfy helper service. - [ntfyrr](https://github.com/leukosaima/ntfyrr) - Overseerr and Maintainerr webhook notification to ntfy helper service (C#)
- [ntfy for Sandstorm](https://apps.sandstorm.io/app/c6rk81r4qk6dm3k04x1kxmyccqewhh4npuxeyg1xrpfypn2ddy0h) - ntfy app for the Sandstorm platform
- [ntfy-heartbeat-monitor](https://codeberg.org/RockWolf/ntfy-heartbeat-monitor) - Application for implementing heartbeat monitoring/alerting by utilizing ntfy
- [ntfy-bridge](https://github.com/AlexGaudon/ntfy-bridge) - An application to bridge Discord messages (or webhooks) to ntfy.
- [ntailfy](https://github.com/leukosaima/ntailfy) - ntfy notifications when Tailscale devices connect/disconnect (Go)
- [BRun](https://github.com/cbrake/brun) - Native Linux automation platform connecting triggers to actions without containers (Go)
## Blog + forum posts ## Blog + forum posts

View File

@@ -1,12 +1,194 @@
# Privacy policy # Privacy policy
I love free software, and I'm doing this because it's fun. I have no bad intentions, and **I will **Last updated:** January 2, 2026
never monetize or sell your information, and this service and software will always stay free and open.**
Neither the server nor the app record any personal information, or share any of the messages and topics with This privacy policy describes how ntfy ("we", "us", or "our") collects, uses, and handles your information
any outside service. All data is exclusively used to make the service function properly. The only external service when you use the ntfy.sh service, web app, and mobile applications (Android and iOS).
I use is Firebase Cloud Messaging (FCM) service, which is required to provide instant Android notifications (see
[FAQ](faq.md) for details). To avoid FCM altogether, download the F-Droid version.
For debugging purposes, the ntfy server may temporarily log request paths, remote IP addresses or even topics ## Our commitment to privacy
or messages, though typically this is turned off.
We love free software, and we're doing this because it's fun. We have no bad intentions, and **we will
never monetize or sell your information**. The ntfy service and software will always stay free and open source.
If you don't trust us or your messages are sensitive, you can [self-host your own ntfy server](install.md).
## Information we collect
### Account information (optional)
If you create an account on ntfy.sh, we collect:
- **Username** - A unique identifier you choose
- **Password** - Stored as a secure bcrypt hash (we never store your plaintext password)
- **Email address** - Only if you subscribe to a paid plan (for billing purposes)
- **Phone number** - Only if you enable the phone call notification feature (verified via SMS/call)
You can use ntfy without creating an account. Anonymous usage is fully supported.
### Messages and notifications
- **Message content** - Messages you publish are temporarily cached on our servers (default: 12 hours) to support
message polling and to overcome client network disruptions. Messages are deleted after the cache duration expires.
- **Attachments** - File attachments are temporarily stored (default: 3 hours) and then automatically deleted.
- **Topic names** - The topic names you publish to or subscribe to are processed by our servers.
### Technical information
- **IP addresses** - Used for rate limiting to prevent abuse. May be temporarily logged for debugging purposes,
though this is typically turned off.
- **Access tokens** - If you create access tokens, we store the token value, an optional label, last access time,
and the IP address of the last access.
- **Web push subscriptions** - If you enable browser notifications, we store your browser's push subscription
endpoint to deliver notifications.
### Billing information (paid plans only)
If you subscribe to a paid plan, payment processing is handled by Stripe. We store:
- Stripe customer ID
- Subscription status and billing period
We do not store your credit card numbers or payment details directly. These are handled entirely by Stripe.
## Third-party services
To provide the ntfy.sh service, we use the following third-party services:
### Firebase Cloud Messaging (FCM)
We use Google's Firebase Cloud Messaging to deliver push notifications to Android and iOS devices. When you
receive a notification through the mobile apps (Google Play or App Store versions):
- Message metadata and content may be transmitted through Google's FCM infrastructure
- Google's [privacy policy](https://policies.google.com/privacy) applies to their handling of this data
**To avoid FCM entirely:** Download the [F-Droid version](https://f-droid.org/en/packages/io.heckel.ntfy/) of
the Android app and use a self-hosted server, or use the instant delivery feature with your own server.
### Twilio (phone calls)
If you use the phone call notification feature (`X-Call` header), we use Twilio to:
- Make voice calls to your verified phone number
- Send SMS or voice calls for phone number verification
Your phone number is shared with Twilio to deliver these services. Twilio's
[privacy policy](https://www.twilio.com/legal/privacy) applies.
### Amazon SES (email delivery)
If you use the email notification feature (`X-Email` header), we use Amazon Simple Email Service (SES) to
deliver emails. The recipient email address and message content are transmitted through Amazon's infrastructure.
Amazon's [privacy policy](https://aws.amazon.com/privacy/) applies.
### Stripe (payments)
If you subscribe to a paid plan, payments are processed by Stripe. Your payment information is handled directly
by Stripe and is subject to Stripe's [privacy policy](https://stripe.com/privacy).
Note: We have explicitly disabled Stripe's telemetry features in our integration.
### Web push providers
If you enable browser notifications in the ntfy web app, push messages are delivered through your browser
vendor's push service:
- Google (Chrome)
- Mozilla (Firefox)
- Apple (Safari)
- Microsoft (Edge)
Your browser's push subscription endpoint is shared with these providers to deliver notifications.
## Mobile applications
### Android app
The Android app is available from two sources:
- **Google Play Store** - Uses Firebase Cloud Messaging for push notifications. Firebase Analytics is
**explicitly disabled** in our app.
- **F-Droid** - Does not include any Google services or Firebase. Uses a foreground service to maintain
a direct connection to the server.
The Android app stores the following data locally on your device:
- Subscribed topics and their settings
- Cached notifications
- User credentials (if you add a server with authentication)
- Application logs (for debugging, stored locally only)
### iOS app
The iOS app uses Firebase Cloud Messaging (via Apple Push Notification service) to deliver notifications.
The app stores the following data locally on your device:
- Subscribed topics
- Cached notifications
- User credentials (if configured)
## Web application
The ntfy web app is a static website that stores all data locally in your browser:
- **IndexedDB** - Stores your subscriptions and cached notifications
- **Local Storage** - Stores your preferences and session information
No cookies are used for tracking. The web app does not have a backend beyond the ntfy API.
## Data retention
| Data type | Retention period |
|------------------------|---------------------------------------------------|
| Messages | 12 hours (configurable by server operators) |
| Attachments | 3 hours (configurable by server operators) |
| User accounts | Until you delete your account |
| Access tokens | Until you revoke them or delete your account |
| Phone numbers | Until you remove them or delete your account |
| Web push subscriptions | 60 days of inactivity, then automatically removed |
| Server logs | Varies; debugging logs are typically temporary |
## Self-hosting
If you prefer complete control over your data, you can [self-host your own ntfy server](install.md).
When self-hosting:
- You control all data storage and retention
- You can choose whether to use Firebase, Twilio, email delivery, or any other integrations
- No data is shared with ntfy.sh or any third party (unless you configure those integrations)
The server and all apps are fully open source:
- Server: [github.com/binwiederhier/ntfy](https://github.com/binwiederhier/ntfy)
- Android app: [github.com/binwiederhier/ntfy-android](https://github.com/binwiederhier/ntfy-android)
- iOS app: [github.com/binwiederhier/ntfy-ios](https://github.com/binwiederhier/ntfy-ios)
## Data security
- All connections to ntfy.sh are encrypted using TLS/HTTPS
- Passwords are hashed using bcrypt before storage
- Access tokens are generated using cryptographically secure random values
- The server does not log message content by default
## Your rights
You have the right to:
- **Access** - View your account information and data
- **Delete** - Delete your account and associated data via the web app
- **Export** - Your messages are available via the API while cached
To delete your account, use the account settings in the web app or contact us.
## Changes to this policy
We may update this privacy policy from time to time. Changes will be posted on this page with an updated
"Last updated" date. You may also review all changes in the [Git history](https://github.com/binwiederhier/ntfy/commits/main/docs/privacy.md).
For significant changes, we may provide additional notice on Discord/Matrix or through the
[announcements](https://ntfy.sh/announcements) ntfy topic.
## Contact
For privacy-related inquiries, please email [privacy@mail.ntfy.sh](mailto:privacy@mail.ntfy.sh).
For all other contact options, see the [contact page](contact.md).

View File

@@ -625,7 +625,7 @@ them with a comma, e.g. `tag1,tag2,tag3`.
or `=?UTF-8?Q?=C3=84pfel?=,tag2` ([quoted-printable](https://en.wikipedia.org/wiki/Quoted-printable)). or `=?UTF-8?Q?=C3=84pfel?=,tag2` ([quoted-printable](https://en.wikipedia.org/wiki/Quoted-printable)).
## Markdown formatting ## Markdown formatting
_Supported on:_ :material-firefox: _Supported on:_ :material-android: :material-firefox:
You can format messages using [Markdown](https://www.markdownguide.org/basic-syntax/) 🤩. That means you can use You can format messages using [Markdown](https://www.markdownguide.org/basic-syntax/) 🤩. That means you can use
**bold text**, *italicized text*, links, images, and more. Supported Markdown features (web app only for now): **bold text**, *italicized text*, links, images, and more. Supported Markdown features (web app only for now):
@@ -705,8 +705,8 @@ As of today, **Markdown is only supported in the web app.** Here's an example of
=== "Python" === "Python"
``` python ``` python
requests.post("https://ntfy.sh/mytopic", requests.post("https://ntfy.sh/mytopic",
data="Look ma, **bold text**, *italics*, ..." data="Look ma, **bold text**, *italics*, ...",
headers={ "Markdown": "yes" })) headers={ "Markdown": "yes" })
``` ```
=== "PHP" === "PHP"
@@ -1106,7 +1106,7 @@ Which will result in a notification that looks like this:
When `X-Template: yes` (aliases: `Template: yes`, `Tpl: yes`) or `?template=yes` is set, you can use Go templates in the `message` and `title` fields of your When `X-Template: yes` (aliases: `Template: yes`, `Tpl: yes`) or `?template=yes` is set, you can use Go templates in the `message` and `title` fields of your
webhook payload. webhook payload.
Inline templates are most useful for templated one-off messages, of if you do not control the ntfy server (e.g., if you're using ntfy.sh). Inline templates are most useful for templated one-off messages, or if you do not control the ntfy server (e.g., if you're using ntfy.sh).
Consider using [pre-defined templates](#pre-defined-templates) or [custom templates](#custom-templates) instead, Consider using [pre-defined templates](#pre-defined-templates) or [custom templates](#custom-templates) instead,
if you control the ntfy server, as templates are much easier to maintain. if you control the ntfy server, as templates are much easier to maintain.
@@ -3679,13 +3679,13 @@ authParam = base64_raw(authHeader) // -> QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM0
The following command will generate the appropriate value for you on *nix systems: The following command will generate the appropriate value for you on *nix systems:
``` ```
echo -n "Basic `echo -n 'testuser:fakepassword' | base64`" | base64 | tr -d '=' echo -n "Basic `echo -n 'testuser:fakepassword' | base64 -w0`" | base64 -w0 | tr -d '='
``` ```
For access tokens, you can use this instead: For access tokens, you can use this instead:
``` ```
echo -n "Bearer faketoken" | base64 | tr -d '=' echo -n "Bearer faketoken" | base64 -w0 | tr -d '='
``` ```
## Advanced features ## Advanced features

View File

@@ -2,7 +2,127 @@
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.13.0 ## Current stable releases
| Component | Version | Release date |
|------------------------------------------|---------|--------------|
| ntfy server | v2.15.0 | Nov 16, 2025 |
| ntfy Android app (_is being rolled out_) | v1.20.0 | Dec 28, 2025 |
| ntfy iOS app | v1.3 | Nov 26, 2023 |
Please check out the release notes for [upcoming releases](#not-released-yet) below.
## ntfy Android app v1.20.0
Released December 28, 2025
This is the last pure maintenance release for now. It'll bring all dependencies and library version to the latest version,
and fixes some crashes. I had to drop support for about 4,000 devices (only ~200 installations), because the libraries
themselves do not support SDK 21 anymore, which was the previous minimum SDK version (Android 5, 2014). Now the minimum
SDK version is 26 (Android 8, 2017).
**Bug fixes + maintenance:**
* Updated dependencies, minimum SDK version to 26, clean up legacy code, upgrade Gradle ([ntfy-android#140](https://github.com/binwiederhier/ntfy-android/pull/140),
thanks to [@cyb3rko](https://github.com/cyb3rko) for the implementation)
* Updated target SDK version to 36 (Android 8, 2017)
* Fixed ForegroundServiceDidNotStartInTimeException ([#1520](https://github.com/binwiederhier/ntfy/issues/1520))
* Fixed crashes with redrawing the list when temporarily muted topics expire
## ntfy Android app v1.19.4
Released December 21, 2025
This release upgrades the Android app to use [Material 3](https://m3.material.io/) design components and adds the
ability to use [dynamic colors](https://developer.android.com/develop/ui/views/theming/dynamic-colors).
**This was a lot of work** and I want to thank [@Bnyro](https://github.com/Bnyro) and [@cyb3rko](https://github.com/cyb3rko) for implementing this. You guys rock!
**Features:**
* Moved the user interface to Material 3 and added dynamic color support ([#580](https://github.com/binwiederhier/ntfy/issues/580),
[ntfy-android#56](https://github.com/binwiederhier/ntfy-android/pull/56), [ntfy-android#126](https://github.com/binwiederhier/ntfy-android/pull/126),
[ntfy-android#135](https://github.com/binwiederhier/ntfy-android/pull/135), thanks to [@Bnyro](https://github.com/Bnyro)
and [@cyb3rko](https://github.com/cyb3rko) for the implementation, and to [@RokeJulianLockhart](https://github.com/RokeJulianLockhart) for reporting)
## ntfy Android app v1.18.0
Released December 4, 2025
**Features:**
* Added GIF support for preview images ([ntfy-android#76](https://github.com/binwiederhier/ntfy-android/pull/76)/[#532](https://github.com/binwiederhier/ntfy/issues/532), thanks to [@MichaelArkh](https://github.com/MichaelArkh) and [@dimatx](https://github.com/dimatx) for reporting)
* Added WebP support for preview images ([ntfy-android#81](https://github.com/binwiederhier/ntfy-android/pull/81)/[ntfy-android#80](https://github.com/binwiederhier/ntfy-android/issues/80), thanks to [@jokakilla](https://github.com/jokakilla))
* Added UnifiedPush distributor selection support ([#137](https://github.com/binwiederhier/ntfy-android/pull/137), thanks to [@p1gp1g](https://github.com/p1gp1g))
**Bug fixes + maintenance:**
* Remove REQUEST_INSTALL_PACKAGES permission ([#684](https://github.com/binwiederhier/ntfy/issues/684))
* Request to ignore battery optimizations before receiving subscription ([ntfy-android#97](https://github.com/binwiederhier/ntfy-android/pull/97), thanks to [@p1gp1g](https://github.com/p1gp1g))
## ntfy server v2.15.0
Released Nov 16, 2025
This release adds a `require-login` flag to topics, which forces users to log in before they can
use the web app. This is useful for self-hosters and will obviously not be enabled on ntfy.sh.
**Features:**
* Add `require-login` flag to redirect to login page if not logged in ([#1434](https://github.com/binwiederhier/ntfy/pull/1434)/[#238](https://github.com/binwiederhier/ntfy/issues/238)/[#1329](https://github.com/binwiederhier/ntfy/pull/1329), thanks to [@theatischbein](https://github.com/theatischbein) for implementing most of this)
**Bug fixes + maintenance:**
* The official ntfy.sh Debian/Ubuntu repository has moved to [archive.ntfy.sh](https://archive.ntfy.sh) ([#1357](https://github.com/binwiederhier/ntfy/issues/1357)/[#1401](https://github.com/binwiederhier/ntfy/issues/1401), thanks to [@skibbipl](https://github.com/skibbipl) and [@lduesing](https://github.com/lduesing) for reporting)
* Add mutex around message cache writes to avoid `database locked` errors ([#1397](https://github.com/binwiederhier/ntfy/pull/1397), [#1391](https://github.com/binwiederhier/ntfy/issues/1391), thanks to [@timofej673](https://github.com/timofej673))
* Add build tags `nopayments`, `nofirebase` and `nowebpush` to allow excluding external dependencies, useful for
packaging in Debian ([#1420](https://github.com/binwiederhier/ntfy/pull/1420), discussion in [#1258](https://github.com/binwiederhier/ntfy/issues/1258), thanks to [@thekhalifa](https://github.com/thekhalifa) for packaging ntfy for Debian/Ubuntu)
* Make copying tokens, phone numbers, etc. possible on HTTP ([#1432](https://github.com/binwiederhier/ntfy/pull/1432)/[#1408](https://github.com/binwiederhier/ntfy/issues/1408)/[#1295](https://github.com/binwiederhier/ntfy/issues/1295), thanks to [@EdwinKM](https://github.com/EdwinKM), [@xxl6097](https://github.com/xxl6097) for reporting)
## ntfy Android app v1.17.13
Released October 21, 2025
This release makes changes to comply with the Google Play policies. See [#1463](https://github.com/binwiederhier/ntfy/issues/1463)
or [ef57cd1](https://github.com/binwiederhier/ntfy-android/commit/ef57cd1374118b3e4d7a7ab496afe337e714fff7) for details.
The policies do not allow directly or indirectly linking to paid plans or donation links that do not go through Google Play.
**Changes:**
* Remove the "Donate" button from menu (all variants)
* Change default display name from "ntfy.sh/mytopic" to "mytopic" (all variants)
* Remove links to ntfy docs and issue tracker (Play variant only)
* Remove how-to links to ntfy.sh in a few places (Play variant only)
* Remove "Copy topic address" from subscription menu (Play variant only)
## ntfy Android app v1.17.8
Released September 23, 2025
This is largely a maintenance update to ensure the SDK is up-to-date.
**Features:**
* Markdown is now rendered if "Markdown: yes" was passed ([#310](https://github.com/binwiederhier/ntfy/issues/310), thanks to [@NiNiyas](https://github.com/NiNiyas) for reporting)
* You can now disable UnifiedPush so ntfy does not act as a UnifiedPush distributor ([#646](https://github.com/binwiederhier/ntfy/issues/646), thanks to [@ollien](https://github.com/ollien) for reporting and to [@wunter8](https://github.com/wunter8) for implementing)
**Bug fixes + maintenance:**
* UnifiedPush subscriptions now include the `Rate-Topics` header to facilitate subscriber-based billing ([#652](https://github.com/binwiederhier/ntfy/issues/652), thanks to [@wunter8](https://github.com/wunter8))
* Subscriptions without icons no longer appear to use another subscription's icon ([#634](https://github.com/binwiederhier/ntfy/issues/634), thanks to [@topcaser](https://github.com/topcaser) for reporting and to [@wunter8](https://github.com/wunter8) for fixing)
* Bumped all dependencies to the latest versions (no ticket)
## ntfy server v2.14.0
Released August 5, 2025
This release adds support for [declarative users](config.md#users-via-the-config), [declarative ACL entries](config.md#acl-entries-via-the-config) and [declarative tokens](config.md#tokens-via-the-config). This allows you to define users, ACL entries and tokens in the config file, which is useful for static deployments or deployments that use a configuration management system.
It also adds support for [pre-defined templates](publish.md#pre-defined-templates) and [custom templates](publish.md#custom-templates) for enhanced JSON webhook support, as well as advanced [template functions](publish.md#template-functions) based on the [Sprig](https://github.com/Masterminds/sprig) functions.
❤️ If you like ntfy, **please consider sponsoring me** via [GitHub Sponsors](https://github.com/sponsors/binwiederhier), [Liberapay](https://en.liberapay.com/ntfy/), Bitcoin (`1626wjrw3uWk9adyjCfYwafw4sQWujyjn8`), or by buying a [paid plan via the web app](https://ntfy.sh/app). ntfy
will always remain open source.
**Features:**
* [Declarative users](config.md#users-via-the-config), [declarative ACL entries](config.md#acl-entries-via-the-config) and [declarative tokens](config.md#tokens-via-the-config) ([#464](https://github.com/binwiederhier/ntfy/issues/464), [#1384](https://github.com/binwiederhier/ntfy/pull/1384), [#1413](https://github.com/binwiederhier/ntfy/pull/1413), thanks to [pinpox](https://github.com/pinpox) for reporting, to [@wunter8](https://github.com/wunter8) for reviewing and implementing parts of it)
* [Pre-defined templates](publish.md#pre-defined-templates) and [custom templates](publish.md#custom-templates) for enhanced JSON webhook support ([#1390](https://github.com/binwiederhier/ntfy/pull/1390))
* Support of advanced [template functions](publish.md#template-functions) based on the [Sprig](https://github.com/Masterminds/sprig) library ([#1121](https://github.com/binwiederhier/ntfy/issues/1121), thanks to [@davidatkinsondoyle](https://github.com/davidatkinsondoyle) for reporting, to [@wunter8](https://github.com/wunter8) for implementing, and to the Sprig team for their work)
## ntfy server v2.13.0
Released July 10, 2025 Released July 10, 2025
This is a relatively small release, mainly to support IPv6 and to add more sophisticated This is a relatively small release, mainly to support IPv6 and to add more sophisticated
@@ -21,7 +141,7 @@ ntfy will always remain open source.
* Update new languages from Weblate. Thanks to all the contributors! * Update new languages from Weblate. Thanks to all the contributors!
* Added Estonian (Esti), Galician (Galego), Romanian (Română), Slovak (Slovenčina) as new languages to the web app * Added Estonian (Esti), Galician (Galego), Romanian (Română), Slovak (Slovenčina) as new languages to the web app
### ntfy server v2.12.0 ## ntfy server v2.12.0
Released May 29, 2025 Released May 29, 2025
This is mainly a maintenance release that updates dependencies, though since it's been over a year, there are a few This is mainly a maintenance release that updates dependencies, though since it's been over a year, there are a few
@@ -81,7 +201,7 @@ user support in Discord/Matrix/GitHub! You rock, man!
* Update new languages from Weblate. Thanks to all the contributors! * Update new languages from Weblate. Thanks to all the contributors!
* Added Tamil (தமிழ்) as a new language to the web app * Added Tamil (தமிழ்) as a new language to the web app
### ntfy server v2.11.0 ## ntfy server v2.11.0
Released May 13, 2024 Released May 13, 2024
This is a tiny release that fixes a database index issue that caused performance issues on ntfy.sh. It also fixes a bug This is a tiny release that fixes a database index issue that caused performance issues on ntfy.sh. It also fixes a bug
@@ -96,7 +216,7 @@ and [Liberapay](https://en.liberapay.com/ntfy/), or buying a [paid plan via the
* Do not set rate visitor for non-eligible topics (no ticket) * Do not set rate visitor for non-eligible topics (no ticket)
* Do not cache `config.js` ([#1098](https://github.com/binwiederhier/ntfy/pull/1098), thanks to [@wunter8](https://github.com/wunter8)) * Do not cache `config.js` ([#1098](https://github.com/binwiederhier/ntfy/pull/1098), thanks to [@wunter8](https://github.com/wunter8))
### ntfy server v2.10.0 ## ntfy server v2.10.0
Released Mar 27, 2024 Released Mar 27, 2024
This release adds support for **message templating** in the ntfy server, which allows you to include a message and/or This release adds support for **message templating** in the ntfy server, which allows you to include a message and/or
@@ -107,7 +227,7 @@ This is great for services that let you specify a webhook URL but do not let you
* [Message templating](publish.md#message-templating): You can now include a message and/or title template that will be filled with values from a JSON body ([#724](https://github.com/binwiederhier/ntfy/issues/724), thanks to [@wunter8](https://github.com/wunter8) for implementing) * [Message templating](publish.md#message-templating): You can now include a message and/or title template that will be filled with values from a JSON body ([#724](https://github.com/binwiederhier/ntfy/issues/724), thanks to [@wunter8](https://github.com/wunter8) for implementing)
### ntfy server v2.9.0 ## ntfy server v2.9.0
Released Mar 7, 2024 Released Mar 7, 2024
A small release after a long pause (lots of day job work). This release adds for **larger messages** and **longer A small release after a long pause (lots of day job work). This release adds for **larger messages** and **longer
@@ -1452,26 +1572,21 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
## Not released yet ## Not released yet
### ntfy server v2.14.0 (UNRELEASED) ### ntfy Android app v1.21.1-rc1 (IN TESTING)
**Features:** **Features:**
* [Declarative users](config.md#users-via-the-config), [declarative ACL entries](config.md#acl-entries-via-the-config) and [declarative tokens](config.md#tokens-via-the-config) ([#464](https://github.com/binwiederhier/ntfy/issues/464), [#1384](https://github.com/binwiederhier/ntfy/pull/1384), thanks to [pinpox](https://github.com/pinpox) for reporting, to [@wunter8](https://github.com/wunter8) for reviewing) * Allow publishing messages through the message bar and publish dialog ([#98](https://github.com/binwiederhier/ntfy/issues/98), [ntfy-android#144](https://github.com/binwiederhier/ntfy-android/pull/144))
* [Pre-defined templates](publish.md#pre-defined-templates) and [custom templates](publish.md#custom-templates) for enhanced JSON webhook support ([#1390](https://github.com/binwiederhier/ntfy/pull/1390)) * Define custom HTTP headers to support authenticated proxies, tunnels and SSO ([ntfy-android#116](https://github.com/binwiederhier/ntfy-android/issues/116), [#1018](https://github.com/binwiederhier/ntfy/issues/1018), [ntfy-android#132](https://github.com/binwiederhier/ntfy-android/pull/132), [ntfy-android#146](https://github.com/binwiederhier/ntfy-android/pull/146), thanks to [@CrazyWolf13](https://github.com/CrazyWolf13))
* Support of advanced [template functions](publish.md#template-functions) based on the [Sprig](https://github.com/Masterminds/sprig) library ([#1121](https://github.com/binwiederhier/ntfy/issues/1121), thanks to [@davidatkinsondoyle](https://github.com/davidatkinsondoyle) for reporting, to [@wunter8](https://github.com/wunter8) for implementing, and to the Sprig team for their work) * Implement UnifiedPush "raise to foreground" requirement ([ntfy-android#98](https://github.com/binwiederhier/ntfy-android/pull/98), [ntfy-android#148](https://github.com/binwiederhier/ntfy-android/pull/148), thanks to [@p1gp1g](https://github.com/p1gp1g))
* Language selector to allow overriding the system language ([#1508](https://github.com/binwiederhier/ntfy/issues/1508), [ntfy-android#145](https://github.com/binwiederhier/ntfy-android/pull/145), thanks to [@hudsonm62](https://github.com/hudsonm62) for reporting)
### ntfy Android app v1.16.1 (UNRELEASED) * Highlight phone numbers and email addresses in notifications ([#957](https://github.com/binwiederhier/ntfy/issues/957), [ntfy-android#71](https://github.com/binwiederhier/ntfy-android/pull/71), thanks to [@brennenputh](https://github.com/brennenputh), and [@XylenSky](https://github.com/XylenSky) for reporting)
* Support for port and display name in [ntfy://](subscribe/phone.md#ntfy-links) links ([ntfy-android#130](https://github.com/binwiederhier/ntfy-android/pull/130), thanks to [@godovski](https://github.com/godovski))
**Features:**
* You can now disable UnifiedPush so ntfy does not act as a UnifiedPush distributor ([#646](https://github.com/binwiederhier/ntfy/issues/646), thanks to [@ollien](https://github.com/ollien) for reporting and to [@wunter8](https://github.com/wunter8) for implementing)
**Bug fixes + maintenance:** **Bug fixes + maintenance:**
* UnifiedPush subscriptions now include the `Rate-Topics` header to facilitate subscriber-based billing ([#652](https://github.com/binwiederhier/ntfy/issues/652), thanks to [@wunter8](https://github.com/wunter8)) * Add support for (technically incorrect) 'image/jpg' MIME type ([ntfy-android#142](https://github.com/binwiederhier/ntfy-android/pull/142), thanks to [@Murilobeluco](https://github.com/Murilobeluco))
* Subscriptions without icons no longer appear to use another subscription's icon ([#634](https://github.com/binwiederhier/ntfy/issues/634), thanks to [@topcaser](https://github.com/topcaser) for reporting and to [@wunter8](https://github.com/wunter8) for fixing) * Unify "copy to clipboard" notifications, use Android 13 style ([ntfy-android#61](https://github.com/binwiederhier/ntfy-android/pull/61), thanks to [@thgoebel](https://github.com/thgoebel))
* Bumped all dependencies to the latest versions (no ticket) * Fix crash in user add dialog (onAddUser)
* Fix ForegroundServiceDidNotStartInTimeException (attempt 2, see [#1520](https://github.com/binwiederhier/ntfy/issues/1520))
**Additional languages:** * Hide "Exact alarms" setting if battery optimization exemption has been granted ([#1456](https://github.com/binwiederhier/ntfy/issues/1456), thanks for reporting [@HappyLer](https://github.com/HappyLer))
* Swedish (thanks to [@hellbown](https://hosted.weblate.org/user/hellbown/))

View File

@@ -295,7 +295,7 @@ Available filters (all case-insensitive):
| `message` | `X-Message`, `m` | `ntfy.sh/mytopic/json?message=lalala` | Only return messages that match this exact message string | | `message` | `X-Message`, `m` | `ntfy.sh/mytopic/json?message=lalala` | Only return messages that match this exact message string |
| `title` | `X-Title`, `t` | `ntfy.sh/mytopic/json?title=some+title` | Only return messages that match this exact title string | | `title` | `X-Title`, `t` | `ntfy.sh/mytopic/json?title=some+title` | Only return messages that match this exact title string |
| `priority` | `X-Priority`, `prio`, `p` | `ntfy.sh/mytopic/json?p=high,urgent` | Only return messages that match *any priority listed* (comma-separated) | | `priority` | `X-Priority`, `prio`, `p` | `ntfy.sh/mytopic/json?p=high,urgent` | Only return messages that match *any priority listed* (comma-separated) |
| `tags` | `X-Tags`, `tag`, `ta` | `ntfy.sh/mytopic?/jsontags=error,alert` | Only return messages that match *all listed tags* (comma-separated) | | `tags` | `X-Tags`, `tag`, `ta` | `ntfy.sh/mytopic/json?tags=error,alert` | Only return messages that match *all listed tags* (comma-separated) |
### Subscribe to multiple topics ### Subscribe to multiple topics
It's possible to subscribe to multiple topics in one HTTP call by providing a comma-separated list of topics It's possible to subscribe to multiple topics in one HTTP call by providing a comma-separated list of topics

View File

@@ -8,13 +8,18 @@ contribute, or [build your own](../develop.md).
<a href="https://f-droid.org/en/packages/io.heckel.ntfy/"><img width="170" src="../../static/img/badge-fdroid.png"></a> <a href="https://f-droid.org/en/packages/io.heckel.ntfy/"><img width="170" src="../../static/img/badge-fdroid.png"></a>
<a href="https://apps.apple.com/us/app/ntfy/id1625396347"><img width="150" src="../../static/img/badge-appstore.png"></a> <a href="https://apps.apple.com/us/app/ntfy/id1625396347"><img width="150" src="../../static/img/badge-appstore.png"></a>
You can get the Android app from both [Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy) and You can get the Android app from [Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy),
from [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/). Both are largely identical, with the one exception that [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/), or via the APKs from [GitHub Releases](https://github.com/binwiederhier/ntfy-android/releases).
the F-Droid flavor does not use Firebase. The iOS app can be downloaded from the [App Store](https://apps.apple.com/us/app/ntfy/id1625396347). The Google Play and F-Droid releases are largely identical, with the one exception that the F-Droid flavor does not use Firebase.
The iOS app can be downloaded from the [App Store](https://apps.apple.com/us/app/ntfy/id1625396347).
Alternatively, you may also want to consider using the **[progressive web app (PWA)](pwa.md)** instead of the native app. Alternatively, you may also want to consider using the **[progressive web app (PWA)](pwa.md)** instead of the native app.
The PWA is a website that you can add to your home screen, and it will behave just like a native app. The PWA is a website that you can add to your home screen, and it will behave just like a native app.
If you're downloading the APKs from [GitHub](https://github.com/binwiederhier/ntfy-android/releases), they are signed with
a certificate with the following SHA-256 fingerprint: `6e145d7ae685eff75468e5067e03a6c3645453343e4e181dac8b6b17ff67489d`.
You can also query the DNS TXT records for `ntfy.sh` to find this fingerprint.
## Overview ## Overview
A picture is worth a thousand words. Here are a few screenshots showing what the app looks like. It's all pretty A picture is worth a thousand words. Here are a few screenshots showing what the app looks like. It's all pretty
straight forward. You can add topics and as soon as you add them, you can [publish messages](../publish.md) to them. straight forward. You can add topics and as soon as you add them, you can [publish messages](../publish.md) to them.
@@ -124,10 +129,11 @@ or to simply directly link to a topic from a mobile website.
**Supported link formats:** **Supported link formats:**
| Link format | Example | Description | | Link format | Example | Description |
|-------------------------------------------------------------------------------|-------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |---------------------------------------------------------------------------------|-------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| <span style="white-space: nowrap">`ntfy://<host>/<topic>`</span> | `ntfy://ntfy.sh/mytopic` | Directly opens the Android app detail view for the given topic and server. Subscribes to the topic if not already subscribed. This is equivalent to the web view `https://ntfy.sh/mytopic` (HTTPS!) | | <span style="white-space: nowrap">`ntfy://<host>/<topic>`</span> | `ntfy://ntfy.sh/mytopic` | Directly opens the Android app detail view for the given topic and server. Subscribes to the topic if not already subscribed. This is equivalent to the web view `https://ntfy.sh/mytopic` (HTTPS!) |
| <span style="white-space: nowrap">`ntfy://<host>/<topic>?secure=false`</span> | `ntfy://example.com/mytopic?secure=false` | Same as above, except that this will use HTTP instead of HTTPS as topic URL. This is equivalent to the web view `http://example.com/mytopic` (HTTP!) | | <span style="white-space: nowrap">`ntfy://<host>/<topic>?display=<name>`</span> | `ntfy://ntfy.sh/mytopic?display=My+Topic` | Same as above, but also defines a display name for the topic. |
| <span style="white-space: nowrap">`ntfy://<host>/<topic>?secure=false`</span> | `ntfy://example.com/mytopic?secure=false` | Same as above, except that this will use HTTP instead of HTTPS as topic URL. This is equivalent to the web view `http://example.com/mytopic` (HTTP!) |
## Integrations ## Integrations

View File

@@ -1,7 +1,7 @@
# Troubleshooting # Troubleshooting
This page lists a few suggestions of what to do when things don't work as expected. This is not a complete list. 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) If this page does not help, feel free to reach out via one of the channels listed on the [contact page](contact.md).
and ask there. We're happy to help. We're happy to help.
## ntfy server ## 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 If you host your own ntfy server, and you're having issues with any component, it is always helpful to enable debugging/tracing

106
go.mod
View File

@@ -1,27 +1,27 @@
module heckel.io/ntfy/v2 module heckel.io/ntfy/v2
go 1.24 go 1.24.0
toolchain go1.24.0 toolchain go1.24.5
require ( require (
cloud.google.com/go/firestore v1.18.0 // indirect cloud.google.com/go/firestore v1.20.0 // indirect
cloud.google.com/go/storage v1.56.0 // indirect cloud.google.com/go/storage v1.58.0 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect github.com/BurntSushi/toml v1.6.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/emersion/go-smtp v0.18.0 github.com/emersion/go-smtp v0.18.0
github.com/gabriel-vasile/mimetype v1.4.9 github.com/gabriel-vasile/mimetype v1.4.12
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
github.com/mattn/go-sqlite3 v1.14.30 github.com/mattn/go-sqlite3 v1.14.32
github.com/olebedev/when v1.1.0 github.com/olebedev/when v1.1.0
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.11.1
github.com/urfave/cli/v2 v2.27.7 github.com/urfave/cli/v2 v2.27.7
golang.org/x/crypto v0.40.0 golang.org/x/crypto v0.46.0
golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sync v0.16.0 golang.org/x/sync v0.19.0
golang.org/x/term v0.33.0 golang.org/x/term v0.38.0
golang.org/x/time v0.12.0 golang.org/x/time v0.14.0
google.golang.org/api v0.244.0 google.golang.org/api v0.258.0
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
) )
@@ -30,38 +30,38 @@ replace github.com/emersion/go-smtp => github.com/emersion/go-smtp v0.17.0 // Pi
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.17.0 firebase.google.com/go/v4 v4.18.0
github.com/SherClockHolmes/webpush-go v1.4.0 github.com/SherClockHolmes/webpush-go v1.4.0
github.com/microcosm-cc/bluemonday v1.0.27 github.com/microcosm-cc/bluemonday v1.0.27
github.com/prometheus/client_golang v1.22.0 github.com/prometheus/client_golang v1.23.2
github.com/stripe/stripe-go/v74 v74.30.0 github.com/stripe/stripe-go/v74 v74.30.0
golang.org/x/text v0.27.0 golang.org/x/text v0.32.0
) )
require ( require (
cel.dev/expr v0.24.0 // indirect cel.dev/expr v0.25.1 // indirect
cloud.google.com/go v0.121.4 // indirect cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth v0.16.3 // indirect cloud.google.com/go/auth v0.18.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.7.0 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/iam v1.5.2 // indirect cloud.google.com/go/iam v1.5.3 // indirect
cloud.google.com/go/longrunning v0.6.7 // indirect cloud.google.com/go/longrunning v0.7.0 // indirect
cloud.google.com/go/monitoring v1.24.2 // indirect cloud.google.com/go/monitoring v1.24.3 // indirect
github.com/AlekSi/pointer v1.2.0 // indirect github.com/AlekSi/pointer v1.2.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect
github.com/MicahParks/keyfunc v1.9.0 // indirect github.com/MicahParks/keyfunc v1.9.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.1 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
@@ -69,36 +69,36 @@ require (
github.com/golang/protobuf v1.5.4 // indirect github.com/golang/protobuf v1.5.4 // indirect
github.com/google/s2a-go v0.1.9 // indirect github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/googleapis/gax-go/v2 v2.16.0 // indirect
github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/css v1.0.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.65.0 // indirect github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.17.0 // indirect github.com/prometheus/procfs v0.19.2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/objx v0.5.2 // indirect
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
github.com/zeebo/errs v1.4.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.37.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect go.opentelemetry.io/otel/sdk v1.39.0 // indirect
go.opentelemetry.io/otel/sdk v1.37.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/net v0.42.0 // indirect golang.org/x/net v0.48.0 // indirect
golang.org/x/sys v0.34.0 // indirect golang.org/x/sys v0.39.0 // indirect
google.golang.org/appengine/v2 v2.0.6 // indirect google.golang.org/appengine/v2 v2.0.6 // indirect
google.golang.org/genproto v0.0.0-20250728155136-f173205681a0 // indirect google.golang.org/genproto v0.0.0-20251213004720-97cd9d5aeac2 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
google.golang.org/grpc v1.74.2 // indirect google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

230
go.sum
View File

@@ -1,41 +1,41 @@
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go v0.121.4 h1:cVvUiY0sX0xwyxPwdSU2KsF9knOVmtRyAMt8xou0iTs= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.121.4/go.mod h1:XEBchUiHFJbz4lKBZwYBDHV/rSyfFktk737TLDU089s= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.16.3 h1:kabzoQ9/bobUmnseYnBO6qQG7q4a/CffFRlJSxv2wCc= cloud.google.com/go/auth v0.18.0 h1:wnqy5hrv7p3k7cShwAU/Br3nzod7fxoqG+k0VZ+/Pk0=
cloud.google.com/go/auth v0.16.3/go.mod h1:NucRGjaXfzP1ltpcQ7On/VTZ0H4kWB5Jy+Y9Dnm76fA= cloud.google.com/go/auth v0.18.0/go.mod h1:wwkPM1AgE1f2u6dG443MiWoD8C3BtOywNsUMcUTVDRo=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/firestore v1.18.0 h1:cuydCaLS7Vl2SatAeivXyhbhDEIR8BDmtn4egDhIn2s= cloud.google.com/go/firestore v1.20.0 h1:JLlT12QP0fM2SJirKVyu2spBCO8leElaW0OOtPm6HEo=
cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU= cloud.google.com/go/firestore v1.20.0/go.mod h1:jqu4yKdBmDN5srneWzx3HlKrHFWFdlkgjgQ6BKIOFQo=
cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY=
cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw=
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E=
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY=
cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
cloud.google.com/go/storage v1.56.0 h1:iixmq2Fse2tqxMbWhLWC9HfBj1qdxqAmiK8/eqtsLxI= cloud.google.com/go/storage v1.58.0 h1:PflFXlmFJjG/nBeR9B7pKddLQWaFaRWx4uUi/LyNxxo=
cloud.google.com/go/storage v1.56.0/go.mod h1:Tpuj6t4NweCLzlNbw9Z9iwxEkrSem20AetIeH/shgVU= cloud.google.com/go/storage v1.58.0/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI=
cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
firebase.google.com/go/v4 v4.17.0 h1:Bih69QV/k0YKPA1qUX04ln0aPT9IERrAo2ezibcngzE= firebase.google.com/go/v4 v4.18.0 h1:S+g0P72oDGqOaG4wlLErX3zQmU9plVdu7j+Bc3R1qFw=
firebase.google.com/go/v4 v4.17.0/go.mod h1:aAPJq/bOyb23tBlc1K6GR+2E8sOGAeJSc8wIJVgl9SM= firebase.google.com/go/v4 v4.18.0/go.mod h1:P7UfBpzc8+Z3MckX79+zsWzKVfpGryr6HLbAe7gCWfs=
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 v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0/go.mod h1:jUZ5LYlw40WMd07qxcQJD5M40aUxrfwqQX1g7zxYnrQ= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
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/SherClockHolmes/webpush-go v1.4.0 h1:ocnzNKWN23T9nvHi6IfyrQjkIc0oJWv1B1pULsf9i3s= github.com/SherClockHolmes/webpush-go v1.4.0 h1:ocnzNKWN23T9nvHi6IfyrQjkIc0oJWv1B1pULsf9i3s=
@@ -46,8 +46,8 @@ 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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -58,20 +58,20 @@ github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.17.0 h1:tq90evlrcyqRfE6DSXaWVH54oX6OuZOQECEmhWBMEtI= github.com/emersion/go-smtp v0.17.0 h1:tq90evlrcyqRfE6DSXaWVH54oX6OuZOQECEmhWBMEtI=
github.com/emersion/go-smtp v0.17.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= github.com/emersion/go-smtp v0.17.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM=
github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs=
github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -96,10 +96,10 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
@@ -112,8 +112,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mattn/go-sqlite3 v1.14.30 h1:bVreufq3EAIG1Quvws73du3/QgdeZ3myglJlrzSYYCY= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.30/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
@@ -127,26 +127,26 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1
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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stripe/stripe-go/v74 v74.30.0 h1:0Kf0KkeFnY7iRhOwvTerX0Ia1BRw+eV1CVJ51mGYAUY= github.com/stripe/stripe-go/v74 v74.30.0 h1:0Kf0KkeFnY7iRhOwvTerX0Ia1BRw+eV1CVJ51mGYAUY=
github.com/stripe/stripe-go/v74 v74.30.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw= github.com/stripe/stripe-go/v74 v74.30.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw=
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
@@ -154,38 +154,38 @@ github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AO
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg= github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=
go.opentelemetry.io/contrib/detectors/gcp v1.37.0 h1:B+WbN9RPsvobe6q4vP6KgM8/9plR/HNjgGBrfcOlweA= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 h1:RN3ifU8y4prNWeEnQp2kRRHz8UwonAEYZl8tUzHEXAk=
go.opentelemetry.io/contrib/detectors/gcp v1.37.0/go.mod h1:K5zQ3TT7p2ru9Qkzk0bKtCql0RGkPj9pRjpXgZJZ+rU= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0/go.mod h1:habDz3tEWiFANTo6oUE99EmaFUrCNYAAg3wiVmusm70=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 h1:rbRJ8BBoVMsQShESYZ0FkvcITu8X8QNwJogcLUmDNNw= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@@ -200,10 +200,10 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
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-20220722155255-886fb9371eb4/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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -211,8 +211,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
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-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=
@@ -225,8 +225,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
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=
@@ -236,8 +236,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
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.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=
@@ -249,10 +249,10 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
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=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 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/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
@@ -261,22 +261,24 @@ golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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=
google.golang.org/api v0.244.0 h1:lpkP8wVibSKr++NCD36XzTk/IzeKJ3klj7vbj+XU5pE= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
google.golang.org/api v0.244.0/go.mod h1:dMVhVcylamkirHdzEBAIQWUCgqY885ivNeZYd7VAVr8= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.258.0 h1:IKo1j5FBlN74fe5isA2PVozN3Y5pwNKriEgAXPOkDAc=
google.golang.org/api v0.258.0/go.mod h1:qhOMTQEZ6lUps63ZNq9jhODswwjkjYYguA7fA3TBFww=
google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw= google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw=
google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI= google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI=
google.golang.org/genproto v0.0.0-20250728155136-f173205681a0 h1:btBcgujH2+KIWEfz0s7Cdtt9R7hpwM4SAEXAdXf/ddw= google.golang.org/genproto v0.0.0-20251213004720-97cd9d5aeac2 h1:stRtB2UVzFOWnorVuwF0BVVEjQ3AN6SjHWdg811UIQM=
google.golang.org/genproto v0.0.0-20250728155136-f173205681a0/go.mod h1:Q4yZQ3kmmIyg6HsMjCGx2vQ8gzN+dntaPmFWz6Zj0fo= google.golang.org/genproto v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 h1:0UOBWO4dC+e51ui0NFKSPbkHHiQ4TmrEfEZMLDyRmY8= google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU=
google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0/go.mod h1:8ytArBbtOy2xfht+y2fqKd5DRDJRUQhqbyEnQ4bDChs= google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 h1:MAKi5q709QWfnkkpNQ0M12hYJ1+e8qYVDyowc4U1XZM= google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
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=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
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/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@@ -99,6 +99,8 @@ nav:
- "Known issues": known-issues.md - "Known issues": known-issues.md
- "Deprecation notices": deprecations.md - "Deprecation notices": deprecations.md
- "Development": develop.md - "Development": develop.md
- "Contributing": contributing.md
- "Privacy policy": privacy.md - "Privacy policy": privacy.md
- "Contact": contact.md

21
payments/payments.go Normal file
View File

@@ -0,0 +1,21 @@
//go:build !nopayments
package payments
import "github.com/stripe/stripe-go/v74"
// Available is a constant used to indicate that Stripe support is available.
// It can be disabled with the 'nopayments' build tag.
const Available = true
// SubscriptionStatus is an alias for stripe.SubscriptionStatus
type SubscriptionStatus stripe.SubscriptionStatus
// PriceRecurringInterval is an alias for stripe.PriceRecurringInterval
type PriceRecurringInterval stripe.PriceRecurringInterval
// Setup sets the Stripe secret key and disables telemetry
func Setup(stripeSecretKey string) {
stripe.EnableTelemetry = false // Whoa!
stripe.Key = stripeSecretKey
}

View File

@@ -0,0 +1,18 @@
//go:build nopayments
package payments
// Available is a constant used to indicate that Stripe support is available.
// It can be disabled with the 'nopayments' build tag.
const Available = false
// SubscriptionStatus is a dummy type
type SubscriptionStatus string
// PriceRecurringInterval is dummy type
type PriceRecurringInterval string
// Setup is a dummy type
func Setup(stripeSecretKey string) {
// Nothing to see here
}

View File

@@ -162,6 +162,7 @@ type Config struct {
BillingContact string BillingContact string
EnableSignup bool // Enable creation of accounts via API and UI EnableSignup bool // Enable creation of accounts via API and UI
EnableLogin bool EnableLogin bool
RequireLogin bool
EnableReservations bool // Allow users with role "user" to own/reserve topics EnableReservations bool // Allow users with role "user" to own/reserve topics
EnableMetrics bool EnableMetrics bool
AccessControlAllowOrigin string // CORS header field to restrict access from web clients AccessControlAllowOrigin string // CORS header field to restrict access from web clients
@@ -256,6 +257,7 @@ func NewConfig() *Config {
EnableSignup: false, EnableSignup: false,
EnableLogin: false, EnableLogin: false,
EnableReservations: false, EnableReservations: false,
RequireLogin: false,
AccessControlAllowOrigin: "*", AccessControlAllowOrigin: "*",
Version: "", Version: "",
WebPushPrivateKey: "", WebPushPrivateKey: "",

View File

@@ -133,6 +133,8 @@ var (
errHTTPConflictTopicReserved = &errHTTP{40902, http.StatusConflict, "conflict: access control entry for topic or topic pattern already exists", "", nil} errHTTPConflictTopicReserved = &errHTTP{40902, http.StatusConflict, "conflict: access control entry for topic or topic pattern already exists", "", nil}
errHTTPConflictSubscriptionExists = &errHTTP{40903, http.StatusConflict, "conflict: topic subscription already exists", "", nil} errHTTPConflictSubscriptionExists = &errHTTP{40903, http.StatusConflict, "conflict: topic subscription already exists", "", nil}
errHTTPConflictPhoneNumberExists = &errHTTP{40904, http.StatusConflict, "conflict: phone number already exists", "", nil} errHTTPConflictPhoneNumberExists = &errHTTP{40904, http.StatusConflict, "conflict: phone number already exists", "", nil}
errHTTPConflictProvisionedUserChange = &errHTTP{40905, http.StatusConflict, "conflict: cannot change or delete provisioned user", "", nil}
errHTTPConflictProvisionedTokenChange = &errHTTP{40906, http.StatusConflict, "conflict: cannot change or delete provisioned token", "", nil}
errHTTPGonePhoneVerificationExpired = &errHTTP{41001, http.StatusGone, "phone number verification expired or does not exist", "", nil} errHTTPGonePhoneVerificationExpired = &errHTTP{41001, http.StatusGone, "phone number verification expired or does not exist", "", nil}
errHTTPEntityTooLargeAttachment = &errHTTP{41301, http.StatusRequestEntityTooLarge, "attachment too large, or bandwidth limit reached", "https://ntfy.sh/docs/publish/#limitations", nil} errHTTPEntityTooLargeAttachment = &errHTTP{41301, http.StatusRequestEntityTooLarge, "attachment too large, or bandwidth limit reached", "https://ntfy.sh/docs/publish/#limitations", nil}
errHTTPEntityTooLargeMatrixRequest = &errHTTP{41302, http.StatusRequestEntityTooLarge, "Matrix request is larger than the max allowed length", "", nil} errHTTPEntityTooLargeMatrixRequest = &errHTTP{41302, http.StatusRequestEntityTooLarge, "Matrix request is larger than the max allowed length", "", nil}

View File

@@ -8,6 +8,7 @@ import (
"net/netip" "net/netip"
"path/filepath" "path/filepath"
"strings" "strings"
"sync"
"time" "time"
_ "github.com/mattn/go-sqlite3" // SQLite driver _ "github.com/mattn/go-sqlite3" // SQLite driver
@@ -38,7 +39,7 @@ const (
priority INT NOT NULL, priority INT NOT NULL,
tags TEXT NOT NULL, tags TEXT NOT NULL,
click TEXT NOT NULL, click TEXT NOT NULL,
icon TEXT NOT NULL, icon TEXT NOT NULL,
actions TEXT NOT NULL, actions TEXT NOT NULL,
attachment_name TEXT NOT NULL, attachment_name TEXT NOT NULL,
attachment_type TEXT NOT NULL, attachment_type TEXT NOT NULL,
@@ -76,32 +77,32 @@ const (
deleteMessageQuery = `DELETE FROM messages WHERE mid = ?` deleteMessageQuery = `DELETE FROM messages WHERE mid = ?`
updateMessagesForTopicExpiryQuery = `UPDATE messages SET expires = ? WHERE topic = ?` updateMessagesForTopicExpiryQuery = `UPDATE messages SET expires = ? WHERE topic = ?`
selectRowIDFromMessageID = `SELECT id FROM messages WHERE mid = ?` // Do not include topic, see #336 and TestServer_PollSinceID_MultipleTopics selectRowIDFromMessageID = `SELECT id FROM messages WHERE mid = ?` // Do not include topic, see #336 and TestServer_PollSinceID_MultipleTopics
selectMessagesByIDQuery = ` selectMessagesByIDQuery = `
SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
FROM messages FROM messages
WHERE mid = ? WHERE mid = ?
` `
selectMessagesSinceTimeQuery = ` selectMessagesSinceTimeQuery = `
SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
FROM messages FROM messages
WHERE topic = ? AND time >= ? AND published = 1 WHERE topic = ? AND time >= ? AND published = 1
ORDER BY mtime, id ORDER BY mtime, id
` `
selectMessagesSinceTimeIncludeScheduledQuery = ` selectMessagesSinceTimeIncludeScheduledQuery = `
SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
FROM messages FROM messages
WHERE topic = ? AND time >= ? WHERE topic = ? AND time >= ?
ORDER BY mtime, id ORDER BY mtime, id
` `
selectMessagesSinceIDQuery = ` selectMessagesSinceIDQuery = `
SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
FROM messages FROM messages
WHERE topic = ? AND id > ? AND published = 1 WHERE topic = ? AND id > ? AND published = 1
ORDER BY mtime, id ORDER BY mtime, id
` `
selectMessagesSinceIDIncludeScheduledQuery = ` selectMessagesSinceIDIncludeScheduledQuery = `
SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
FROM messages FROM messages
WHERE topic = ? AND (id > ? OR published = 0) WHERE topic = ? AND (id > ? OR published = 0)
ORDER BY mtime, id ORDER BY mtime, id
` `
@@ -111,10 +112,10 @@ const (
WHERE topic = ? AND published = 1 WHERE topic = ? AND published = 1
ORDER BY time DESC, id DESC ORDER BY time DESC, id DESC
LIMIT 1 LIMIT 1
` `
selectMessagesDueQuery = ` selectMessagesDueQuery = `
SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted SELECT mid, sid, time, mtime, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding, deleted
FROM messages FROM messages
WHERE time <= ? AND published = 0 WHERE time <= ? AND published = 0
ORDER BY mtime, id ORDER BY mtime, id
` `
@@ -299,6 +300,7 @@ type messageCache struct {
db *sql.DB db *sql.DB
queue *util.BatchingQueue[*message] queue *util.BatchingQueue[*message]
nop bool nop bool
mu sync.Mutex
} }
// newSqliteCache creates a SQLite file-backed cache // newSqliteCache creates a SQLite file-backed cache
@@ -363,6 +365,8 @@ func (c *messageCache) AddMessage(m *message) error {
// addMessages synchronously stores a match of messages. If the database is locked, the transaction waits until // addMessages synchronously stores a match of messages. If the database is locked, the transaction waits until
// SQLite's busy_timeout is exceeded before erroring out. // SQLite's busy_timeout is exceeded before erroring out.
func (c *messageCache) addMessages(ms []*message) error { func (c *messageCache) addMessages(ms []*message) error {
c.mu.Lock()
defer c.mu.Unlock()
if c.nop { if c.nop {
return nil return nil
} }
@@ -547,6 +551,8 @@ func (c *messageCache) Message(id string) (*message, error) {
} }
func (c *messageCache) MarkPublished(m *message) error { func (c *messageCache) MarkPublished(m *message) error {
c.mu.Lock()
defer c.mu.Unlock()
_, err := c.db.Exec(updateMessagePublishedQuery, m.ID) _, err := c.db.Exec(updateMessagePublishedQuery, m.ID)
return err return err
} }
@@ -592,6 +598,8 @@ func (c *messageCache) Topics() (map[string]*topic, error) {
} }
func (c *messageCache) DeleteMessages(ids ...string) error { func (c *messageCache) DeleteMessages(ids ...string) error {
c.mu.Lock()
defer c.mu.Unlock()
tx, err := c.db.Begin() tx, err := c.db.Begin()
if err != nil { if err != nil {
return err return err
@@ -606,6 +614,8 @@ func (c *messageCache) DeleteMessages(ids ...string) error {
} }
func (c *messageCache) ExpireMessages(topics ...string) error { func (c *messageCache) ExpireMessages(topics ...string) error {
c.mu.Lock()
defer c.mu.Unlock()
tx, err := c.db.Begin() tx, err := c.db.Begin()
if err != nil { if err != nil {
return err return err
@@ -640,6 +650,8 @@ func (c *messageCache) AttachmentsExpired() ([]string, error) {
} }
func (c *messageCache) MarkAttachmentsDeleted(ids ...string) error { func (c *messageCache) MarkAttachmentsDeleted(ids ...string) error {
c.mu.Lock()
defer c.mu.Unlock()
tx, err := c.db.Begin() tx, err := c.db.Begin()
if err != nil { if err != nil {
return err return err
@@ -791,6 +803,8 @@ func readMessage(rows *sql.Rows) (*message, error) {
} }
func (c *messageCache) UpdateStats(messages int64) error { func (c *messageCache) UpdateStats(messages int64) error {
c.mu.Lock()
defer c.mu.Unlock()
_, err := c.db.Exec(updateStatsQuery, messages) _, err := c.db.Exec(updateStatsQuery, messages)
return err return err
} }

View File

@@ -3,8 +3,10 @@ package server
import ( import (
"database/sql" "database/sql"
"fmt" "fmt"
"github.com/stretchr/testify/assert"
"net/netip" "net/netip"
"path/filepath" "path/filepath"
"sync"
"testing" "testing"
"time" "time"
@@ -92,6 +94,26 @@ func testCacheMessages(t *testing.T, c *messageCache) {
require.Empty(t, messages) require.Empty(t, messages)
} }
func TestSqliteCache_MessagesLock(t *testing.T) {
testCacheMessagesLock(t, newSqliteTestCache(t))
}
func TestMemCache_MessagesLock(t *testing.T) {
testCacheMessagesLock(t, newMemTestCache(t))
}
func testCacheMessagesLock(t *testing.T, c *messageCache) {
var wg sync.WaitGroup
for i := 0; i < 5000; i++ {
wg.Add(1)
go func() {
assert.Nil(t, c.AddMessage(newDefaultMessage("mytopic", "test message")))
wg.Done()
}()
}
wg.Wait()
}
func TestSqliteCache_MessagesScheduled(t *testing.T) { func TestSqliteCache_MessagesScheduled(t *testing.T) {
testCacheMessagesScheduled(t, newSqliteTestCache(t)) testCacheMessagesScheduled(t, newSqliteTestCache(t))
} }

View File

@@ -9,7 +9,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"gopkg.in/yaml.v2"
"io" "io"
"net" "net"
"net/http" "net/http"
@@ -32,7 +31,9 @@ import (
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"gopkg.in/yaml.v2"
"heckel.io/ntfy/v2/log" "heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/payments"
"heckel.io/ntfy/v2/user" "heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util" "heckel.io/ntfy/v2/util"
"heckel.io/ntfy/v2/util/sprig" "heckel.io/ntfy/v2/util/sprig"
@@ -167,7 +168,7 @@ func New(conf *Config) (*Server, error) {
mailer = &smtpSender{config: conf} mailer = &smtpSender{config: conf}
} }
var stripe stripeAPI var stripe stripeAPI
if conf.StripeSecretKey != "" { if payments.Available && conf.StripeSecretKey != "" {
stripe = newStripeAPI() stripe = newStripeAPI()
} }
messageCache, err := createMessageCache(conf) messageCache, err := createMessageCache(conf)
@@ -601,6 +602,7 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi
BaseURL: "", // Will translate to window.location.origin BaseURL: "", // Will translate to window.location.origin
AppRoot: s.config.WebRoot, AppRoot: s.config.WebRoot,
EnableLogin: s.config.EnableLogin, EnableLogin: s.config.EnableLogin,
RequireLogin: s.config.RequireLogin,
EnableSignup: s.config.EnableSignup, EnableSignup: s.config.EnableSignup,
EnablePayments: s.config.StripeSecretKey != "", EnablePayments: s.config.StripeSecretKey != "",
EnableCalls: s.config.TwilioAccount != "", EnableCalls: s.config.TwilioAccount != "",

View File

@@ -258,9 +258,11 @@
# #
# - enable-signup allows users to sign up via the web app, or API # - enable-signup allows users to sign up via the web app, or API
# - enable-login allows users to log in via the web app, or API # - enable-login allows users to log in via the web app, or API
# - require-login redirects users to the login page if they are not logged in (disallows web app access without login)
# - enable-reservations allows users to reserve topics (if their tier allows it) # - enable-reservations allows users to reserve topics (if their tier allows it)
# #
# enable-signup: false # enable-signup: false
# require-login: false
# enable-login: false # enable-login: false
# enable-reservations: false # enable-reservations: false

View File

@@ -85,6 +85,7 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis
response.Username = u.Name response.Username = u.Name
response.Role = string(u.Role) response.Role = string(u.Role)
response.SyncTopic = u.SyncTopic response.SyncTopic = u.SyncTopic
response.Provisioned = u.Provisioned
if u.Prefs != nil { if u.Prefs != nil {
if u.Prefs.Language != nil { if u.Prefs.Language != nil {
response.Language = *u.Prefs.Language response.Language = *u.Prefs.Language
@@ -139,11 +140,12 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis
lastOrigin = t.LastOrigin.String() lastOrigin = t.LastOrigin.String()
} }
response.Tokens = append(response.Tokens, &apiAccountTokenResponse{ response.Tokens = append(response.Tokens, &apiAccountTokenResponse{
Token: t.Value, Token: t.Value,
Label: t.Label, Label: t.Label,
LastAccess: t.LastAccess.Unix(), LastAccess: t.LastAccess.Unix(),
LastOrigin: lastOrigin, LastOrigin: lastOrigin,
Expires: t.Expires.Unix(), Expires: t.Expires.Unix(),
Provisioned: t.Provisioned,
}) })
} }
} }
@@ -174,6 +176,12 @@ func (s *Server) handleAccountDelete(w http.ResponseWriter, r *http.Request, v *
if _, err := s.userManager.Authenticate(u.Name, req.Password); err != nil { if _, err := s.userManager.Authenticate(u.Name, req.Password); err != nil {
return errHTTPBadRequestIncorrectPasswordConfirmation return errHTTPBadRequestIncorrectPasswordConfirmation
} }
if err := s.userManager.CanChangeUser(u.Name); err != nil {
if errors.Is(err, user.ErrProvisionedUserChange) {
return errHTTPConflictProvisionedUserChange
}
return err
}
if s.webPush != nil && u.ID != "" { if s.webPush != nil && u.ID != "" {
if err := s.webPush.RemoveSubscriptionsByUserID(u.ID); err != nil { if err := s.webPush.RemoveSubscriptionsByUserID(u.ID); err != nil {
logvr(v, r).Err(err).Warn("Error removing web push subscriptions for %s", u.Name) logvr(v, r).Err(err).Warn("Error removing web push subscriptions for %s", u.Name)
@@ -208,6 +216,9 @@ func (s *Server) handleAccountPasswordChange(w http.ResponseWriter, r *http.Requ
} }
logvr(v, r).Tag(tagAccount).Debug("Changing password for user %s", u.Name) logvr(v, r).Tag(tagAccount).Debug("Changing password for user %s", u.Name)
if err := s.userManager.ChangePassword(u.Name, req.NewPassword, false); err != nil { if err := s.userManager.ChangePassword(u.Name, req.NewPassword, false); err != nil {
if errors.Is(err, user.ErrProvisionedUserChange) {
return errHTTPConflictProvisionedUserChange
}
return err return err
} }
return s.writeJSON(w, newSuccessResponse()) return s.writeJSON(w, newSuccessResponse())
@@ -274,6 +285,9 @@ func (s *Server) handleAccountTokenUpdate(w http.ResponseWriter, r *http.Request
Debug("Updating token for user %s as deleted", u.Name) Debug("Updating token for user %s as deleted", u.Name)
token, err := s.userManager.ChangeToken(u.ID, req.Token, req.Label, expires) token, err := s.userManager.ChangeToken(u.ID, req.Token, req.Label, expires)
if err != nil { if err != nil {
if errors.Is(err, user.ErrProvisionedTokenChange) {
return errHTTPConflictProvisionedTokenChange
}
return err return err
} }
response := &apiAccountTokenResponse{ response := &apiAccountTokenResponse{
@@ -296,6 +310,9 @@ func (s *Server) handleAccountTokenDelete(w http.ResponseWriter, r *http.Request
} }
} }
if err := s.userManager.RemoveToken(u.ID, token); err != nil { if err := s.userManager.RemoveToken(u.ID, token); err != nil {
if errors.Is(err, user.ErrProvisionedTokenChange) {
return errHTTPConflictProvisionedTokenChange
}
return err return err
} }
logvr(v, r). logvr(v, r).

View File

@@ -251,7 +251,11 @@ func TestAccount_Subscription_AddUpdateDelete(t *testing.T) {
} }
func TestAccount_ChangePassword(t *testing.T) { func TestAccount_ChangePassword(t *testing.T) {
s := newTestServer(t, newTestConfigWithAuthFile(t)) conf := newTestConfigWithAuthFile(t)
conf.AuthUsers = []*user.User{
{Name: "philuser", Hash: "$2a$10$U4WSIYY6evyGmZaraavM2e2JeVG6EMGUKN1uUwufUeeRd4Jpg6cGC", Role: user.RoleUser}, // philuser:philpass
}
s := newTestServer(t, conf)
defer s.closeDatabases() defer s.closeDatabases()
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
@@ -281,6 +285,12 @@ func TestAccount_ChangePassword(t *testing.T) {
"Authorization": util.BasicAuth("phil", "new password"), "Authorization": util.BasicAuth("phil", "new password"),
}) })
require.Equal(t, 200, rr.Code) require.Equal(t, 200, rr.Code)
// Cannot change password of provisioned user
rr = request(t, s, "POST", "/v1/account/password", `{"password": "philpass", "new_password": "new password"}`, map[string]string{
"Authorization": util.BasicAuth("philuser", "philpass"),
})
require.Equal(t, 409, rr.Code)
} }
func TestAccount_ChangePassword_NoAccount(t *testing.T) { func TestAccount_ChangePassword_NoAccount(t *testing.T) {

View File

@@ -1,3 +1,5 @@
//go:build !nofirebase
package server package server
import ( import (
@@ -14,6 +16,10 @@ import (
) )
const ( const (
// FirebaseAvailable is a constant used to indicate that Firebase support is available.
// It can be disabled with the 'nofirebase' build tag.
FirebaseAvailable = true
fcmMessageLimit = 4000 fcmMessageLimit = 4000
fcmApnsBodyMessageLimit = 100 fcmApnsBodyMessageLimit = 100
) )
@@ -73,8 +79,8 @@ type firebaseSenderImpl struct {
client *messaging.Client client *messaging.Client
} }
func newFirebaseSender(credentialsFile string) (*firebaseSenderImpl, error) { func newFirebaseSender(credentialsFile string) (firebaseSender, error) {
fb, err := firebase.NewApp(context.Background(), nil, option.WithCredentialsFile(credentialsFile)) fb, err := firebase.NewApp(context.Background(), nil, option.WithAuthCredentialsFile(option.ServiceAccount, credentialsFile))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -0,0 +1,38 @@
//go:build nofirebase
package server
import (
"errors"
"heckel.io/ntfy/v2/user"
)
const (
// FirebaseAvailable is a constant used to indicate that Firebase support is available.
// It can be disabled with the 'nofirebase' build tag.
FirebaseAvailable = false
)
var (
errFirebaseNotAvailable = errors.New("Firebase not available")
errFirebaseTemporarilyBanned = errors.New("visitor temporarily banned from using Firebase")
)
type firebaseClient struct {
}
func (c *firebaseClient) Send(v *visitor, m *message) error {
return errFirebaseNotAvailable
}
type firebaseSender interface {
Send(m string) error
}
func newFirebaseClient(sender firebaseSender, auther user.Auther) *firebaseClient {
return nil
}
func newFirebaseSender(credentialsFile string) (firebaseSender, error) {
return nil, errFirebaseNotAvailable
}

View File

@@ -1,3 +1,5 @@
//go:build !nofirebase
package server package server
import ( import (

View File

@@ -1,3 +1,5 @@
//go:build !nopayments
package server package server
import ( import (
@@ -12,6 +14,7 @@ import (
"github.com/stripe/stripe-go/v74/subscription" "github.com/stripe/stripe-go/v74/subscription"
"github.com/stripe/stripe-go/v74/webhook" "github.com/stripe/stripe-go/v74/webhook"
"heckel.io/ntfy/v2/log" "heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/payments"
"heckel.io/ntfy/v2/user" "heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util" "heckel.io/ntfy/v2/util"
"io" "io"
@@ -22,7 +25,7 @@ import (
// Payments in ntfy are done via Stripe. // Payments in ntfy are done via Stripe.
// //
// Pretty much all payments related things are in this file. The following processes // Pretty much all payments-related things are in this file. The following processes
// handle payments: // handle payments:
// //
// - Checkout: // - Checkout:
@@ -464,8 +467,8 @@ func (s *Server) updateSubscriptionAndTier(r *http.Request, v *visitor, u *user.
billing := &user.Billing{ billing := &user.Billing{
StripeCustomerID: customerID, StripeCustomerID: customerID,
StripeSubscriptionID: subscriptionID, StripeSubscriptionID: subscriptionID,
StripeSubscriptionStatus: stripe.SubscriptionStatus(status), StripeSubscriptionStatus: payments.SubscriptionStatus(status),
StripeSubscriptionInterval: stripe.PriceRecurringInterval(interval), StripeSubscriptionInterval: payments.PriceRecurringInterval(interval),
StripeSubscriptionPaidUntil: time.Unix(paidUntil, 0), StripeSubscriptionPaidUntil: time.Unix(paidUntil, 0),
StripeSubscriptionCancelAt: time.Unix(cancelAt, 0), StripeSubscriptionCancelAt: time.Unix(cancelAt, 0),
} }

View File

@@ -0,0 +1,47 @@
//go:build nopayments
package server
import (
"net/http"
)
type stripeAPI interface {
CancelSubscription(id string) (string, error)
}
func newStripeAPI() stripeAPI {
return nil
}
func (s *Server) fetchStripePrices() (map[string]int64, error) {
return nil, errHTTPNotFound
}
func (s *Server) handleBillingTiersGet(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
return errHTTPNotFound
}
func (s *Server) handleAccountBillingSubscriptionCreate(w http.ResponseWriter, r *http.Request, v *visitor) error {
return errHTTPNotFound
}
func (s *Server) handleAccountBillingSubscriptionCreateSuccess(w http.ResponseWriter, r *http.Request, v *visitor) error {
return errHTTPNotFound
}
func (s *Server) handleAccountBillingSubscriptionUpdate(w http.ResponseWriter, r *http.Request, v *visitor) error {
return errHTTPNotFound
}
func (s *Server) handleAccountBillingSubscriptionDelete(w http.ResponseWriter, r *http.Request, v *visitor) error {
return errHTTPNotFound
}
func (s *Server) handleAccountBillingPortalSessionCreate(w http.ResponseWriter, r *http.Request, v *visitor) error {
return errHTTPNotFound
}
func (s *Server) handleAccountBillingWebhook(_ http.ResponseWriter, r *http.Request, v *visitor) error {
return errHTTPNotFound
}

View File

@@ -1,3 +1,5 @@
//go:build !nopayments
package server package server
import ( import (
@@ -6,6 +8,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stripe/stripe-go/v74" "github.com/stripe/stripe-go/v74"
"golang.org/x/time/rate" "golang.org/x/time/rate"
"heckel.io/ntfy/v2/payments"
"heckel.io/ntfy/v2/user" "heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util" "heckel.io/ntfy/v2/util"
"io" "io"
@@ -345,8 +348,8 @@ func TestPayments_Checkout_Success_And_Increase_Rate_Limits_Reset_Visitor(t *tes
require.Nil(t, u.Tier) require.Nil(t, u.Tier)
require.Equal(t, "", u.Billing.StripeCustomerID) require.Equal(t, "", u.Billing.StripeCustomerID)
require.Equal(t, "", u.Billing.StripeSubscriptionID) require.Equal(t, "", u.Billing.StripeSubscriptionID)
require.Equal(t, stripe.SubscriptionStatus(""), u.Billing.StripeSubscriptionStatus) require.Equal(t, payments.SubscriptionStatus(""), u.Billing.StripeSubscriptionStatus)
require.Equal(t, stripe.PriceRecurringInterval(""), u.Billing.StripeSubscriptionInterval) require.Equal(t, payments.PriceRecurringInterval(""), u.Billing.StripeSubscriptionInterval)
require.Equal(t, int64(0), u.Billing.StripeSubscriptionPaidUntil.Unix()) require.Equal(t, int64(0), u.Billing.StripeSubscriptionPaidUntil.Unix())
require.Equal(t, int64(0), u.Billing.StripeSubscriptionCancelAt.Unix()) require.Equal(t, int64(0), u.Billing.StripeSubscriptionCancelAt.Unix())
require.Equal(t, int64(0), u.Stats.Messages) // Messages and emails are not persisted for no-tier users! require.Equal(t, int64(0), u.Stats.Messages) // Messages and emails are not persisted for no-tier users!
@@ -362,8 +365,8 @@ func TestPayments_Checkout_Success_And_Increase_Rate_Limits_Reset_Visitor(t *tes
require.Equal(t, "starter", u.Tier.Code) // Not "pro" require.Equal(t, "starter", u.Tier.Code) // Not "pro"
require.Equal(t, "acct_5555", u.Billing.StripeCustomerID) require.Equal(t, "acct_5555", u.Billing.StripeCustomerID)
require.Equal(t, "sub_1234", u.Billing.StripeSubscriptionID) require.Equal(t, "sub_1234", u.Billing.StripeSubscriptionID)
require.Equal(t, stripe.SubscriptionStatusActive, u.Billing.StripeSubscriptionStatus) require.Equal(t, payments.SubscriptionStatus(stripe.SubscriptionStatusActive), u.Billing.StripeSubscriptionStatus)
require.Equal(t, stripe.PriceRecurringIntervalMonth, u.Billing.StripeSubscriptionInterval) require.Equal(t, payments.PriceRecurringInterval(stripe.PriceRecurringIntervalMonth), u.Billing.StripeSubscriptionInterval)
require.Equal(t, int64(123456789), u.Billing.StripeSubscriptionPaidUntil.Unix()) require.Equal(t, int64(123456789), u.Billing.StripeSubscriptionPaidUntil.Unix())
require.Equal(t, int64(0), u.Billing.StripeSubscriptionCancelAt.Unix()) require.Equal(t, int64(0), u.Billing.StripeSubscriptionCancelAt.Unix())
require.Equal(t, int64(0), u.Stats.Messages) require.Equal(t, int64(0), u.Stats.Messages)
@@ -473,8 +476,8 @@ func TestPayments_Webhook_Subscription_Updated_Downgrade_From_PastDue_To_Active(
billing := &user.Billing{ billing := &user.Billing{
StripeCustomerID: "acct_5555", StripeCustomerID: "acct_5555",
StripeSubscriptionID: "sub_1234", StripeSubscriptionID: "sub_1234",
StripeSubscriptionStatus: stripe.SubscriptionStatusPastDue, StripeSubscriptionStatus: payments.SubscriptionStatus(stripe.SubscriptionStatusPastDue),
StripeSubscriptionInterval: stripe.PriceRecurringIntervalMonth, StripeSubscriptionInterval: payments.PriceRecurringInterval(stripe.PriceRecurringIntervalMonth),
StripeSubscriptionPaidUntil: time.Unix(123, 0), StripeSubscriptionPaidUntil: time.Unix(123, 0),
StripeSubscriptionCancelAt: time.Unix(456, 0), StripeSubscriptionCancelAt: time.Unix(456, 0),
} }
@@ -517,10 +520,10 @@ func TestPayments_Webhook_Subscription_Updated_Downgrade_From_PastDue_To_Active(
require.Equal(t, "starter", u.Tier.Code) // Not "pro" require.Equal(t, "starter", u.Tier.Code) // Not "pro"
require.Equal(t, "acct_5555", u.Billing.StripeCustomerID) require.Equal(t, "acct_5555", u.Billing.StripeCustomerID)
require.Equal(t, "sub_1234", u.Billing.StripeSubscriptionID) require.Equal(t, "sub_1234", u.Billing.StripeSubscriptionID)
require.Equal(t, stripe.SubscriptionStatusActive, u.Billing.StripeSubscriptionStatus) // Not "past_due" require.Equal(t, payments.SubscriptionStatus(stripe.SubscriptionStatusActive), u.Billing.StripeSubscriptionStatus) // Not "past_due"
require.Equal(t, stripe.PriceRecurringIntervalYear, u.Billing.StripeSubscriptionInterval) // Not "month" require.Equal(t, payments.PriceRecurringInterval(stripe.PriceRecurringIntervalYear), u.Billing.StripeSubscriptionInterval) // Not "month"
require.Equal(t, int64(1674268231), u.Billing.StripeSubscriptionPaidUntil.Unix()) // Updated require.Equal(t, int64(1674268231), u.Billing.StripeSubscriptionPaidUntil.Unix()) // Updated
require.Equal(t, int64(1674299999), u.Billing.StripeSubscriptionCancelAt.Unix()) // Updated require.Equal(t, int64(1674299999), u.Billing.StripeSubscriptionCancelAt.Unix()) // Updated
// Verify that reservations were deleted // Verify that reservations were deleted
r, err := s.userManager.Reservations("phil") r, err := s.userManager.Reservations("phil")
@@ -580,8 +583,8 @@ func TestPayments_Webhook_Subscription_Deleted(t *testing.T) {
require.Nil(t, s.userManager.ChangeBilling(u.Name, &user.Billing{ require.Nil(t, s.userManager.ChangeBilling(u.Name, &user.Billing{
StripeCustomerID: "acct_5555", StripeCustomerID: "acct_5555",
StripeSubscriptionID: "sub_1234", StripeSubscriptionID: "sub_1234",
StripeSubscriptionStatus: stripe.SubscriptionStatusPastDue, StripeSubscriptionStatus: payments.SubscriptionStatus(stripe.SubscriptionStatusPastDue),
StripeSubscriptionInterval: stripe.PriceRecurringIntervalMonth, StripeSubscriptionInterval: payments.PriceRecurringInterval(stripe.PriceRecurringIntervalMonth),
StripeSubscriptionPaidUntil: time.Unix(123, 0), StripeSubscriptionPaidUntil: time.Unix(123, 0),
StripeSubscriptionCancelAt: time.Unix(0, 0), StripeSubscriptionCancelAt: time.Unix(0, 0),
})) }))
@@ -598,7 +601,7 @@ func TestPayments_Webhook_Subscription_Deleted(t *testing.T) {
require.Nil(t, u.Tier) require.Nil(t, u.Tier)
require.Equal(t, "acct_5555", u.Billing.StripeCustomerID) require.Equal(t, "acct_5555", u.Billing.StripeCustomerID)
require.Equal(t, "", u.Billing.StripeSubscriptionID) require.Equal(t, "", u.Billing.StripeSubscriptionID)
require.Equal(t, stripe.SubscriptionStatus(""), u.Billing.StripeSubscriptionStatus) require.Equal(t, payments.SubscriptionStatus(""), u.Billing.StripeSubscriptionStatus)
require.Equal(t, int64(0), u.Billing.StripeSubscriptionPaidUntil.Unix()) require.Equal(t, int64(0), u.Billing.StripeSubscriptionPaidUntil.Unix())
require.Equal(t, int64(0), u.Billing.StripeSubscriptionCancelAt.Unix()) require.Equal(t, int64(0), u.Billing.StripeSubscriptionCancelAt.Unix())

View File

@@ -23,7 +23,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/SherClockHolmes/webpush-go"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"heckel.io/ntfy/v2/log" "heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/util" "heckel.io/ntfy/v2/util"
@@ -281,30 +280,6 @@ func TestServer_WebEnabled(t *testing.T) {
rr = request(t, s2, "GET", "/app.html", "", nil) rr = request(t, s2, "GET", "/app.html", "", nil)
require.Equal(t, 200, rr.Code) require.Equal(t, 200, rr.Code)
} }
func TestServer_WebPushEnabled(t *testing.T) {
conf := newTestConfig(t)
conf.WebRoot = "" // Disable web app
s := newTestServer(t, conf)
rr := request(t, s, "GET", "/manifest.webmanifest", "", nil)
require.Equal(t, 404, rr.Code)
conf2 := newTestConfig(t)
s2 := newTestServer(t, conf2)
rr = request(t, s2, "GET", "/manifest.webmanifest", "", nil)
require.Equal(t, 404, rr.Code)
conf3 := newTestConfigWithWebPush(t)
s3 := newTestServer(t, conf3)
rr = request(t, s3, "GET", "/manifest.webmanifest", "", nil)
require.Equal(t, 200, rr.Code)
require.Equal(t, "application/manifest+json", rr.Header().Get("Content-Type"))
}
func TestServer_PublishLargeMessage(t *testing.T) { func TestServer_PublishLargeMessage(t *testing.T) {
c := newTestConfig(t) c := newTestConfig(t)
c.AttachmentCacheDir = "" // Disable attachments c.AttachmentCacheDir = "" // Disable attachments
@@ -3325,17 +3300,6 @@ func newTestConfigWithAuthFile(t *testing.T) *Config {
return conf return conf
} }
func newTestConfigWithWebPush(t *testing.T) *Config {
conf := newTestConfig(t)
privateKey, publicKey, err := webpush.GenerateVAPIDKeys()
require.Nil(t, err)
conf.WebPushFile = filepath.Join(t.TempDir(), "webpush.db")
conf.WebPushEmailAddress = "testing@example.com"
conf.WebPushPrivateKey = privateKey
conf.WebPushPublicKey = publicKey
return conf
}
func newTestServer(t *testing.T, config *Config) *Server { func newTestServer(t *testing.T, config *Config) *Server {
server, err := New(config) server, err := New(config)
require.Nil(t, err) require.Nil(t, err)

View File

@@ -1,3 +1,5 @@
//go:build !nowebpush
package server package server
import ( import (
@@ -13,6 +15,10 @@ import (
) )
const ( const (
// WebPushAvailable is a constant used to indicate that WebPush support is available.
// It can be disabled with the 'nowebpush' build tag.
WebPushAvailable = true
webPushTopicSubscribeLimit = 50 webPushTopicSubscribeLimit = 50
) )

View File

@@ -0,0 +1,29 @@
//go:build nowebpush
package server
import (
"net/http"
)
const (
// WebPushAvailable is a constant used to indicate that WebPush support is available.
// It can be disabled with the 'nowebpush' build tag.
WebPushAvailable = false
)
func (s *Server) handleWebPushUpdate(w http.ResponseWriter, r *http.Request, v *visitor) error {
return errHTTPNotFound
}
func (s *Server) handleWebPushDelete(w http.ResponseWriter, r *http.Request, _ *visitor) error {
return errHTTPNotFound
}
func (s *Server) publishToWebPushEndpoints(v *visitor, m *message) {
// Nothing to see here
}
func (s *Server) pruneAndNotifyWebPushSubscriptions() {
// Nothing to see here
}

View File

@@ -1,8 +1,11 @@
//go:build !nowebpush
package server package server
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/SherClockHolmes/webpush-go"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"heckel.io/ntfy/v2/user" "heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util" "heckel.io/ntfy/v2/util"
@@ -10,6 +13,7 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/netip" "net/netip"
"path/filepath"
"strings" "strings"
"sync/atomic" "sync/atomic"
"testing" "testing"
@@ -20,6 +24,28 @@ const (
testWebPushEndpoint = "https://updates.push.services.mozilla.com/wpush/v1/AAABBCCCDDEEEFFF" testWebPushEndpoint = "https://updates.push.services.mozilla.com/wpush/v1/AAABBCCCDDEEEFFF"
) )
func TestServer_WebPush_Enabled(t *testing.T) {
conf := newTestConfig(t)
conf.WebRoot = "" // Disable web app
s := newTestServer(t, conf)
rr := request(t, s, "GET", "/manifest.webmanifest", "", nil)
require.Equal(t, 404, rr.Code)
conf2 := newTestConfig(t)
s2 := newTestServer(t, conf2)
rr = request(t, s2, "GET", "/manifest.webmanifest", "", nil)
require.Equal(t, 404, rr.Code)
conf3 := newTestConfigWithWebPush(t)
s3 := newTestServer(t, conf3)
rr = request(t, s3, "GET", "/manifest.webmanifest", "", nil)
require.Equal(t, 200, rr.Code)
require.Equal(t, "application/manifest+json", rr.Header().Get("Content-Type"))
}
func TestServer_WebPush_Disabled(t *testing.T) { func TestServer_WebPush_Disabled(t *testing.T) {
s := newTestServer(t, newTestConfig(t)) s := newTestServer(t, newTestConfig(t))
@@ -254,3 +280,14 @@ func requireSubscriptionCount(t *testing.T, s *Server, topic string, expectedLen
require.Nil(t, err) require.Nil(t, err)
require.Len(t, subs, expectedLength) require.Len(t, subs, expectedLength)
} }
func newTestConfigWithWebPush(t *testing.T) *Config {
conf := newTestConfig(t)
privateKey, publicKey, err := webpush.GenerateVAPIDKeys()
require.Nil(t, err)
conf.WebPushFile = filepath.Join(t.TempDir(), "webpush.db")
conf.WebPushEmailAddress = "testing@example.com"
conf.WebPushPrivateKey = privateKey
conf.WebPushPublicKey = publicKey
return conf
}

View File

@@ -371,11 +371,12 @@ type apiAccountTokenUpdateRequest struct {
} }
type apiAccountTokenResponse struct { type apiAccountTokenResponse struct {
Token string `json:"token"` Token string `json:"token"`
Label string `json:"label,omitempty"` Label string `json:"label,omitempty"`
LastAccess int64 `json:"last_access,omitempty"` LastAccess int64 `json:"last_access,omitempty"`
LastOrigin string `json:"last_origin,omitempty"` LastOrigin string `json:"last_origin,omitempty"`
Expires int64 `json:"expires,omitempty"` // Unix timestamp Expires int64 `json:"expires,omitempty"` // Unix timestamp
Provisioned bool `json:"provisioned,omitempty"` // True if this token was provisioned by the server config
} }
type apiAccountPhoneNumberVerifyRequest struct { type apiAccountPhoneNumberVerifyRequest struct {
@@ -437,6 +438,7 @@ type apiAccountResponse struct {
Username string `json:"username"` Username string `json:"username"`
Role string `json:"role,omitempty"` Role string `json:"role,omitempty"`
SyncTopic string `json:"sync_topic,omitempty"` SyncTopic string `json:"sync_topic,omitempty"`
Provisioned bool `json:"provisioned,omitempty"`
Language string `json:"language,omitempty"` Language string `json:"language,omitempty"`
Notification *user.NotificationPrefs `json:"notification,omitempty"` Notification *user.NotificationPrefs `json:"notification,omitempty"`
Subscriptions []*user.Subscription `json:"subscriptions,omitempty"` Subscriptions []*user.Subscription `json:"subscriptions,omitempty"`
@@ -458,6 +460,7 @@ type apiConfigResponse struct {
BaseURL string `json:"base_url"` BaseURL string `json:"base_url"`
AppRoot string `json:"app_root"` AppRoot string `json:"app_root"`
EnableLogin bool `json:"enable_login"` EnableLogin bool `json:"enable_login"`
RequireLogin bool `json:"require_login"`
EnableSignup bool `json:"enable_signup"` EnableSignup bool `json:"enable_signup"`
EnablePayments bool `json:"enable_payments"` EnablePayments bool `json:"enable_payments"`
EnableCalls bool `json:"enable_calls"` EnableCalls bool `json:"enable_calls"`

View File

@@ -1,3 +1,5 @@
//go:build !nofirebase
package main package main
import ( import (
@@ -26,7 +28,7 @@ func main() {
} }
data[kv[0]] = kv[1] data[kv[0]] = kv[1]
} }
fb, err := firebase.NewApp(context.Background(), nil, option.WithCredentialsFile(*conffile)) fb, err := firebase.NewApp(context.Background(), nil, option.WithAuthCredentialsFile(option.ServiceAccount, *conffile))
if err != nil { if err != nil {
fail(err.Error()) fail(err.Error())
} }

View File

@@ -7,9 +7,9 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/mattn/go-sqlite3" "github.com/mattn/go-sqlite3"
"github.com/stripe/stripe-go/v74"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"heckel.io/ntfy/v2/log" "heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/payments"
"heckel.io/ntfy/v2/util" "heckel.io/ntfy/v2/util"
"net/netip" "net/netip"
"path/filepath" "path/filepath"
@@ -773,6 +773,9 @@ func (a *Manager) ChangeToken(userID, token string, label *string, expires *time
if token == "" { if token == "" {
return nil, errNoTokenProvided return nil, errNoTokenProvided
} }
if err := a.CanChangeToken(userID, token); err != nil {
return nil, err
}
tx, err := a.db.Begin() tx, err := a.db.Begin()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -796,6 +799,9 @@ func (a *Manager) ChangeToken(userID, token string, label *string, expires *time
// RemoveToken deletes the token defined in User.Token // RemoveToken deletes the token defined in User.Token
func (a *Manager) RemoveToken(userID, token string) error { func (a *Manager) RemoveToken(userID, token string) error {
if err := a.CanChangeToken(userID, token); err != nil {
return err
}
return execTx(a.db, func(tx *sql.Tx) error { return execTx(a.db, func(tx *sql.Tx) error {
return a.removeTokenTx(tx, userID, token) return a.removeTokenTx(tx, userID, token)
}) })
@@ -811,6 +817,17 @@ func (a *Manager) removeTokenTx(tx *sql.Tx, userID, token string) error {
return nil return nil
} }
// CanChangeToken checks if the token can be changed. If the token is provisioned, it cannot be changed.
func (a *Manager) CanChangeToken(userID, token string) error {
t, err := a.Token(userID, token)
if err != nil {
return err
} else if t.Provisioned {
return ErrProvisionedTokenChange
}
return nil
}
// RemoveExpiredTokens deletes all expired tokens from the database // RemoveExpiredTokens deletes all expired tokens from the database
func (a *Manager) RemoveExpiredTokens() error { func (a *Manager) RemoveExpiredTokens() error {
if _, err := a.db.Exec(deleteExpiredTokensQuery, time.Now().Unix()); err != nil { if _, err := a.db.Exec(deleteExpiredTokensQuery, time.Now().Unix()); err != nil {
@@ -1049,7 +1066,7 @@ func (a *Manager) addUserTx(tx *sql.Tx, username, password string, role Role, ha
var err error = nil var err error = nil
if hashed { if hashed {
hash = password hash = password
if err := ValidPasswordHash(hash); err != nil { if err := ValidPasswordHash(hash, a.config.BcryptCost); err != nil {
return err return err
} }
} else { } else {
@@ -1072,6 +1089,9 @@ func (a *Manager) addUserTx(tx *sql.Tx, username, password string, role Role, ha
// RemoveUser deletes the user with the given username. The function returns nil on success, even // RemoveUser deletes the user with the given username. The function returns nil on success, even
// if the user did not exist in the first place. // if the user did not exist in the first place.
func (a *Manager) RemoveUser(username string) error { func (a *Manager) RemoveUser(username string) error {
if err := a.CanChangeUser(username); err != nil {
return err
}
return execTx(a.db, func(tx *sql.Tx) error { return execTx(a.db, func(tx *sql.Tx) error {
return a.removeUserTx(tx, username) return a.removeUserTx(tx, username)
}) })
@@ -1222,12 +1242,12 @@ func (a *Manager) readUser(rows *sql.Rows) (*User, error) {
Calls: calls, Calls: calls,
}, },
Billing: &Billing{ Billing: &Billing{
StripeCustomerID: stripeCustomerID.String, // May be empty StripeCustomerID: stripeCustomerID.String, // May be empty
StripeSubscriptionID: stripeSubscriptionID.String, // May be empty StripeSubscriptionID: stripeSubscriptionID.String, // May be empty
StripeSubscriptionStatus: stripe.SubscriptionStatus(stripeSubscriptionStatus.String), // May be empty StripeSubscriptionStatus: payments.SubscriptionStatus(stripeSubscriptionStatus.String), // May be empty
StripeSubscriptionInterval: stripe.PriceRecurringInterval(stripeSubscriptionInterval.String), // May be empty StripeSubscriptionInterval: payments.PriceRecurringInterval(stripeSubscriptionInterval.String), // May be empty
StripeSubscriptionPaidUntil: time.Unix(stripeSubscriptionPaidUntil.Int64, 0), // May be zero StripeSubscriptionPaidUntil: time.Unix(stripeSubscriptionPaidUntil.Int64, 0), // May be zero
StripeSubscriptionCancelAt: time.Unix(stripeSubscriptionCancelAt.Int64, 0), // May be zero StripeSubscriptionCancelAt: time.Unix(stripeSubscriptionCancelAt.Int64, 0), // May be zero
}, },
Deleted: deleted.Valid, Deleted: deleted.Valid,
} }
@@ -1389,17 +1409,32 @@ func (a *Manager) ReservationOwner(topic string) (string, error) {
// ChangePassword changes a user's password // ChangePassword changes a user's password
func (a *Manager) ChangePassword(username, password string, hashed bool) error { func (a *Manager) ChangePassword(username, password string, hashed bool) error {
if err := a.CanChangeUser(username); err != nil {
return err
}
return execTx(a.db, func(tx *sql.Tx) error { return execTx(a.db, func(tx *sql.Tx) error {
return a.changePasswordTx(tx, username, password, hashed) return a.changePasswordTx(tx, username, password, hashed)
}) })
} }
// CanChangeUser checks if the user with the given username can be changed.
// This is used to prevent changes to provisioned users, which are defined in the config file.
func (a *Manager) CanChangeUser(username string) error {
user, err := a.User(username)
if err != nil {
return err
} else if user.Provisioned {
return ErrProvisionedUserChange
}
return nil
}
func (a *Manager) changePasswordTx(tx *sql.Tx, username, password string, hashed bool) error { func (a *Manager) changePasswordTx(tx *sql.Tx, username, password string, hashed bool) error {
var hash string var hash string
var err error var err error
if hashed { if hashed {
hash = password hash = password
if err := ValidPasswordHash(hash); err != nil { if err := ValidPasswordHash(hash, a.config.BcryptCost); err != nil {
return err return err
} }
} else { } else {
@@ -1417,6 +1452,9 @@ func (a *Manager) changePasswordTx(tx *sql.Tx, username, password string, hashed
// ChangeRole changes a user's role. When a role is changed from RoleUser to RoleAdmin, // ChangeRole changes a user's role. When a role is changed from RoleUser to RoleAdmin,
// all existing access control entries (Grant) are removed, since they are no longer needed. // all existing access control entries (Grant) are removed, since they are no longer needed.
func (a *Manager) ChangeRole(username string, role Role) error { func (a *Manager) ChangeRole(username string, role Role) error {
if err := a.CanChangeUser(username); err != nil {
return err
}
return execTx(a.db, func(tx *sql.Tx) error { return execTx(a.db, func(tx *sql.Tx) error {
return a.changeRoleTx(tx, username, role) return a.changeRoleTx(tx, username, role)
}) })
@@ -1437,14 +1475,8 @@ func (a *Manager) changeRoleTx(tx *sql.Tx, username string, role Role) error {
return nil return nil
} }
// ChangeProvisioned changes the provisioned status of a user. This is used to mark users as // changeProvisionedTx changes the provisioned status of a user. This is used to mark users as
// provisioned. A provisioned user is a user defined in the config file. // provisioned. A provisioned user is a user defined in the config file.
func (a *Manager) ChangeProvisioned(username string, provisioned bool) error {
return execTx(a.db, func(tx *sql.Tx) error {
return a.changeProvisionedTx(tx, username, provisioned)
})
}
func (a *Manager) changeProvisionedTx(tx *sql.Tx, username string, provisioned bool) error { func (a *Manager) changeProvisionedTx(tx *sql.Tx, username string, provisioned bool) error {
if _, err := tx.Exec(updateUserProvisionedQuery, provisioned, username); err != nil { if _, err := tx.Exec(updateUserProvisionedQuery, provisioned, username); err != nil {
return err return err
@@ -1670,7 +1702,7 @@ func (a *Manager) Tiers() ([]*Tier, error) {
tiers := make([]*Tier, 0) tiers := make([]*Tier, 0)
for { for {
tier, err := a.readTier(rows) tier, err := a.readTier(rows)
if err == ErrTierNotFound { if errors.Is(err, ErrTierNotFound) {
break break
} else if err != nil { } else if err != nil {
return nil, err return nil, err

View File

@@ -4,7 +4,6 @@ import (
"database/sql" "database/sql"
"fmt" "fmt"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stripe/stripe-go/v74"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"heckel.io/ntfy/v2/util" "heckel.io/ntfy/v2/util"
"net/netip" "net/netip"
@@ -164,8 +163,8 @@ func TestManager_AddUser_And_Query(t *testing.T) {
require.Nil(t, a.ChangeBilling("user", &Billing{ require.Nil(t, a.ChangeBilling("user", &Billing{
StripeCustomerID: "acct_123", StripeCustomerID: "acct_123",
StripeSubscriptionID: "sub_123", StripeSubscriptionID: "sub_123",
StripeSubscriptionStatus: stripe.SubscriptionStatusActive, StripeSubscriptionStatus: "active",
StripeSubscriptionInterval: stripe.PriceRecurringIntervalMonth, StripeSubscriptionInterval: "month",
StripeSubscriptionPaidUntil: time.Now().Add(time.Hour), StripeSubscriptionPaidUntil: time.Now().Add(time.Hour),
StripeSubscriptionCancelAt: time.Unix(0, 0), StripeSubscriptionCancelAt: time.Unix(0, 0),
})) }))
@@ -1163,7 +1162,7 @@ func TestManager_WithProvisionedUsers(t *testing.T) {
// Re-open the DB (second app start) // Re-open the DB (second app start)
require.Nil(t, a.db.Close()) require.Nil(t, a.db.Close())
conf.Users = []*User{ conf.Users = []*User{
{Name: "philuser", Hash: "$2a$10$AAAU21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", Role: RoleUser}, {Name: "philuser", Hash: "$2a$10$AAAAU21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", Role: RoleUser},
} }
conf.Access = map[string][]*Grant{ conf.Access = map[string][]*Grant{
"philuser": { "philuser": {
@@ -1209,6 +1208,9 @@ func TestManager_WithProvisionedUsers(t *testing.T) {
require.Equal(t, "tk_u48wqendnkx9er21pqqcadlytbutx", tokens[1].Value) require.Equal(t, "tk_u48wqendnkx9er21pqqcadlytbutx", tokens[1].Value)
require.Equal(t, "Another token", tokens[1].Label) require.Equal(t, "Another token", tokens[1].Label)
// Try changing provisioned user's password
require.Error(t, a.ChangePassword("philuser", "new-pass", false))
// Re-open the DB again (third app start) // Re-open the DB again (third app start)
require.Nil(t, a.db.Close()) require.Nil(t, a.db.Close())
conf.Users = []*User{} conf.Users = []*User{}
@@ -1290,7 +1292,7 @@ func TestManager_UpdateNonProvisionedUsersToProvisionedUsers(t *testing.T) {
// Re-open the DB (second app start) // Re-open the DB (second app start)
require.Nil(t, a.db.Close()) require.Nil(t, a.db.Close())
conf.Users = []*User{ conf.Users = []*User{
{Name: "philuser", Hash: "$2a$10$AAAU21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", Role: RoleUser}, {Name: "philuser", Hash: "$2a$10$AAAAU21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", Role: RoleUser},
} }
conf.Access = map[string][]*Grant{ conf.Access = map[string][]*Grant{
"philuser": { "philuser": {
@@ -1306,7 +1308,7 @@ func TestManager_UpdateNonProvisionedUsersToProvisionedUsers(t *testing.T) {
require.Len(t, users, 2) require.Len(t, users, 2)
require.Equal(t, "philuser", users[0].Name) require.Equal(t, "philuser", users[0].Name)
require.Equal(t, RoleUser, users[0].Role) require.Equal(t, RoleUser, users[0].Role)
require.Equal(t, "$2a$10$AAAU21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", users[0].Hash) require.Equal(t, "$2a$10$AAAAU21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", users[0].Hash)
require.True(t, users[0].Provisioned) // Updated to provisioned! require.True(t, users[0].Provisioned) // Updated to provisioned!
grants, err = a.Grants("philuser") grants, err = a.Grants("philuser")

View File

@@ -2,8 +2,8 @@ package user
import ( import (
"errors" "errors"
"github.com/stripe/stripe-go/v74"
"heckel.io/ntfy/v2/log" "heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/payments"
"net/netip" "net/netip"
"strings" "strings"
"time" "time"
@@ -140,8 +140,8 @@ type Stats struct {
type Billing struct { type Billing struct {
StripeCustomerID string StripeCustomerID string
StripeSubscriptionID string StripeSubscriptionID string
StripeSubscriptionStatus stripe.SubscriptionStatus StripeSubscriptionStatus payments.SubscriptionStatus
StripeSubscriptionInterval stripe.PriceRecurringInterval StripeSubscriptionInterval payments.PriceRecurringInterval
StripeSubscriptionPaidUntil time.Time StripeSubscriptionPaidUntil time.Time
StripeSubscriptionCancelAt time.Time StripeSubscriptionCancelAt time.Time
} }
@@ -244,15 +244,18 @@ const (
// Error constants used by the package // Error constants used by the package
var ( var (
ErrUnauthenticated = errors.New("unauthenticated") ErrUnauthenticated = errors.New("unauthenticated")
ErrUnauthorized = errors.New("unauthorized") ErrUnauthorized = errors.New("unauthorized")
ErrInvalidArgument = errors.New("invalid argument") ErrInvalidArgument = errors.New("invalid argument")
ErrUserNotFound = errors.New("user not found") ErrUserNotFound = errors.New("user not found")
ErrUserExists = errors.New("user already exists") ErrUserExists = errors.New("user already exists")
ErrPasswordHashInvalid = errors.New("password hash but be a bcrypt hash, use 'ntfy user hash' to generate") ErrPasswordHashInvalid = errors.New("password hash must be a bcrypt hash, use 'ntfy user hash' to generate")
ErrTierNotFound = errors.New("tier not found") ErrPasswordHashWeak = errors.New("password hash too weak, use 'ntfy user hash' to generate")
ErrTokenNotFound = errors.New("token not found") ErrTierNotFound = errors.New("tier not found")
ErrPhoneNumberNotFound = errors.New("phone number not found") ErrTokenNotFound = errors.New("token not found")
ErrTooManyReservations = errors.New("new tier has lower reservation limit") ErrPhoneNumberNotFound = errors.New("phone number not found")
ErrPhoneNumberExists = errors.New("phone number already exists") ErrTooManyReservations = errors.New("new tier has lower reservation limit")
ErrPhoneNumberExists = errors.New("phone number already exists")
ErrProvisionedUserChange = errors.New("cannot change or delete provisioned user")
ErrProvisionedTokenChange = errors.New("cannot change or delete provisioned token")
) )

View File

@@ -41,10 +41,16 @@ func AllowedTier(tier string) bool {
} }
// ValidPasswordHash checks if the given password hash is a valid bcrypt hash // ValidPasswordHash checks if the given password hash is a valid bcrypt hash
func ValidPasswordHash(hash string) error { func ValidPasswordHash(hash string, minCost int) error {
if !strings.HasPrefix(hash, "$2a$") && !strings.HasPrefix(hash, "$2b$") && !strings.HasPrefix(hash, "$2y$") { if !strings.HasPrefix(hash, "$2a$") && !strings.HasPrefix(hash, "$2b$") && !strings.HasPrefix(hash, "$2y$") {
return ErrPasswordHashInvalid return ErrPasswordHashInvalid
} }
cost, err := bcrypt.Cost([]byte(hash))
if err != nil { // Check if the hash is valid (length, format, etc.)
return err
} else if cost < minCost {
return ErrPasswordHashWeak
}
return nil return nil
} }

1702
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@ var config = {
base_url: window.location.origin, // Change to test against a different server base_url: window.location.origin, // Change to test against a different server
app_root: "/", app_root: "/",
enable_login: true, enable_login: true,
require_login: false,
enable_signup: true, enable_signup: true,
enable_payments: false, enable_payments: false,
enable_reservations: true, enable_reservations: true,

View File

@@ -22,7 +22,7 @@
"publish_dialog_chip_email_label": "Препращане към ел. поща", "publish_dialog_chip_email_label": "Препращане към ел. поща",
"publish_dialog_chip_attach_url_label": "Прикачване на файл от адрес", "publish_dialog_chip_attach_url_label": "Прикачване на файл от адрес",
"publish_dialog_chip_attach_file_label": "Прикачване местен файл", "publish_dialog_chip_attach_file_label": "Прикачване местен файл",
"publish_dialog_chip_delay_label": "Отлагане на изпращането", "publish_dialog_chip_delay_label": "Отложено изпращане",
"publish_dialog_chip_topic_label": "Промяна на темата", "publish_dialog_chip_topic_label": "Промяна на темата",
"publish_dialog_button_cancel_sending": "Отменяне на изпращането", "publish_dialog_button_cancel_sending": "Отменяне на изпращането",
"publish_dialog_button_cancel": "Отказ", "publish_dialog_button_cancel": "Отказ",
@@ -121,7 +121,7 @@
"subscribe_dialog_login_button_login": "Вход", "subscribe_dialog_login_button_login": "Вход",
"subscribe_dialog_error_user_not_authorized": "Потребителят {{username}} няма достъп", "subscribe_dialog_error_user_not_authorized": "Потребителят {{username}} няма достъп",
"prefs_appearance_title": "Външен вид", "prefs_appearance_title": "Външен вид",
"publish_dialog_delay_placeholder": "Отлагане на изпращането, {{unixTimestamp}}, {{relativeTime}} или „{{naturalLanguage}}“ (на английски)", "publish_dialog_delay_placeholder": "Отложено изпращане, {{unixTimestamp}}, {{relativeTime}} или „{{naturalLanguage}}“ (на английски)",
"prefs_notifications_delete_after_one_week": "След една седмица", "prefs_notifications_delete_after_one_week": "След една седмица",
"prefs_users_title": "Управление на потребители", "prefs_users_title": "Управление на потребители",
"prefs_users_table_base_url_header": "Адрес на услугата", "prefs_users_table_base_url_header": "Адрес на услугата",
@@ -177,7 +177,7 @@
"publish_dialog_topic_reset": "Нулиране на тема", "publish_dialog_topic_reset": "Нулиране на тема",
"publish_dialog_click_reset": "Премахване на адрес", "publish_dialog_click_reset": "Премахване на адрес",
"publish_dialog_email_reset": "Премахване на препращането към ел. поща", "publish_dialog_email_reset": "Премахване на препращането към ел. поща",
"publish_dialog_delay_reset": "Премахва отлагането на изпращането", "publish_dialog_delay_reset": "Премахва отложеното на изпращане",
"publish_dialog_attached_file_remove": "Премахване на прикачения файл", "publish_dialog_attached_file_remove": "Премахване на прикачения файл",
"emoji_picker_search_clear": "Изчистване на търсенето", "emoji_picker_search_clear": "Изчистване на търсенето",
"subscribe_dialog_subscribe_base_url_label": "Адрес на услугата", "subscribe_dialog_subscribe_base_url_label": "Адрес на услугата",
@@ -253,7 +253,7 @@
"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_other": "{{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": "Премахване на профила",
"account_upgrade_dialog_title": "Промяна нивото на профила", "account_upgrade_dialog_title": "Промяна нивото на профила",
@@ -403,5 +403,7 @@
"prefs_appearance_theme_system": "Системна (подразбирана)", "prefs_appearance_theme_system": "Системна (подразбирана)",
"web_push_subscription_expiring_title": "Известията временно ще бъдат спрени", "web_push_subscription_expiring_title": "Известията временно ще бъдат спрени",
"web_push_subscription_expiring_body": "За да продължите да получавате известия, отворете ntfy", "web_push_subscription_expiring_body": "За да продължите да получавате известия, отворете ntfy",
"action_bar_unmute_notifications": "Включване звука на известията" "action_bar_unmute_notifications": "Включване звука на известията",
"account_tokens_table_cannot_delete_or_edit_provisioned_token": "Кодът за защита от външна система не може да бъде променян или премахван",
"account_basics_cannot_edit_or_delete_provisioned_user": "Потребител от външна система не може да бъде променян или премахван"
} }

View File

@@ -3,5 +3,14 @@
"action_bar_profile_title": "Perfil", "action_bar_profile_title": "Perfil",
"action_bar_settings": "Configuració", "action_bar_settings": "Configuració",
"action_bar_account": "Compte", "action_bar_account": "Compte",
"common_add": "Afegir" "common_add": "Afegir",
"common_cancel": "Cancel·la",
"common_save": "Desa",
"common_back": "Enrere",
"common_copy_to_clipboard": "Copia al portaretalls",
"signup_title": "Crea un compte ntfy",
"signup_form_username": "Nom d'usuari",
"signup_form_password": "Contrasenya",
"signup_form_confirm_password": "Confirma la contrasenya",
"signup_form_button_submit": "Dona't d'alta"
} }

View File

@@ -0,0 +1,8 @@
{
"common_cancel": "Отмѣнити",
"common_save": "Сохрани",
"common_add": "Приложити",
"common_back": "Назадъ",
"login_form_button_submit": "Въниди",
"signup_form_password": "Таино слово"
}

View File

@@ -403,5 +403,7 @@
"web_push_subscription_expiring_body": "Öffne ntfy um weiterhin Benachrichtigungen zu erhalten", "web_push_subscription_expiring_body": "Öffne ntfy um weiterhin Benachrichtigungen zu erhalten",
"web_push_unknown_notification_title": "Unbekannte Benachrichtigung vom Server empfangen", "web_push_unknown_notification_title": "Unbekannte Benachrichtigung vom Server empfangen",
"web_push_unknown_notification_body": "Du musst möglicherweise ntfy aktualisieren, indem du die Web App öffnest", "web_push_unknown_notification_body": "Du musst möglicherweise ntfy aktualisieren, indem du die Web App öffnest",
"prefs_notifications_web_push_enabled_description": "Benachrichtigungen werden empfangen, auch wenn die Web App nicht geöffnet ist (via Web Push)" "prefs_notifications_web_push_enabled_description": "Benachrichtigungen werden empfangen, auch wenn die Web App nicht geöffnet ist (via Web Push)",
"account_tokens_table_cannot_delete_or_edit_provisioned_token": "Bereitgestelltes Token kann nicht bearbeitet oder gelöscht werden",
"account_basics_cannot_edit_or_delete_provisioned_user": "Ein bereitgestellter Benutzer kann nicht bearbeitet oder gelöscht werden"
} }

View File

@@ -214,6 +214,7 @@
"account_basics_phone_numbers_dialog_check_verification_button": "Confirm code", "account_basics_phone_numbers_dialog_check_verification_button": "Confirm code",
"account_basics_phone_numbers_dialog_channel_sms": "SMS", "account_basics_phone_numbers_dialog_channel_sms": "SMS",
"account_basics_phone_numbers_dialog_channel_call": "Call", "account_basics_phone_numbers_dialog_channel_call": "Call",
"account_basics_cannot_edit_or_delete_provisioned_user": "A provisioned user cannot be edited or deleted",
"account_usage_title": "Usage", "account_usage_title": "Usage",
"account_usage_of_limit": "of {{limit}}", "account_usage_of_limit": "of {{limit}}",
"account_usage_unlimited": "Unlimited", "account_usage_unlimited": "Unlimited",
@@ -293,6 +294,7 @@
"account_tokens_table_current_session": "Current browser session", "account_tokens_table_current_session": "Current browser session",
"account_tokens_table_copied_to_clipboard": "Access token copied", "account_tokens_table_copied_to_clipboard": "Access token copied",
"account_tokens_table_cannot_delete_or_edit": "Cannot edit or delete current session token", "account_tokens_table_cannot_delete_or_edit": "Cannot edit or delete current session token",
"account_tokens_table_cannot_delete_or_edit_provisioned_token": "Cannot edit or delete provisioned token",
"account_tokens_table_create_token_button": "Create access token", "account_tokens_table_create_token_button": "Create access token",
"account_tokens_table_last_origin_tooltip": "From IP address {{ip}}, click to lookup", "account_tokens_table_last_origin_tooltip": "From IP address {{ip}}, click to lookup",
"account_tokens_dialog_title_create": "Create access token", "account_tokens_dialog_title_create": "Create access token",

View File

@@ -1 +1,5 @@
{} {
"common_cancel": "Nuligi",
"common_save": "Konservi",
"common_add": "Aldoni"
}

View File

@@ -404,5 +404,7 @@
"prefs_appearance_theme_dark": "Oscuro", "prefs_appearance_theme_dark": "Oscuro",
"web_push_subscription_expiring_body": "Abrir ntfy para seguir recibiendo notificaciones", "web_push_subscription_expiring_body": "Abrir ntfy para seguir recibiendo notificaciones",
"web_push_unknown_notification_title": "Notificación desconocida recibida del servidor", "web_push_unknown_notification_title": "Notificación desconocida recibida del servidor",
"web_push_unknown_notification_body": "Puede que necesites actualizar ntfy abriendo la aplicación web" "web_push_unknown_notification_body": "Puede que necesites actualizar ntfy abriendo la aplicación web",
"account_basics_cannot_edit_or_delete_provisioned_user": "Un usuario provisionado no se puede editar o eliminar",
"account_tokens_table_cannot_delete_or_edit_provisioned_token": "No se puede editar o eliminar un token provisionado"
} }

View File

@@ -24,7 +24,7 @@
"signup_form_toggle_password_visibility": "Vaheta salasõna nähtavust", "signup_form_toggle_password_visibility": "Vaheta salasõna nähtavust",
"action_bar_account": "Kasutajakonto", "action_bar_account": "Kasutajakonto",
"action_bar_sign_in": "Logi sisse", "action_bar_sign_in": "Logi sisse",
"nav_button_documentation": "Dokumentatsioon", "nav_button_documentation": "Juhendid ja teave",
"action_bar_profile_title": "Profiil", "action_bar_profile_title": "Profiil",
"action_bar_profile_settings": "Seadistused", "action_bar_profile_settings": "Seadistused",
"action_bar_sign_up": "Liitu", "action_bar_sign_up": "Liitu",
@@ -53,8 +53,8 @@
"account_tokens_table_token_header": "Tunnusluba", "account_tokens_table_token_header": "Tunnusluba",
"account_tokens_table_last_origin_tooltip": "IP-aadressilt {{ip}}, klõpsi täpsema teabe nägemiseks", "account_tokens_table_last_origin_tooltip": "IP-aadressilt {{ip}}, klõpsi täpsema teabe nägemiseks",
"action_bar_reservation_add": "Reserveeri teema", "action_bar_reservation_add": "Reserveeri teema",
"action_bar_reservation_edit": "Muuda reserveeringut", "action_bar_reservation_edit": "Muuda reserveerimist",
"action_bar_reservation_delete": "Eemalda reserveering", "action_bar_reservation_delete": "Eemalda reserveerimine",
"action_bar_reservation_limit_reached": "Ülempiir on käes", "action_bar_reservation_limit_reached": "Ülempiir on käes",
"action_bar_send_test_notification": "Saata testteavitus", "action_bar_send_test_notification": "Saata testteavitus",
"action_bar_clear_notifications": "Kustuta kõik teavitused", "action_bar_clear_notifications": "Kustuta kõik teavitused",
@@ -126,7 +126,7 @@
"account_usage_unlimited": "Piiramatu", "account_usage_unlimited": "Piiramatu",
"prefs_notifications_delete_after_never": "Mitte kunagi", "prefs_notifications_delete_after_never": "Mitte kunagi",
"account_upgrade_dialog_interval_monthly": "Iga kuu", "account_upgrade_dialog_interval_monthly": "Iga kuu",
"account_upgrade_dialog_tier_price_per_month": "kuu", "account_upgrade_dialog_tier_price_per_month": "kuus",
"prefs_notifications_web_push_disabled": "Pole kasutusel", "prefs_notifications_web_push_disabled": "Pole kasutusel",
"prefs_appearance_title": "Välimus", "prefs_appearance_title": "Välimus",
"prefs_appearance_language_title": "Keel", "prefs_appearance_language_title": "Keel",
@@ -139,7 +139,7 @@
"display_name_dialog_placeholder": "Kuvatav nimi", "display_name_dialog_placeholder": "Kuvatav nimi",
"publish_dialog_title_no_topic": "Avalda teavitus", "publish_dialog_title_no_topic": "Avalda teavitus",
"publish_dialog_progress_uploading": "Laadin üles…", "publish_dialog_progress_uploading": "Laadin üles…",
"publish_dialog_message_published": "Teavitus on saadetud", "publish_dialog_message_published": "Teavitus on avaldatud",
"publish_dialog_emoji_picker_show": "Vali emoji", "publish_dialog_emoji_picker_show": "Vali emoji",
"publish_dialog_priority_low": "Vähetähtis", "publish_dialog_priority_low": "Vähetähtis",
"publish_dialog_priority_default": "Vaikimisi tähtsus", "publish_dialog_priority_default": "Vaikimisi tähtsus",
@@ -185,7 +185,7 @@
"notifications_loading": "Laadin teavitusi…", "notifications_loading": "Laadin teavitusi…",
"publish_dialog_title_topic": "Avalda teemas {{topic}}", "publish_dialog_title_topic": "Avalda teemas {{topic}}",
"publish_dialog_progress_uploading_detail": "Üleslaadimisel {{loaded}}/{{total}} ({{percent}}%) …", "publish_dialog_progress_uploading_detail": "Üleslaadimisel {{loaded}}/{{total}} ({{percent}}%) …",
"publish_dialog_topic_placeholder": "Teema nimi, nt. kati_teavitused", "publish_dialog_topic_placeholder": "Teema nimi, nt. kadri_kiirteated",
"publish_dialog_title_placeholder": "Teavituse pealkiri, nt. Andmeruumi teavitus", "publish_dialog_title_placeholder": "Teavituse pealkiri, nt. Andmeruumi teavitus",
"publish_dialog_message_placeholder": "Siia sisesta sõnum", "publish_dialog_message_placeholder": "Siia sisesta sõnum",
"notifications_none_for_any_title": "Sa pole veel saanud ühtegi teavitust.", "notifications_none_for_any_title": "Sa pole veel saanud ühtegi teavitust.",
@@ -270,5 +270,140 @@
"account_basics_phone_numbers_dialog_number_label": "Telefoninumber", "account_basics_phone_numbers_dialog_number_label": "Telefoninumber",
"prefs_notifications_delete_after_one_week": "Ühe nädala möödumisel", "prefs_notifications_delete_after_one_week": "Ühe nädala möödumisel",
"prefs_notifications_delete_after_one_day": "Ühe päeva möödumisel", "prefs_notifications_delete_after_one_day": "Ühe päeva möödumisel",
"prefs_notifications_delete_after_one_month": "Ühe kuu möödumisel" "prefs_notifications_delete_after_one_month": "Ühe kuu möödumisel",
"publish_dialog_attached_file_title": "Manustatud fail:",
"publish_dialog_attached_file_filename_placeholder": "Manuse faili nimi",
"publish_dialog_attached_file_remove": "Eemalda manustatud fail",
"publish_dialog_drop_file_here": "Lohista fail siia",
"emoji_picker_search_placeholder": "Otsi emojit",
"publish_dialog_checkbox_publish_another": "Avalda veel midagi",
"emoji_picker_search_clear": "Tühjenda otsing",
"account_usage_reservations_title": "Reserveeritud teemad",
"account_usage_reservations_none": "Sellel kasutajakontol pole reserveeritud teemasid",
"account_usage_attachment_storage_title": "Manuste andmeruum",
"account_usage_calls_none": "Selle kasutajakontoga ei saa helistada",
"account_usage_calls_title": "Helistatud kõnesid",
"account_usage_messages_title": "Avaldatud sõnumeid",
"account_usage_emails_title": "Saadetud e-kirju",
"account_basics_tier_manage_billing_button": "Halda arveldust",
"account_basics_tier_canceled_subscription": "Sinu teenusetellimus on katkestatud ja muutub tasuta {{date}} kontoks.",
"account_basics_tier_paid_until": "Tellimus on tasutud kuni {{date}} ja kuulub automaatselt uuendamisele",
"account_basics_tier_upgrade_button": "Hakka kasutama Pro-teenust",
"account_basics_tier_payment_overdue": "Sinu arve(d) on tasumata. Palun uuenda oma maksmisviisi või vastasel juhul peame varsti sinu kasutajakonto taseme muutma madalamaks.",
"account_upgrade_dialog_tier_features_no_reservations": "Reserveeritud teemasid pole",
"account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} reserveeritud teemat",
"account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} reserveeritud teema",
"prefs_notifications_sound_title": "Teavituse heli",
"account_tokens_delete_dialog_submit_button": "Kustuta tunnusluba jäädavalt",
"account_tokens_delete_dialog_description": "Enne tunnusloa kustutamist palun kontrolli, et ükski rakendus ei kasutaks seda. <strong>Seda tegevust ei saa tagasi pöörata</strong>.",
"account_tokens_delete_dialog_title": "Kustuta ligipääsu tunnusluba",
"account_tokens_dialog_expires_never": "Tunnusluba ei aegu iialgi",
"account_tokens_dialog_expires_x_days": "Tunnusluba aegub {{days}} päeva pärast",
"account_tokens_dialog_expires_x_hours": "Tunnusluba aegub {{hours}} tunni pärast",
"account_tokens_dialog_expires_unchanged": "Jäta aegumise kuupäev muutmata",
"account_tokens_dialog_expires_label": "Tunnusluba aegub",
"account_tokens_dialog_button_update": "Uuenda tunnusluba",
"account_tokens_dialog_button_create": "Loo tunnusluba",
"prefs_users_title": "Halda kasutajaid",
"subscribe_dialog_subscribe_use_another_label": "Kasuta muud serverit",
"subscribe_dialog_subscribe_use_another_background_info": "Teavitused muudest serveritest ei toimi, kui veebirakendus pole avatud",
"subscribe_dialog_login_description": "See teema on kaitstud salasõnaga. Tellimiseks sisesta palun kasutajanimi ja salasõna.",
"subscribe_dialog_error_topic_already_reserved": "Teema on juba reserveeritud",
"account_delete_title": "Kustuta kasutajakonto",
"account_delete_description": "Kustuta oma kasutajakonto jäädavalt",
"account_delete_dialog_description": "Järgnevaga kustutad serverist lõplikult oma kasutajakonto ning kõik temaga seotud andmed. Peale kustutamist pole kasutajanimi saadaval 7 päeva jooksul. Kui sa tõesti soovid kustutamisega jätkata, siis palun sisesta alljärgnevasse kasti oma salasõna.",
"web_push_unknown_notification_title": "Serverist saabus tundmatu teavitus",
"web_push_subscription_expiring_body": "Kui soovid, et jätkuvalt saabuks teavitused, siis ava ntfy",
"web_push_subscription_expiring_title": "Teavitused on ajutiselt peatatud",
"error_boundary_unsupported_indexeddb_title": "Veebibrauseri privaatne režiim pole toetatud",
"error_boundary_button_reload_ntfy": "Laadi ntfy uuesti",
"error_boundary_button_copy_stack_trace": "Kopeeri pinujälg",
"error_boundary_stack_trace": "Pinujälg",
"error_boundary_gathering_info": "Kogu täiendavat teavet…",
"notifications_none_for_any_description": "Teemakohaste teavituste saatmiseks tee PUT või POST meetodiga päring teema võrguaadressile. Siin on üks näide ühe sinu teemaga.",
"notifications_no_subscriptions_title": "Tundub, et sul pole veel ühtegi tellimust.",
"notifications_no_subscriptions_description": "Olemasoleva teema tellimiseks või uue loomiseks klõpsa „{{linktext}}“. Peale seda saad PUT või POST meetodiga päringuga saata sõnumeid ning neid siin vastu võtta.",
"notifications_more_details": "Lisateavet leiad <websiteLink>veebisaidist</websiteLink> või <docsLink>juhendist</docsLink>.",
"publish_dialog_details_examples_description": "Näited ja saatmisvõimaluste üksikasjaliku kirjelduse leiad <docsLink>juhendist</docsLink>.",
"account_tokens_description": "Selleks, et ei peaks ntfy API abil avaldamise ja tellimuse päringusse lisama kasutajanime ja salasõna, kasuta tunnuslubasid. Lisateavet leiad <Link>juhendist</Link>.",
"subscribe_dialog_subscribe_title": "Telli teema",
"subscribe_dialog_subscribe_description": "Teemasid ei saa salasõnaga kaitsta, seega vali teema nimi, mida pole väga lihtne ära arvata. Peale tellimuse tegemist võide kohe hakata PUT või POST päringutega sõnumeid saatma.",
"subscribe_dialog_subscribe_topic_placeholder": "Teema nimi, näiteks kadri_kiirteated",
"subscribe_dialog_error_user_not_authorized": "Kasutajal {{username}} puudub volitus",
"account_usage_of_limit": "piirangust {{limit}}",
"account_usage_limits_reset_daily": "Kasutuspiirangud lähtestatakse keskööl (UTC järgi)",
"account_basics_tier_admin_suffix_with_tier": "(tasemega {{tier}})",
"account_basics_tier_admin_suffix_no_tier": "(tase puudub)",
"account_upgrade_dialog_title": "Muuda kasutajakonto taset",
"account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} faili kohta",
"account_upgrade_dialog_tier_features_calls_other": "{{calls}} kõnet päevas",
"account_upgrade_dialog_tier_features_calls_one": "{{calls}} kõne päevas",
"account_upgrade_dialog_tier_features_no_calls": "Ilma telefonikõnedeta",
"account_upgrade_dialog_tier_features_attachment_total_size": "andmeruum kokku {{totalsize}}",
"account_upgrade_dialog_tier_price_billed_monthly": "{{price}}aastas. Arveldatuna kord kuus.",
"account_upgrade_dialog_tier_price_billed_yearly": "{{price}} arveldatuna kord aastas. Sa säästad {{save}}.",
"account_upgrade_dialog_button_pay_now": "Maksa nüüd ja telli",
"account_upgrade_dialog_button_cancel_subscription": "Katkesta tellimus",
"account_upgrade_dialog_interval_yearly_discount_save": "säästa {{discount}}%",
"account_upgrade_dialog_interval_yearly_discount_save_up_to": "säästa kuni {{discount}}%",
"account_upgrade_dialog_billing_contact_email": "Küsimuste puhul arvelduste kohta, palun <Link>kontakteeru meiega otse</Link>.",
"account_upgrade_dialog_billing_contact_website": "Küsimuste puhul arvelduste kohta, palun <Link>vaata meie veebisaiti</Link>.",
"account_delete_dialog_billing_warning": "Sinu kasutajakonto kustutamisel katkeb koheselt ka tellimus. Muu hulgas ei saa sa enam ligi arvelduste haldusvaatele.",
"account_upgrade_dialog_button_update_subscription": "Uuenda tellimust",
"account_tokens_title": "Tunnusload ligipääsuks",
"account_tokens_dialog_label": "Silt, näiteks „Salaradari teavitused“",
"account_usage_attachment_storage_description": "{{filesize}} faili kohta, kustutatud peale {{expiry}}",
"account_usage_cannot_create_portal_session": "Arvelduste vaate avamine ei õnnestu",
"prefs_notifications_min_priority_any": "Kõik prioriteedid",
"prefs_notifications_min_priority_low_and_higher": "Vähetähtsad ja kõrgemad",
"prefs_notifications_min_priority_default_and_higher": "Vaikimisi tähtsusega ja kõrgemad",
"prefs_notifications_min_priority_high_and_higher": "Väga tähtsad ja kõrgemad",
"prefs_notifications_min_priority_max_only": "Vaid kõrgeim prioriteet",
"prefs_reservations_table_everyone_deny_all": "Vaid mina saan avaldada ja tellida",
"prefs_reservations_table_everyone_read_only": "Mina saan avaldada ja tellida, kõik saavad tellida",
"prefs_reservations_table_everyone_write_only": "Mina saan avaldada ja tellida, kõik saavad avaldada",
"prefs_reservations_table_everyone_read_write": "Kõik saavad avaldada ja tellida",
"prefs_reservations_table_not_subscribed": "Pole tellitud",
"prefs_reservations_table_click_to_subscribe": "Tellimiseks klõpsi",
"prefs_reservations_dialog_title_add": "Reserveeri teema",
"prefs_reservations_dialog_title_edit": "Muuda reserveeritud teemat",
"prefs_reservations_dialog_title_delete": "Kustuta teema reserveering",
"prefs_reservations_dialog_description": "Teema reserveerimisega muutud selle omanikuks ja saad teiste jaoks määrata ligipääsuõigusi teemale.",
"reservation_delete_dialog_description": "Teema reserveerimisest loobudes annad teistele võimaluse seda reserveerida ja muutuda selle omanikuks. Sina saad otsustada, kas vanad sõnumid jäävad alles või kustutatakse.",
"reservation_delete_dialog_action_keep_title": "Säilita puhverdatud sõnumid ja manused",
"reservation_delete_dialog_action_keep_description": "Serveris puhverdatud sõnumid ja manused muutuvad avalikult nähtavaks neile, kes teavad teema nime.",
"reservation_delete_dialog_action_delete_title": "Kustuta puhverdatud sõnumid ja manused",
"reservation_delete_dialog_action_delete_description": "Puhverdatud sõnumid ja manused kustuvad jäädavalt. Seda tegevust ei saa hiljem tagasi pöörata.",
"reservation_delete_dialog_submit_button": "Kustuta reserveerimine",
"prefs_reservations_description": "Sa võid teemade nimesid reserveerida isiklikuks kasutuseks. Sellega muutud teema omanikuks ja saad määrata, kes ning mis viisil teemale ligi saab.",
"prefs_reservations_limit_reached": "Oled jõudnud reserveeritud teemade arvu ülempiirini.",
"prefs_reservations_add_button": "Lisa reserveeritud teema",
"prefs_reservations_edit_button": "Muuda ligipääsu teemale",
"prefs_reservations_delete_button": "Lähtesta ligipääs teemale",
"prefs_reservations_table": "Reserveeritud teemade tabel",
"web_push_unknown_notification_body": "Avades veebirakenduse peaksid vist tegema ntfy uuenduse",
"prefs_users_description_no_sync": "Kasutajad ja nende salasõnad pole sinu kontoga sünkroonitud.",
"error_boundary_title": "Vaat, kus lops - ntfy jooksis kokku",
"error_boundary_description": "Ilmselgelt ei peaks niimoodi juhtuma. Vabandust.<br/>Kui sul on mõni hetk aega, siis palun <githubLink>seate sellest GitHubis</githubLink> või kirjuta <discordLink>Discordis</discordLink> või <matrixLink>Matrixis</matrixLink>.",
"error_boundary_unsupported_indexeddb_description": "Meie ntfy veebirakendus vajab korralikuks toimimiseks brauseri IndexedDB funktsionaalsust, aga sinu veebibrauser seda privaatses režiimis ei toeta.<br/><br/>See on nüüd õnnetu lugu küll, aga olemuslikult pole ntfy veebirakenduse kasutamisel privaatses režiimis eriti mõtet - kõike hoitakse ju brauseri hallatavas andmekogus. Lisateavet selle kohta leiad <githubLink>GitHubist siit</githubLink>, aga saad ka teema üle meiega arutleda <discordLink>Discordis</discordLink> või <matrixLink>Matrixis</matrixLink>.",
"account_usage_basis_ip_description": "Selle kasutajakonto statistika ja kasutuspiirangud põhinevad sinu IP-aadressil ja seega võivad nad olla teistega jagatud. Siin näidatud piirangud on hinnangulised ja põhinevad üldistel päringupiirangutel.",
"prefs_notifications_web_push_enabled": "Kasutusel serveris {{server}}",
"prefs_notifications_web_push_disabled_description": "Saad teavitusi siis, kui rakendus on töös (WebSocketi abil)",
"prefs_notifications_web_push_enabled_description": "Saad teavitusi siis, kui rakendus pole töös (Web Pushi abil)",
"prefs_notifications_web_push_title": "Teavitused taustal",
"prefs_notifications_min_priority_description_max": "Näita teavitusi siis, kui prioriteet on 5 (maksimaalne)",
"prefs_notifications_min_priority_description_x_or_higher": "Näita teavitusi siis, kui prioriteet on {{number}} ({{name}}) või kõrgem",
"prefs_notifications_sound_description_none": "Teavitused ei kasuta saabumisel helimärguannet",
"prefs_notifications_sound_description_some": "Teavitused kasutavad saabumisel helimärguannet {{sound}}",
"prefs_notifications_sound_no_sound": "Helimärguanne puudub",
"prefs_notifications_sound_play": "Esita valitud helimärguannet",
"prefs_notifications_min_priority_title": "Väikseim prioriteet",
"prefs_notifications_min_priority_description_any": "Näitan kõiki teavitusi ja seejuures ei arvesta prioriteetidega",
"account_upgrade_dialog_cancel_warning": "Sellega <strong>katkestad oma tellimuse</strong> ja {{date}} muutub sinu kasutajakonto tase madalamaks. Sel kuupäeval teemade reserveeringud tühistuvad ja puhverdatud sõnumid <strong>kustutatakse serverist</strong>.",
"account_upgrade_dialog_proration_info": "<strong>Summade jagamine</strong>: Kui muudad teenusepaketti paremaks, siis pead hinnavahe <strong>maksma kohe</strong>. Kui muudad teenusepaketti madalamaks, siis hinnavahe arvelt hüvituvad mõned järgmised maksed.",
"account_upgrade_dialog_reservations_warning_one": "Sinu praegune teenusepakett võimaldab senise paketiga võrreldes reserveerida vähem teemasid. Enne paketi muutmist <strong>palun esmalt kustuta vähemalt üks reserveering</strong>. Seda saad <Link>teha siin</Link>.",
"account_upgrade_dialog_reservations_warning_other": "Sinu praegune teenusepakett võimaldab senise paketiga võrreldes reserveerida vähem teemasid. Enne paketi muutmist <strong>palun esmalt kustuta vähemalt {{count}} reserveeringut</strong>. Seda saad <Link>teha siin</Link>.",
"prefs_users_description": "Oma kaitstud teemade kasutajaid saad lisada ja eemaldada siin. Palun arvesta, et kasutajanimi ja salasõna on salvestatud veebibrauseri kohalikus andmeruumis.",
"account_basics_cannot_edit_or_delete_provisioned_user": "Eelsisestatud kasutajat ei saa muuta ega kustutada",
"account_tokens_table_cannot_delete_or_edit_provisioned_token": "Eelsisestatud tunnusluba ei saa muuta ega kustutada"
} }

View File

@@ -15,7 +15,7 @@
"notifications_attachment_copy_url_title": "Copier l'URL de la pièce jointe dans le presse-papiers", "notifications_attachment_copy_url_title": "Copier l'URL de la pièce jointe dans le presse-papiers",
"notifications_attachment_open_title": "Aller à {{url}}", "notifications_attachment_open_title": "Aller à {{url}}",
"notifications_attachment_link_expired": "lien de téléchargement expiré", "notifications_attachment_link_expired": "lien de téléchargement expiré",
"nav_button_publish_message": "Publier la notification", "nav_button_publish_message": "Publier une notification",
"notifications_copied_to_clipboard": "Copié dans le presse-papiers", "notifications_copied_to_clipboard": "Copié dans le presse-papiers",
"alert_not_supported_title": "Notifications non prises en charge", "alert_not_supported_title": "Notifications non prises en charge",
"notifications_tags": "Étiquettes", "notifications_tags": "Étiquettes",
@@ -64,7 +64,7 @@
"notifications_actions_not_supported": "Cette action n'est pas supportée dans l'application web", "notifications_actions_not_supported": "Cette action n'est pas supportée dans l'application web",
"notifications_actions_http_request_title": "Envoyer une requête HTTP {{method}} à {{url}}", "notifications_actions_http_request_title": "Envoyer une requête HTTP {{method}} à {{url}}",
"publish_dialog_attachment_limits_quota_reached": "quota dépassé, {{remainingBytes}} restants", "publish_dialog_attachment_limits_quota_reached": "quota dépassé, {{remainingBytes}} restants",
"publish_dialog_tags_placeholder": "Liste séparée par des virgules d'étiquettes, par ex. avertissement,backup-srv1", "publish_dialog_tags_placeholder": "Liste d'étiquettes séparée par des virgules, par ex. avertissement,backup-srv1",
"publish_dialog_priority_label": "Priorité", "publish_dialog_priority_label": "Priorité",
"publish_dialog_click_label": "URL du clic", "publish_dialog_click_label": "URL du clic",
"publish_dialog_click_placeholder": "URL ouverte lors d'un clic sur la notification", "publish_dialog_click_placeholder": "URL ouverte lors d'un clic sur la notification",
@@ -272,7 +272,7 @@
"account_delete_dialog_button_submit": "Supprimer définitivement le compte", "account_delete_dialog_button_submit": "Supprimer définitivement le compte",
"account_delete_dialog_billing_warning": "Supprimer votre compte annule aussi immédiatement votre facturation. Vous n'aurez plus accès à votre tableau de bord de facturation.", "account_delete_dialog_billing_warning": "Supprimer votre compte annule aussi immédiatement votre facturation. Vous n'aurez plus accès à votre tableau de bord de facturation.",
"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 vers un tiers payant, la différence de prix sera débitée <strong>immédiatement</strong>. En passant d'un tiers payant a gratuit, votre solde sera utilisé pour payer de futur factures.", "account_upgrade_dialog_proration_info": "<strong>Proratisation</strong> : Lors d'un changement vers le haut entre plans payants, la différence de prix sera <strong>facturée immédiatement</strong>. En cas de diminutions vers un plan plus économique, la balance sera utilisée pour le paiement des factures suivantes.",
"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_other": "{{reservations}} sujets réservés", "account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} sujets réservés",
"account_upgrade_dialog_tier_features_messages_other": "{{messages}} messages journaliers", "account_upgrade_dialog_tier_features_messages_other": "{{messages}} messages journaliers",
@@ -403,5 +403,7 @@
"web_push_subscription_expiring_title": "Les notifications seront suspendues", "web_push_subscription_expiring_title": "Les notifications seront suspendues",
"web_push_subscription_expiring_body": "Ouvrez ntfy pour continuer à recevoir les notifications", "web_push_subscription_expiring_body": "Ouvrez ntfy pour continuer à recevoir les notifications",
"web_push_unknown_notification_title": "Notification inconnue reçue du serveur", "web_push_unknown_notification_title": "Notification inconnue reçue du serveur",
"web_push_unknown_notification_body": "Il est possible que vous deviez mettre à jour ntfy en ouvrant l'application web" "web_push_unknown_notification_body": "Il est possible que vous deviez mettre à jour ntfy en ouvrant l'application web",
"account_basics_cannot_edit_or_delete_provisioned_user": "Un utilisateur provisionné ne peut pas être modifié ou supprimé",
"account_tokens_table_cannot_delete_or_edit_provisioned_token": "Impossible de modifier ou de supprimer le jeton provisionné"
} }

View File

@@ -23,7 +23,7 @@
"nav_topics_title": "Topik yang dilanggani", "nav_topics_title": "Topik yang dilanggani",
"nav_button_subscribe": "Berlangganan ke topik", "nav_button_subscribe": "Berlangganan ke topik",
"alert_notification_permission_required_title": "Notifikasi dinonaktifkan", "alert_notification_permission_required_title": "Notifikasi dinonaktifkan",
"alert_notification_permission_required_description": "Berikan izin ke peramban untuk menampilkan notifikasi desktop.", "alert_notification_permission_required_description": "Berikan izin ke peramban web Anda untuk menampilkan notifikasi desktop",
"alert_not_supported_description": "Notifikasi tidak didukung dalam peramban Anda", "alert_not_supported_description": "Notifikasi tidak didukung dalam peramban Anda",
"notifications_attachment_open_title": "Pergi ke {{url}}", "notifications_attachment_open_title": "Pergi ke {{url}}",
"notifications_attachment_open_button": "Buka lampiran", "notifications_attachment_open_button": "Buka lampiran",
@@ -65,11 +65,11 @@
"publish_dialog_attachment_limits_file_reached": "melebihi batasan file {{fileSizeLimit}", "publish_dialog_attachment_limits_file_reached": "melebihi batasan file {{fileSizeLimit}",
"publish_dialog_attachment_limits_file_and_quota_reached": "melebihi batasan file dan kuota {{fileSizeLimit}}, hanya {{remainingBytes}}", "publish_dialog_attachment_limits_file_and_quota_reached": "melebihi batasan file dan kuota {{fileSizeLimit}}, hanya {{remainingBytes}}",
"publish_dialog_attachment_limits_quota_reached": "melebihi kuota, hanya {{remainingBytes}}", "publish_dialog_attachment_limits_quota_reached": "melebihi kuota, hanya {{remainingBytes}}",
"publish_dialog_priority_min": "Prioritas min.", "publish_dialog_priority_min": "Prioritas minimal",
"publish_dialog_priority_low": "Prioritas rendah", "publish_dialog_priority_low": "Prioritas rendah",
"publish_dialog_priority_default": "Prioritas bawaan", "publish_dialog_priority_default": "Prioritas bawaan",
"publish_dialog_priority_high": "Prioritas tinggi", "publish_dialog_priority_high": "Prioritas tinggi",
"publish_dialog_priority_max": "Prioritas maks.", "publish_dialog_priority_max": "Prioritas maksimal",
"publish_dialog_topic_label": "Nama topik", "publish_dialog_topic_label": "Nama topik",
"publish_dialog_message_placeholder": "Ketik sebuah pesan di sini", "publish_dialog_message_placeholder": "Ketik sebuah pesan di sini",
"publish_dialog_click_label": "Klik URL", "publish_dialog_click_label": "Klik URL",

View File

@@ -8,11 +8,11 @@
"message_bar_error_publishing": "Errore durante la pubblicazione della notifica", "message_bar_error_publishing": "Errore durante la pubblicazione della notifica",
"message_bar_show_dialog": "Mostra la finestra di dialogo di pubblicazione", "message_bar_show_dialog": "Mostra la finestra di dialogo di pubblicazione",
"message_bar_publish": "Pubblica messaggio", "message_bar_publish": "Pubblica messaggio",
"nav_topics_title": "Topic a cui si è iscritti", "nav_topics_title": "Argomenti a cui si è iscritti",
"nav_button_all_notifications": "Tutte le notifiche", "nav_button_all_notifications": "Tutte le notifiche",
"nav_button_settings": "Impostazioni", "nav_button_settings": "Impostazioni",
"nav_button_publish_message": "Pubblica notifica", "nav_button_publish_message": "Pubblica notifica",
"nav_button_subscribe": "Iscriviti al topic", "nav_button_subscribe": "Iscriviti all'argomento",
"nav_button_muted": "Notifiche disattivate", "nav_button_muted": "Notifiche disattivate",
"nav_button_connecting": "connessione", "nav_button_connecting": "connessione",
"alert_notification_permission_required_title": "Le notifiche sono disabilitate", "alert_notification_permission_required_title": "Le notifiche sono disabilitate",
@@ -31,17 +31,17 @@
"notifications_attachment_open_title": "Vai a {{url}}", "notifications_attachment_open_title": "Vai a {{url}}",
"notifications_attachment_open_button": "Apri allegato", "notifications_attachment_open_button": "Apri allegato",
"notifications_attachment_link_expires": "Il collegamento scade il {{date}}", "notifications_attachment_link_expires": "Il collegamento scade il {{date}}",
"notifications_attachment_link_expired": "link per il download scaduto", "notifications_attachment_link_expired": "collegamento per il download scaduto",
"notifications_attachment_file_image": "file immagine", "notifications_attachment_file_image": "file immagine",
"notifications_attachment_file_video": "file video", "notifications_attachment_file_video": "file video",
"action_bar_toggle_mute": "Abilita/disabilita le notifiche", "action_bar_toggle_mute": "Abilita/disabilita le notifiche",
"notifications_attachment_file_document": "altro documento", "notifications_attachment_file_document": "altro documento",
"notifications_click_copy_url_button": "Copia link", "notifications_click_copy_url_button": "Copia collegamento",
"notifications_click_open_button": "Apri link", "notifications_click_open_button": "Apri collegamento",
"notifications_actions_open_url_title": "Vai a {{url}}", "notifications_actions_open_url_title": "Vai a {{url}}",
"notifications_actions_not_supported": "Azione non supportata nell'app Web", "notifications_actions_not_supported": "Azione non supportata nell'app Web",
"notifications_none_for_topic_title": "Non hai ancora ricevuto alcuna notifica per questo topic.", "notifications_none_for_topic_title": "Non hai ancora ricevuto alcuna notifica per questo argomento.",
"notifications_none_for_topic_description": "Per inviare notifiche a questo argomento, è sufficiente PUT o POST all'URL del topic.", "notifications_none_for_topic_description": "Per inviare notifiche a questo argomento, è sufficiente PUT o POST all'URL dell'argomento.",
"notifications_none_for_any_title": "Non hai ricevuto alcuna notifica.", "notifications_none_for_any_title": "Non hai ricevuto alcuna notifica.",
"notifications_no_subscriptions_title": "Sembra che tu non abbia ancora abbonamenti.", "notifications_no_subscriptions_title": "Sembra che tu non abbia ancora abbonamenti.",
"notifications_example": "Esempio", "notifications_example": "Esempio",
@@ -63,9 +63,9 @@
"publish_dialog_priority_max": "Max. priorità", "publish_dialog_priority_max": "Max. priorità",
"publish_dialog_base_url_label": "URL del servizio", "publish_dialog_base_url_label": "URL del servizio",
"publish_dialog_base_url_placeholder": "URL del servizio, ad es. https://esempio.com", "publish_dialog_base_url_placeholder": "URL del servizio, ad es. https://esempio.com",
"publish_dialog_topic_label": "Nome topic", "publish_dialog_topic_label": "Nome argomento",
"publish_dialog_topic_placeholder": "Nome topic, ad es. avvisi_di_phil", "publish_dialog_topic_placeholder": "Nome argomento, ad es. avvisi_di_phil",
"publish_dialog_topic_reset": "Reset topic", "publish_dialog_topic_reset": "Reimposta argomento",
"publish_dialog_title_label": "Titolo", "publish_dialog_title_label": "Titolo",
"publish_dialog_title_placeholder": "Titolo della notifica, ad es. Avviso di spazio su disco", "publish_dialog_title_placeholder": "Titolo della notifica, ad es. Avviso di spazio su disco",
"publish_dialog_message_label": "Messaggio", "publish_dialog_message_label": "Messaggio",
@@ -97,13 +97,13 @@
"publish_dialog_attached_file_remove": "Rimuovi il file allegato", "publish_dialog_attached_file_remove": "Rimuovi il file allegato",
"publish_dialog_drop_file_here": "Trascina il file qui", "publish_dialog_drop_file_here": "Trascina il file qui",
"emoji_picker_search_clear": "Cancella ricerca", "emoji_picker_search_clear": "Cancella ricerca",
"subscribe_dialog_subscribe_title": "Iscriviti al topic", "subscribe_dialog_subscribe_title": "Iscriviti all'argomento",
"subscribe_dialog_subscribe_topic_placeholder": "Nome dell'argomento, ad es. avvisi_di_phil", "subscribe_dialog_subscribe_topic_placeholder": "Nome dell'argomento, ad es. avvisi_di_phil",
"subscribe_dialog_subscribe_base_url_label": "URL del servizio", "subscribe_dialog_subscribe_base_url_label": "URL del servizio",
"subscribe_dialog_subscribe_button_cancel": "Annulla", "subscribe_dialog_subscribe_button_cancel": "Annulla",
"subscribe_dialog_login_title": "Accesso richiesto", "subscribe_dialog_login_title": "Accesso richiesto",
"subscribe_dialog_login_username_label": "Nome utente, ad es. phil", "subscribe_dialog_login_username_label": "Nome utente, ad es. phil",
"subscribe_dialog_login_button_login": "Login", "subscribe_dialog_login_button_login": "Accesso",
"subscribe_dialog_error_user_anonymous": "anonimo", "subscribe_dialog_error_user_anonymous": "anonimo",
"prefs_notifications_sound_title": "Suono di notifica", "prefs_notifications_sound_title": "Suono di notifica",
"prefs_notifications_sound_description_some": "Le notifiche riproducono il suono {{sound}} quando arrivano", "prefs_notifications_sound_description_some": "Le notifiche riproducono il suono {{sound}} quando arrivano",
@@ -122,7 +122,7 @@
"prefs_notifications_delete_after_one_week_description": "Le notifiche vengono eliminate automaticamente dopo una settimana", "prefs_notifications_delete_after_one_week_description": "Le notifiche vengono eliminate automaticamente dopo una settimana",
"prefs_notifications_delete_after_one_month_description": "Le notifiche vengono eliminate automaticamente dopo un mese", "prefs_notifications_delete_after_one_month_description": "Le notifiche vengono eliminate automaticamente dopo un mese",
"prefs_users_title": "Gestisci gli utenti", "prefs_users_title": "Gestisci gli utenti",
"prefs_users_description": "Aggiungi/rimuovi utenti per i tuoi topic protetti qui. Tieni presente che nome utente e password sono memorizzati nella memoria locale del browser.", "prefs_users_description": "Aggiungi/rimuovi utenti per i tuoi argomenti protetti qui. Tieni presente che nome utente e password sono memorizzati nella memoria locale del browser.",
"prefs_users_table": "Tabella utenti", "prefs_users_table": "Tabella utenti",
"prefs_users_add_button": "Aggiungi utente", "prefs_users_add_button": "Aggiungi utente",
"prefs_users_edit_button": "Modifica utente", "prefs_users_edit_button": "Modifica utente",
@@ -158,16 +158,16 @@
"alert_notification_permission_required_description": "Concedi al tuo browser l'autorizzazione a visualizzare le notifiche sul desktop", "alert_notification_permission_required_description": "Concedi al tuo browser l'autorizzazione a visualizzare le notifiche sul desktop",
"alert_not_supported_title": "Notifiche non supportate", "alert_not_supported_title": "Notifiche non supportate",
"notifications_attachment_file_app": "file app Android", "notifications_attachment_file_app": "file app Android",
"notifications_no_subscriptions_description": "Fai clic sul link \"{{linktext}}\" per creare o iscriverti a un topic. Successivamente, puoi inviare messaggi tramite PUT o POST e riceverai le notifiche qui.", "notifications_no_subscriptions_description": "Fai clic sul collegamento \"{{linktext}}\" per creare o iscriverti a un argomento. Successivamente, puoi inviare messaggi tramite PUT o POST e riceverai le notifiche qui.",
"notifications_attachment_file_audio": "file audio", "notifications_attachment_file_audio": "file audio",
"notifications_none_for_any_description": "Per inviare notifiche a un topic, è sufficiente PUT o POST all'URL del topic. Ecco un esempio utilizzando uno dei tuoi topic.", "notifications_none_for_any_description": "Per inviare notifiche a un argomento, è sufficiente PUT o POST all'URL dell'argomento. Ecco un esempio utilizzando uno dei tuoi argomenti.",
"notifications_click_copy_url_title": "Copia l'URL del collegamento negli appunti", "notifications_click_copy_url_title": "Copia l'URL del collegamento negli appunti",
"prefs_notifications_sound_description_none": "Le notifiche non emettono alcun suono quando arrivano", "prefs_notifications_sound_description_none": "Le notifiche non emettono alcun suono quando arrivano",
"publish_dialog_delay_label": "Ritardo", "publish_dialog_delay_label": "Ritardo",
"publish_dialog_tags_placeholder": "Elenco di tag separato da virgole, ad es. avviso, backup-srv1", "publish_dialog_tags_placeholder": "Elenco di tag separato da virgole, ad es. avviso, backup-srv1",
"publish_dialog_click_placeholder": "URL che viene aperto quando si fa clic sulla notifica", "publish_dialog_click_placeholder": "URL che viene aperto quando si fa clic sulla notifica",
"publish_dialog_attach_placeholder": "Allega file tramite URL, ad es. https://f-droid.org/F-Droid.apk", "publish_dialog_attach_placeholder": "Allega file tramite URL, ad es. https://f-droid.org/F-Droid.apk",
"publish_dialog_chip_topic_label": "Cambia topic", "publish_dialog_chip_topic_label": "Cambia argomento",
"publish_dialog_details_examples_description": "Per esempi e una descrizione dettagliata di tutte le funzioni di invio, fare riferimento alla <docsLink>documentazione</docsLink>.", "publish_dialog_details_examples_description": "Per esempi e una descrizione dettagliata di tutte le funzioni di invio, fare riferimento alla <docsLink>documentazione</docsLink>.",
"publish_dialog_attached_file_filename_placeholder": "Nome file allegato", "publish_dialog_attached_file_filename_placeholder": "Nome file allegato",
"emoji_picker_search_placeholder": "Cerca emoji", "emoji_picker_search_placeholder": "Cerca emoji",
@@ -177,7 +177,7 @@
"subscribe_dialog_subscribe_button_subscribe": "Iscriviti", "subscribe_dialog_subscribe_button_subscribe": "Iscriviti",
"prefs_notifications_sound_play": "Riproduci il suono selezionato", "prefs_notifications_sound_play": "Riproduci il suono selezionato",
"prefs_notifications_min_priority_title": "Priorità minima", "prefs_notifications_min_priority_title": "Priorità minima",
"subscribe_dialog_login_description": "Questo argomento è protetto da password. Per favore inserisci username e password per iscriverti.", "subscribe_dialog_login_description": "Questo argomento è protetto da password. Per favore inserisci nome utente e password per iscriverti.",
"common_back": "Indietro", "common_back": "Indietro",
"subscribe_dialog_error_user_not_authorized": "Utente {{username}} non autorizzato", "subscribe_dialog_error_user_not_authorized": "Utente {{username}} non autorizzato",
"prefs_notifications_title": "Notifiche", "prefs_notifications_title": "Notifiche",
@@ -268,7 +268,7 @@
"publish_dialog_chip_call_no_verified_numbers_tooltip": "Nessun numero verificato", "publish_dialog_chip_call_no_verified_numbers_tooltip": "Nessun numero verificato",
"account_basics_phone_numbers_title": "Numeri di telefono", "account_basics_phone_numbers_title": "Numeri di telefono",
"account_basics_phone_numbers_dialog_description": "Per usare la funzionalità di notifica tramite chiamata telefonica, devi aggiungere e verificare almeno un numero di telefono. La verifica può essere fatta tramite SMS o chiamata telefonica.", "account_basics_phone_numbers_dialog_description": "Per usare la funzionalità di notifica tramite chiamata telefonica, devi aggiungere e verificare almeno un numero di telefono. La verifica può essere fatta tramite SMS o chiamata telefonica.",
"account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} topic riservato", "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} argomento riservato",
"account_upgrade_dialog_billing_contact_email": "Per domande di fatturazione, <Link>contattaci</Link> direttamente.", "account_upgrade_dialog_billing_contact_email": "Per domande di fatturazione, <Link>contattaci</Link> direttamente.",
"account_upgrade_dialog_tier_current_label": "Attuale", "account_upgrade_dialog_tier_current_label": "Attuale",
"account_basics_phone_numbers_dialog_number_label": "Numero di telefono", "account_basics_phone_numbers_dialog_number_label": "Numero di telefono",
@@ -276,13 +276,13 @@
"account_basics_phone_numbers_dialog_verify_button_sms": "Invia SMS", "account_basics_phone_numbers_dialog_verify_button_sms": "Invia SMS",
"account_basics_phone_numbers_no_phone_numbers_yet": "Ancora nessun numero di telefono", "account_basics_phone_numbers_no_phone_numbers_yet": "Ancora nessun numero di telefono",
"account_basics_phone_numbers_dialog_title": "Aggiungi un numero di telefono", "account_basics_phone_numbers_dialog_title": "Aggiungi un numero di telefono",
"account_upgrade_dialog_button_cancel": "Cancella", "account_upgrade_dialog_button_cancel": "Annulla",
"account_upgrade_dialog_billing_contact_website": "Per domande di fatturazione, visita per favore in nostro <Link>sito</Link>.", "account_upgrade_dialog_billing_contact_website": "Per domande di fatturazione, visita per favore in nostro <Link>sito</Link>.",
"account_upgrade_dialog_button_cancel_subscription": "Cancella iscrizione", "account_upgrade_dialog_button_cancel_subscription": "Annulla iscrizione",
"account_basics_phone_numbers_description": "Per notifiche via chiamata", "account_basics_phone_numbers_description": "Per notifiche via chiamata",
"account_basics_phone_numbers_copied_to_clipboard": "Numero di telefono copiato negli appunti", "account_basics_phone_numbers_copied_to_clipboard": "Numero di telefono copiato negli appunti",
"account_basics_phone_numbers_dialog_number_placeholder": "p. e. +391234567890", "account_basics_phone_numbers_dialog_number_placeholder": "es. +391234567890",
"account_basics_phone_numbers_dialog_code_placeholder": "p. e. 123456", "account_basics_phone_numbers_dialog_code_placeholder": "es. 123456",
"account_tokens_title": "Token d'accesso", "account_tokens_title": "Token d'accesso",
"account_upgrade_dialog_tier_price_billed_monthly": "{{price}} all'anno. Addebitato annualmente.", "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} all'anno. Addebitato annualmente.",
"account_basics_phone_numbers_dialog_channel_call": "Chiama", "account_basics_phone_numbers_dialog_channel_call": "Chiama",
@@ -296,7 +296,7 @@
"account_upgrade_dialog_tier_selected_label": "Selezionato", "account_upgrade_dialog_tier_selected_label": "Selezionato",
"account_upgrade_dialog_button_update_subscription": "Aggiorna iscrizione", "account_upgrade_dialog_button_update_subscription": "Aggiorna iscrizione",
"account_usage_attachment_storage_title": "Archivio allegati", "account_usage_attachment_storage_title": "Archivio allegati",
"account_delete_dialog_description": "Il tuo account sarà permanentemente cancellato assieme a tutti i tuoi dati presenti sul server. Dopo la cancellazione, la tua username non sarà disponibile per 7 giorni. Se desideri davvero procedere, inserisci la tua password nella seguente casella.", "account_delete_dialog_description": "Il tuo account sarà permanentemente eliminato insieme a tutti i tuoi dati presenti sul server. Dopo l'eliminazione, il tuo nome utente non sarà disponibile per 7 giorni. Se desideri davvero procedere, inserisci la tua password nella seguente casella.",
"account_delete_dialog_button_cancel": "Annulla", "account_delete_dialog_button_cancel": "Annulla",
"account_usage_calls_title": "Chiamate effettuate", "account_usage_calls_title": "Chiamate effettuate",
"account_delete_description": "Elimina permanentemente il tuo account", "account_delete_description": "Elimina permanentemente il tuo account",
@@ -326,7 +326,7 @@
"account_tokens_dialog_title_edit": "Modifica token di accesso", "account_tokens_dialog_title_edit": "Modifica token di accesso",
"account_tokens_dialog_button_create": "Crea token", "account_tokens_dialog_button_create": "Crea token",
"account_tokens_dialog_button_update": "Aggiorna token", "account_tokens_dialog_button_update": "Aggiorna token",
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} e-mails giornaliere", "account_upgrade_dialog_tier_features_emails_one": "{{emails}} email giornaliere",
"account_upgrade_dialog_tier_features_messages_other": "{{messages}} messaggi giornalieri", "account_upgrade_dialog_tier_features_messages_other": "{{messages}} messaggi giornalieri",
"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}} spazio di archiviazione totale", "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} spazio di archiviazione totale",
@@ -348,7 +348,7 @@
"account_tokens_dialog_title_create": "Crea token di accesso", "account_tokens_dialog_title_create": "Crea token di accesso",
"account_tokens_dialog_button_cancel": "Annulla", "account_tokens_dialog_button_cancel": "Annulla",
"web_push_unknown_notification_body": "Potrebbe essere necessario aggiornare ntfy aprendo l'app web", "web_push_unknown_notification_body": "Potrebbe essere necessario aggiornare ntfy aprendo l'app web",
"account_upgrade_dialog_proration_info": "<strong>Prorata</strong>: quando si esegue l'upgrade tra piani a pagamento, la differenza di prezzo verrà <strong>addebitata immediatamente</strong>. Quando si esegue il downgrade a un livello inferiore, il saldo verrà utilizzato per pagare i periodi di fatturazione futuri.", "account_upgrade_dialog_proration_info": "<strong>Prorata</strong>: quando si esegue l'aggiornamento tra piani a pagamento, la differenza di prezzo verrà <strong>addebitata immediatamente</strong>. Quando si esegue il ritorna ad un livello inferiore, il saldo verrà utilizzato per pagare i periodi di fatturazione futuri.",
"account_tokens_table_last_access_header": "Ultimo accesso", "account_tokens_table_last_access_header": "Ultimo accesso",
"account_tokens_table_expires_header": "Scade", "account_tokens_table_expires_header": "Scade",
"account_tokens_table_never_expires": "Non scade mai", "account_tokens_table_never_expires": "Non scade mai",

View File

@@ -403,5 +403,7 @@
"prefs_appearance_theme_system": "システム (既定)", "prefs_appearance_theme_system": "システム (既定)",
"prefs_appearance_theme_dark": "ダークモード", "prefs_appearance_theme_dark": "ダークモード",
"web_push_unknown_notification_title": "不明な通知を受信しました", "web_push_unknown_notification_title": "不明な通知を受信しました",
"web_push_unknown_notification_body": "ウェブアプリを開いてntfyをアップデートする必要があります" "web_push_unknown_notification_body": "ウェブアプリを開いてntfyをアップデートする必要があります",
"account_basics_cannot_edit_or_delete_provisioned_user": "自動作成されたユーザーの編集や削除はできません",
"account_tokens_table_cannot_delete_or_edit_provisioned_token": "自動作成されたトークンは編集や削除はできません"
} }

View File

@@ -187,5 +187,6 @@
"prefs_users_dialog_username_label": "사용자 이름, 예를 들면 phil", "prefs_users_dialog_username_label": "사용자 이름, 예를 들면 phil",
"prefs_users_dialog_password_label": "비밀번호", "prefs_users_dialog_password_label": "비밀번호",
"priority_max": "최상", "priority_max": "최상",
"error_boundary_description": "이것은 당연히 발생되어서는 안됩니다. 굉장히 죄송합니다.<br/>가능하시다면 <githubLink>이 문제를 깃허브에 제보</githubLink>해 주시거나, <discordLink>디스코드 서버</discordLink>나 <matrixLink>Matrix</matrixLink>를 통해 알려주세요." "error_boundary_description": "이것은 당연히 발생되어서는 안됩니다. 굉장히 죄송합니다.<br/>가능하시다면 <githubLink>이 문제를 깃허브에 제보</githubLink>해 주시거나, <discordLink>디스코드 서버</discordLink>나 <matrixLink>Matrix</matrixLink>를 통해 알려주세요.",
"common_copy_to_clipboard": "클립보드에 복사"
} }

View File

@@ -0,0 +1,54 @@
{
"common_cancel": "Откажи",
"common_save": "Зачувај",
"common_add": "Додади",
"common_back": "Назад",
"common_copy_to_clipboard": "Копирај",
"action_bar_profile_logout": "Одјави се",
"action_bar_sign_in": "Најави се",
"action_bar_sign_up": "Регистрирај се",
"message_bar_type_message": "Пишете порака тука",
"action_bar_profile_title": "Профил",
"action_bar_profile_settings": "Подесувања",
"signup_form_username": "Корисничко име",
"signup_form_password": "Лозинка",
"signup_form_confirm_password": "Повтори лозинка",
"login_form_button_submit": "Најави се",
"login_link_signup": "Регистрирај се",
"signup_form_button_submit": "Регистрирај се",
"action_bar_settings": "Подесувања",
"signup_title": "Создади ntfy профил",
"signup_form_toggle_password_visibility": "Покажи/сокриј лозинка",
"signup_already_have_account": "Имате профил? Најавете се!",
"signup_disabled": "Регистрирање е исклучено",
"signup_error_username_taken": "Корисничкото име {{username}} е веќе земено",
"signup_error_creation_limit_reached": "Лимитот на создадени профили е надминат",
"login_title": "Најавете се на вашиот ntfy профил",
"login_disabled": "Најавувањето е исклучено",
"action_bar_show_menu": "Покажи мени",
"action_bar_logo_alt": "ntfy лого",
"action_bar_account": "Профил",
"action_bar_change_display_name": "Промени покажано име",
"action_bar_reservation_add": "Резервирај тема",
"action_bar_reservation_edit": "Промени резервација",
"account_basics_title": "Профил",
"account_basics_username_title": "Корисничко име",
"nav_button_account": "Профил",
"nav_button_settings": "Подесувања",
"nav_button_documentation": "Документација",
"notifications_attachment_copy_url_button": "Копирај URL",
"publish_dialog_message_label": "Порака",
"action_bar_reservation_delete": "Отстрани резервација",
"action_bar_reservation_limit_reached": "Достигната е границата",
"action_bar_send_test_notification": "Испрати тест нотификација",
"action_bar_clear_notifications": "Исчисти ги сите нотификации",
"action_bar_mute_notifications": "Загуши ги нотификациите",
"action_bar_unsubscribe": "Отпиши се",
"action_bar_toggle_action_menu": "Отвори/затвори мени за акција",
"message_bar_error_publishing": "Грешки при публикација на нотификацијата",
"message_bar_show_dialog": "Покажи дијалог за публикација",
"nav_topics_title": "Претплатени теми",
"nav_button_all_notifications": "Сите нотификации",
"nav_button_publish_message": "Објави нотификација",
"nav_button_subscribe": "Претплати се на тема"
}

View File

@@ -309,5 +309,100 @@
"account_delete_dialog_button_cancel": "Cancelar", "account_delete_dialog_button_cancel": "Cancelar",
"account_upgrade_dialog_cancel_warning": "Isto irá <strong>cancelar a sua assinatura</strong>, e fazer downgrade da sua conta em {{date}}. Nessa data, tópicos reservados bem como mensagens guardadas no servidor <strong>serão eliminados</strong>.", "account_upgrade_dialog_cancel_warning": "Isto irá <strong>cancelar a sua assinatura</strong>, e fazer downgrade da sua conta em {{date}}. Nessa data, tópicos reservados bem como mensagens guardadas no servidor <strong>serão eliminados</strong>.",
"account_upgrade_dialog_proration_info": "<strong>Proporção</strong>: Quando atualizar entre planos pagos, a diferença de preço será <strong>debitada imediatamente</strong>. Quando efetuar um downgrade para um escalão inferior, o saldo disponível será usado para futuros períodos de faturação.", "account_upgrade_dialog_proration_info": "<strong>Proporção</strong>: Quando atualizar entre planos pagos, a diferença de preço será <strong>debitada imediatamente</strong>. Quando efetuar um downgrade para um escalão inferior, o saldo disponível será usado para futuros períodos de faturação.",
"prefs_users_description_no_sync": "Utilizadores e palavras-passe não estão sincronizados com a sua conta." "prefs_users_description_no_sync": "Utilizadores e palavras-passe não estão sincronizados com a sua conta.",
"account_upgrade_dialog_reservations_warning_one": "O nível selecionado permite menos tópicos reservados do que o nível atual. Antes de alterar o seu nível, <strong>apague pelo menos uma reserva</strong>. Pode remover reservas nas <Link>Configurações</Link>.",
"account_upgrade_dialog_reservations_warning_other": "O nível selecionado permite menos tópicos reservados do que o seu nível atual. Antes de mudar o seu nível, <strong>por favor apague ao menos {{count}} reservas</strong>. Pode remover reservas nas <Link>Configurações</Link>.",
"account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} tópico reservado",
"account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} tópicos reservados",
"account_upgrade_dialog_tier_features_no_reservations": "Sem tópicos reservados",
"account_upgrade_dialog_tier_features_messages_one": "{{messages}} mensagen diária",
"account_upgrade_dialog_tier_features_messages_other": "{{messages}} mensagens diárias",
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} email diário",
"account_upgrade_dialog_tier_features_emails_other": "{{emails}} emails diários",
"account_upgrade_dialog_tier_features_calls_one": "{{calls}} chamadas diárias",
"account_upgrade_dialog_tier_features_calls_other": "{{calls}} chamadas telefônicas diárias",
"account_upgrade_dialog_tier_features_no_calls": "Nenhuma chamada",
"account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} por ficheiro",
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} armazenamento total",
"account_upgrade_dialog_tier_price_per_month": "mês",
"account_upgrade_dialog_tier_price_billed_monthly": "{{price}} por ano. Cobrado mensalmente.",
"account_upgrade_dialog_tier_price_billed_yearly": "{{price}} cobrado anualmente. Gravar {{save}}.",
"account_upgrade_dialog_tier_selected_label": "Selecionado",
"account_upgrade_dialog_tier_current_label": "Atual",
"account_upgrade_dialog_billing_contact_email": "Para questões de cobrança, <Link>entre em contacto conosco</Link> diretamente.",
"account_upgrade_dialog_billing_contact_website": "Para perguntas sobre o faturamento, consulte o nosso <Link>website</Link>.",
"account_upgrade_dialog_button_cancel": "Cancelar",
"account_upgrade_dialog_button_redirect_signup": "Cadastre-se agora",
"account_upgrade_dialog_button_pay_now": "Pague agora para assinar",
"account_upgrade_dialog_button_cancel_subscription": "Cancelar assinatura",
"account_upgrade_dialog_button_update_subscription": "Atualizar assinatura",
"account_tokens_title": "Tokens de Acesso",
"account_tokens_description": "Use tokens de acesso ao publicar e assinar por meio da API ntfy, para que não precise enviar as credenciais da sua conta. Consulte a <Link>documentação</Link> para saber mais.",
"account_tokens_table_token_header": "Token",
"account_tokens_table_label_header": "Rótulo",
"account_tokens_table_last_access_header": "Último acesso",
"account_tokens_table_expires_header": "Expira",
"account_tokens_table_never_expires": "Nunca expira",
"account_tokens_table_current_session": "Sessão atual do navegador",
"account_tokens_table_copied_to_clipboard": "Token de acesso copiado",
"account_tokens_table_cannot_delete_or_edit": "Não é possível editar ou apagar o token da sessão atual",
"account_tokens_table_create_token_button": "Criar token de acesso",
"account_tokens_table_last_origin_tooltip": "Do endereço IP {{ip}}, clique para pesquisar",
"account_tokens_dialog_title_create": "Criar token de acesso",
"account_tokens_dialog_title_edit": "Editar token de acesso",
"account_tokens_dialog_title_delete": "Apagar token de acesso",
"account_tokens_dialog_label": "Rótulo, por exemplo, notificações de Radarr",
"account_tokens_dialog_button_create": "Criar token",
"account_tokens_dialog_button_update": "Atualizar token",
"account_tokens_dialog_button_cancel": "Cancelar",
"account_tokens_dialog_expires_label": "O token de acesso expira em",
"account_tokens_dialog_expires_unchanged": "Deixar a data de validade inalterada",
"account_tokens_dialog_expires_x_hours": "O token expira em {{hours}} horas",
"account_tokens_dialog_expires_x_days": "O token expira em {{days}} dias",
"account_tokens_dialog_expires_never": "O token nunca expira",
"account_tokens_delete_dialog_title": "Apagar token de acesso",
"account_tokens_delete_dialog_description": "Antes de apagar um token de acesso, certifique-se de que nenhuma aplicação ou script esteja usando-lo ativamente. <strong>Esta ação não pode ser desfeita</strong>.",
"account_tokens_delete_dialog_submit_button": "Apagar token permanentemente",
"prefs_notifications_web_push_title": "Notificações em segundo plano",
"prefs_notifications_web_push_enabled_description": "As notificações são recebidas mesmo quando a aplicação Web não está em execução (via Web Push)",
"prefs_notifications_web_push_disabled_description": "As notificações são recebidas quando a aplicação Web está em execução (via WebSocket)",
"prefs_notifications_web_push_enabled": "Ativado para {{server}}",
"prefs_notifications_web_push_disabled": "Desativado",
"prefs_users_table_cannot_delete_or_edit": "Não é possível apagar ou editar o utilizador conectado",
"prefs_appearance_theme_title": "Tema",
"prefs_appearance_theme_system": "Sistema (padrão)",
"prefs_appearance_theme_dark": "Modo escuro",
"prefs_appearance_theme_light": "Modo claro",
"prefs_reservations_title": "Tópicos reservados",
"prefs_reservations_description": "Pode reservar nomes de tópicos para uso pessoal aqui. A reserva de um tópico lhe dá propriedade sobre ele e permite que defina permissões de acesso para outros utilizadores sobre o tópico.",
"prefs_reservations_limit_reached": "Atingiu o seu limite de tópicos reservados.",
"prefs_reservations_add_button": "Adicionar tópico reservado",
"prefs_reservations_edit_button": "Editar o acesso ao tópico",
"prefs_reservations_delete_button": "Redefinir o acesso ao tópico",
"prefs_reservations_table": "Tabela de tópicos reservados",
"prefs_reservations_table_topic_header": "Tópico",
"prefs_reservations_table_access_header": "Acesso",
"prefs_reservations_table_everyone_deny_all": "Somente eu posso publicar e me inscrever",
"prefs_reservations_table_everyone_read_only": "Posso publicar e me inscrever, todos podem se inscrever",
"prefs_reservations_table_everyone_write_only": "Posso publicar e me inscrever, todos podem publicar",
"prefs_reservations_table_everyone_read_write": "Todos podem publicar e se inscreverem",
"prefs_reservations_table_not_subscribed": "Não inscrito",
"prefs_reservations_table_click_to_subscribe": "Clique para se inscrever",
"prefs_reservations_dialog_title_add": "Reservar tópico",
"prefs_reservations_dialog_title_edit": "Editar tópico reservado",
"prefs_reservations_dialog_title_delete": "Apagar reserva de tópico",
"prefs_reservations_dialog_description": "A reserva de um tópico lhe dá propriedade sobre ele e permite definir permissões de acesso para outros utilizadores sobre o tópico.",
"prefs_reservations_dialog_topic_label": "Tópico",
"prefs_reservations_dialog_access_label": "Acesso",
"reservation_delete_dialog_description": "A remoção de uma reserva abre mão da propriedade sobre o tópico e permite que outros o reservem. Pode manter ou apagar as mensagens e os anexos existentes.",
"reservation_delete_dialog_action_keep_title": "Manter mensagens e anexos em cache",
"reservation_delete_dialog_action_keep_description": "As mensagens e os anexos armazenados em cache no servidor ficarão visíveis publicamente para as pessoas que souberem o nome do tópico.",
"reservation_delete_dialog_action_delete_title": "Apagar mensagens e anexos armazenados em cache",
"reservation_delete_dialog_action_delete_description": "As mensagens e os anexos armazenados em cache serão apagados permanentemente. Esta ação não pode ser desfeita.",
"reservation_delete_dialog_submit_button": "Apagar reserva",
"error_boundary_button_reload_ntfy": "Recarregar ntfy",
"web_push_subscription_expiring_title": "As notificações serão pausadas",
"web_push_subscription_expiring_body": "Abra o ntfy para continuar recebendo notificações",
"web_push_unknown_notification_title": "Notificação desconhecida recebida do servidor",
"web_push_unknown_notification_body": "Talvez seja necessário atualizar o ntfy abrindo a aplicação da Web"
} }

View File

@@ -301,7 +301,7 @@
"publish_dialog_checkbox_markdown": "Formatar como Markdown", "publish_dialog_checkbox_markdown": "Formatar como Markdown",
"subscribe_dialog_subscribe_use_another_background_info": "Notificações de outros servidores não serão recebidas quando o web app não estiver aberto", "subscribe_dialog_subscribe_use_another_background_info": "Notificações de outros servidores não serão recebidas quando o web app não estiver aberto",
"account_usage_basis_ip_description": "As estatísticas e limites de uso desta conta são baseados no seu endereço IP, portanto, podem ser compartilhados com outros usuários. Os limites mostrados acima são aproximados com base nos limites de taxa existentes.", "account_usage_basis_ip_description": "As estatísticas e limites de uso desta conta são baseados no seu endereço IP, portanto, podem ser compartilhados com outros usuários. Os limites mostrados acima são aproximados com base nos limites de taxa existentes.",
"account_usage_cannot_create_portal_session": "Não foi possível abrir o portal de cobrança", "account_usage_cannot_create_portal_session": "Não é possível abrir o portal de cobrança",
"account_delete_description": "Deletar sua conta permanentemente", "account_delete_description": "Deletar sua conta permanentemente",
"account_delete_dialog_button_cancel": "Cancelar", "account_delete_dialog_button_cancel": "Cancelar",
"account_delete_dialog_button_submit": "Deletar conta permanentemente", "account_delete_dialog_button_submit": "Deletar conta permanentemente",
@@ -342,7 +342,7 @@
"account_tokens_table_expires_header": "Expira", "account_tokens_table_expires_header": "Expira",
"prefs_users_description_no_sync": "Usuários e senhas não estão sincronizados com a sua conta.", "prefs_users_description_no_sync": "Usuários e senhas não estão sincronizados com a sua conta.",
"account_tokens_description": "Use tokens de acesso ao publicar e assinar por meio da API ntfy, para que você não precise enviar as credenciais da sua conta. Consulte a <Link>documentação</Link> para saber mais.", "account_tokens_description": "Use tokens de acesso ao publicar e assinar por meio da API ntfy, para que você não precise enviar as credenciais da sua conta. Consulte a <Link>documentação</Link> para saber mais.",
"account_tokens_table_cannot_delete_or_edit": "Não é possível editar ou excluir o token da sessão atual", "account_tokens_table_cannot_delete_or_edit": "Não é possível editar ou apagar o token da sessão atual",
"account_tokens_dialog_title_edit": "Editar token de acesso", "account_tokens_dialog_title_edit": "Editar token de acesso",
"account_tokens_dialog_title_delete": "Excluir token de acesso", "account_tokens_dialog_title_delete": "Excluir token de acesso",
"prefs_reservations_table_everyone_read_write": "Todos podem publicar e se inscrever", "prefs_reservations_table_everyone_read_write": "Todos podem publicar e se inscrever",
@@ -369,7 +369,7 @@
"account_tokens_dialog_button_update": "Atualizar token", "account_tokens_dialog_button_update": "Atualizar token",
"prefs_reservations_table": "Tabela de tópicos reservados", "prefs_reservations_table": "Tabela de tópicos reservados",
"prefs_reservations_table_everyone_deny_all": "Somente eu posso publicar e me inscrever", "prefs_reservations_table_everyone_deny_all": "Somente eu posso publicar e me inscrever",
"account_tokens_delete_dialog_description": "Antes de excluir um token de acesso, certifique-se de que nenhum aplicativo ou script o esteja usando ativamente. <strong>Esta ação não pode ser desfeita</strong>.", "account_tokens_delete_dialog_description": "Antes de apagar um token de acesso, certifique-se de que nenhum aplicativo ou script o esteja usando ativamente. <strong>Esta ação não pode ser desfeita</strong>.",
"account_tokens_delete_dialog_submit_button": "Excluir token permanentemente", "account_tokens_delete_dialog_submit_button": "Excluir token permanentemente",
"account_tokens_dialog_expires_x_hours": "O token expira em {{hours}} horas", "account_tokens_dialog_expires_x_hours": "O token expira em {{hours}} horas",
"account_tokens_dialog_expires_x_days": "O token expira em {{days}} dias", "account_tokens_dialog_expires_x_days": "O token expira em {{days}} dias",
@@ -383,7 +383,7 @@
"prefs_notifications_web_push_enabled": "Ativado para {{server}}", "prefs_notifications_web_push_enabled": "Ativado para {{server}}",
"prefs_notifications_web_push_disabled": "Desativado", "prefs_notifications_web_push_disabled": "Desativado",
"prefs_appearance_theme_title": "Tema", "prefs_appearance_theme_title": "Tema",
"prefs_users_table_cannot_delete_or_edit": "Não é possível excluir ou editar o usuário conectado", "prefs_users_table_cannot_delete_or_edit": "Não é possível apagar ou editar o usuário conectado",
"prefs_appearance_theme_system": "Sistema (padrão)", "prefs_appearance_theme_system": "Sistema (padrão)",
"prefs_appearance_theme_dark": "Modo escuro", "prefs_appearance_theme_dark": "Modo escuro",
"prefs_appearance_theme_light": "Modo claro", "prefs_appearance_theme_light": "Modo claro",

View File

@@ -242,5 +242,123 @@
"account_usage_attachment_storage_title": "Stocare atașamente", "account_usage_attachment_storage_title": "Stocare atașamente",
"account_usage_basis_ip_description": "Statistica și limitele de utilizare pentru acest cont se bazează pe adresa ta IP, așadar pot fi partajate cu alți utilizatori. Limitele afișate mai sus sunt aproximative, bazate pe limitele de viteză existente.", "account_usage_basis_ip_description": "Statistica și limitele de utilizare pentru acest cont se bazează pe adresa ta IP, așadar pot fi partajate cu alți utilizatori. Limitele afișate mai sus sunt aproximative, bazate pe limitele de viteză existente.",
"account_usage_reservations_none": "Nu există subiecte rezervate pentru acest cont", "account_usage_reservations_none": "Nu există subiecte rezervate pentru acest cont",
"account_basics_tier_canceled_subscription": "Abonamentul tău a fost anulat și va fi retrogradat la un cont gratuit în data de {{date}}." "account_basics_tier_canceled_subscription": "Abonamentul tău a fost anulat și va fi retrogradat la un cont gratuit în data de {{date}}.",
"account_delete_dialog_label": "Parolă",
"account_delete_dialog_button_cancel": "Anulează",
"account_delete_dialog_button_submit": "Șterge permanent contul",
"account_delete_dialog_billing_warning": "Ștergerea contului tău anulează imediat și abonamentul de facturare. Nu vei mai avea acces la tabloul de bord pentru facturare.",
"account_upgrade_dialog_title": "Schimbă nivelul contului",
"account_upgrade_dialog_interval_monthly": "Lunar",
"account_upgrade_dialog_interval_yearly": "Anual",
"account_upgrade_dialog_interval_yearly_discount_save": "economisești {{discount}}%",
"account_upgrade_dialog_interval_yearly_discount_save_up_to": "economisești până la {{discount}}%",
"prefs_notifications_title": "Notificări",
"prefs_notifications_sound_description_none": "Notificările nu redau niciun sunet atunci când sosesc",
"prefs_notifications_sound_description_some": "Notificările redau sunetul {{sound}} atunci când sosesc",
"prefs_notifications_min_priority_description_any": "Se afișează toate notificările, indiferent de prioritate",
"prefs_notifications_min_priority_description_x_or_higher": "Afișează notificările dacă prioritatea este {{number}} ({{name}}) sau mai mare",
"prefs_notifications_min_priority_description_max": "Afișează notificări dacă prioritatea este 5 (maxim)",
"prefs_notifications_delete_after_title": "Șterge notificările",
"prefs_notifications_delete_after_never_description": "Notificările nu sunt niciodată șterse automat",
"prefs_notifications_delete_after_three_hours_description": "Notificările sunt șterse automat după trei ore",
"prefs_notifications_delete_after_one_day_description": "Notificările sunt șterse automat după o zi",
"prefs_notifications_delete_after_one_week_description": "Notificările sunt șterse automat după o săptămână",
"prefs_notifications_delete_after_one_month_description": "Notificările sunt șterse automat după o lună",
"prefs_notifications_web_push_title": "Notificări în fundal",
"prefs_notifications_web_push_enabled_description": "Notificările sunt primite chiar și atunci când aplicația web nu rulează (prin Web Push)",
"web_push_subscription_expiring_title": "Notificările vor fi suspendate",
"web_push_subscription_expiring_body": "Deschide ntfy pentru a continua să primești notificări",
"account_upgrade_dialog_tier_features_reservations_one": "subiect rezervat {{reservations}}",
"account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} subiecte rezervate",
"account_upgrade_dialog_tier_features_no_reservations": "Nu există subiecte rezervate",
"account_upgrade_dialog_tier_features_messages_one": "{{messages}} mesaj zilnic",
"account_upgrade_dialog_tier_features_messages_other": "{{messages}} mesaje zilnice",
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} e-mail zilnic",
"account_upgrade_dialog_tier_features_emails_other": "{{emails}} e-mailuri zilnice",
"account_upgrade_dialog_tier_features_calls_one": "{{calls}} apeluri telefonice zilnice",
"account_upgrade_dialog_tier_features_calls_other": "{{calls}} apeluri telefonice zilnice",
"account_upgrade_dialog_tier_features_no_calls": "Fără apeluri telefonice",
"account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} per fișier",
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} stocare totală",
"account_upgrade_dialog_tier_price_per_month": "lună",
"account_upgrade_dialog_tier_price_billed_monthly": "{{price}} pe an. Facturat lunar.",
"account_upgrade_dialog_tier_selected_label": "Selectat",
"account_upgrade_dialog_tier_current_label": "Actual",
"account_upgrade_dialog_button_cancel": "Anulează",
"account_upgrade_dialog_button_redirect_signup": "Înscrie-te acum",
"account_upgrade_dialog_button_pay_now": "Plătește acum și abonează-te",
"account_upgrade_dialog_button_cancel_subscription": "Anulează abonamentul",
"account_tokens_table_token_header": "Token",
"account_tokens_table_label_header": "Etichetă",
"account_tokens_table_last_access_header": "Ultimul acces",
"account_tokens_table_expires_header": "Expiră",
"account_tokens_table_never_expires": "Nu expiră niciodată",
"account_tokens_table_current_session": "Sesiunea curentă a browserului",
"account_tokens_table_copied_to_clipboard": "Tokenul de acces a fost copiat",
"account_tokens_table_last_origin_tooltip": "De la adresa IP {{ip}}, faceți clic pentru a căuta",
"account_tokens_dialog_title_create": "Crează un token de acces",
"account_tokens_dialog_title_edit": "Modifică tokenul de acces",
"account_tokens_dialog_title_delete": "Șterge tokenul de acces",
"account_tokens_dialog_label": "Etichetă, de exemplu, notificări Radarr",
"account_tokens_dialog_button_create": "Crează un token",
"account_tokens_dialog_button_update": "Actualizare token",
"account_tokens_dialog_button_cancel": "Anulează",
"account_tokens_dialog_expires_label": "Tokenul de acces expiră în",
"account_tokens_dialog_expires_never": "Tokenul nu expiră niciodată",
"account_tokens_delete_dialog_title": "Șterge tokenul de acces",
"account_tokens_delete_dialog_submit_button": "Șterge definitiv tokenul",
"prefs_notifications_sound_title": "Sunet de notificare",
"prefs_notifications_sound_no_sound": "Niciun sunet",
"prefs_notifications_sound_play": "Redă sunetul selectat",
"prefs_notifications_min_priority_title": "Prioritate minimă",
"prefs_notifications_min_priority_any": "Orice prioritate",
"prefs_notifications_min_priority_low_and_higher": "Prioritate scăzută și mai mare",
"prefs_notifications_min_priority_default_and_higher": "Prioritate implicită și mai mare",
"prefs_notifications_min_priority_high_and_higher": "Prioritate ridicată și mai mare",
"prefs_notifications_min_priority_max_only": "Numai prioritate maximă",
"prefs_notifications_delete_after_never": "Niciodată",
"prefs_notifications_delete_after_three_hours": "După trei ore",
"prefs_notifications_delete_after_one_day": "După o zi",
"prefs_notifications_delete_after_one_week": "După o săptămână",
"prefs_notifications_delete_after_one_month": "După o lună",
"prefs_notifications_web_push_disabled_description": "Notificările sunt primite atunci când aplicația web rulează (prin WebSocket)",
"prefs_notifications_web_push_enabled": "Activat pentru {{server}}",
"prefs_notifications_web_push_disabled": "Dezactivat",
"prefs_users_title": "Gestionează utilizatorii",
"prefs_users_description_no_sync": "Utilizatorii și parolele nu sunt sincronizate cu contul tău.",
"prefs_users_table": "Tabel utilizatori",
"prefs_users_add_button": "Adăugă utilizator",
"prefs_users_edit_button": "Modifică utilizatorul",
"prefs_users_delete_button": "Șterge utilizatorul",
"prefs_users_table_cannot_delete_or_edit": "Nu se poate șterge sau modifica utilizatorul conectat",
"prefs_users_table_user_header": "Utilizator",
"prefs_users_table_base_url_header": "URL-ul serviciului",
"prefs_users_dialog_title_add": "Adaugă utilizator",
"prefs_users_dialog_title_edit": "Modifică utilizatorul",
"prefs_users_dialog_base_url_label": "URL-ul serviciului, de exemplu https://ntfy.sh",
"prefs_users_dialog_username_label": "Nume de utilizator, de ex. ionel",
"prefs_users_dialog_password_label": "Parolă",
"prefs_appearance_title": "Aspect",
"prefs_appearance_language_title": "Limbă",
"prefs_appearance_theme_title": "Temă",
"prefs_appearance_theme_system": "Sistem (implicit)",
"prefs_appearance_theme_dark": "Mod întunecat",
"prefs_appearance_theme_light": "Mod luminos",
"prefs_reservations_title": "Subiecte rezervate",
"prefs_reservations_limit_reached": "Ai atins limita de subiecte rezervate.",
"prefs_reservations_add_button": "Adaugă un subiect rezervat",
"prefs_reservations_delete_button": "Resetează accesul la topic",
"prefs_reservations_table_access_header": "Acces",
"prefs_reservations_table_everyone_deny_all": "Numai eu pot publica și mă pot abona",
"prefs_reservations_table_not_subscribed": "Neabonat",
"prefs_reservations_dialog_access_label": "Acces",
"reservation_delete_dialog_action_keep_title": "Păstrează mesajele și atașamentele în cache",
"prefs_users_description": "Adaugă/elimină utilizatori pentru subiectele protejate aici. Reține că numele de utilizator și parola sunt stocate în memoria locală a browserului.",
"reservation_delete_dialog_submit_button": "Șterge rezervarea",
"priority_min": "minim",
"priority_low": "scăzut",
"priority_default": "implicit",
"priority_high": "ridicat",
"priority_max": "maxim",
"error_boundary_button_reload_ntfy": "Reîncarcă ntfy"
} }

View File

@@ -0,0 +1,56 @@
{
"common_cancel": "ยกเลิก",
"common_save": "บันทึก",
"common_add": "เพิ่ม",
"common_back": "กลับ",
"common_copy_to_clipboard": "คัดลอกไปยังคลิปบอร์ด",
"signup_title": "สร้างบัญชี ntfy",
"signup_form_username": "ชื่อผู้ใช้",
"signup_form_password": "รหัสผ่าน",
"signup_form_confirm_password": "ยืนยันรหัสผ่าน",
"signup_form_button_submit": "สมัครสมาชิก",
"signup_form_toggle_password_visibility": "สลับการมองเห็นรหัสผ่าน",
"signup_already_have_account": "มีบัญชีอยู่แล้วใช่ไหม? เข้าสู่ระบบ!",
"signup_disabled": "การลงทะเบียนถูกปิดใช้งาน",
"signup_error_username_taken": "ชื่อผู้ใช้ {{username}} ถูกใช้ไปแล้ว",
"signup_error_creation_limit_reached": "ถึงขีดจำกัดการสร้างบัญชีแล้ว",
"login_title": "ลงชื่อเข้าใช้บัญชี ntfy ของคุณ",
"login_form_button_submit": "ลงชื่อเข้าใช้",
"login_link_signup": "สมัครสมาชิก",
"login_disabled": "การเข้าสู่ระบบถูกปิดใช้งาน",
"action_bar_show_menu": "แสดงเมนู",
"action_bar_logo_alt": "โลโก้ ntfy",
"action_bar_settings": "การตั้งค่า",
"action_bar_account": "บัญชี",
"action_bar_change_display_name": "เปลี่ยนชื่อที่แสดง",
"action_bar_reservation_add": "หัวข้อที่สงวนไว้",
"action_bar_reservation_edit": "เปลี่ยนแปลงการจอง",
"action_bar_reservation_delete": "ลบการจอง",
"action_bar_reservation_limit_reached": "ถึงขีดจำกัดแล้ว",
"action_bar_send_test_notification": "ทดสอบการส่งการแจ้งเตือน",
"action_bar_clear_notifications": "ล้างการแจ้งเตือนทั้งหมด",
"action_bar_mute_notifications": "ปิดเสียงการแจ้งเตือนชั่วคราว",
"action_bar_unmute_notifications": "เปิดเสียงการแจ้งเตือน",
"action_bar_unsubscribe": "ยกเลิกการสมัครรับ",
"action_bar_toggle_mute": "ปิดเสียง/เปิดเสียงการแจ้งเตือน",
"action_bar_toggle_action_menu": "เปิด/ปิดเมนูการดำเนินการ",
"action_bar_profile_title": "โปรไฟล์",
"action_bar_profile_settings": "การตั้งค่า",
"action_bar_profile_logout": "ออกจากระบบ",
"action_bar_sign_in": "ลงชื่อเข้าใช้",
"action_bar_sign_up": "สมัครสมาชิก",
"message_bar_type_message": "พิมพ์ข้อความที่นี่",
"message_bar_publish": "เผยแพร่ข้อความ",
"nav_topics_title": "หัวข้อที่สมัครรับข้อมูล",
"nav_button_all_notifications": "การแจ้งเตือนทั้งหมด",
"nav_button_account": "บัญชี",
"nav_button_settings": "การตั้งค่า",
"nav_button_documentation": "เอกสารประกอบ",
"nav_button_publish_message": "เผยแพร่การแจ้งเตือน",
"message_bar_error_publishing": "เกิดข้อผิดพลาดในการเผยแพร่การแจ้งเตือน",
"message_bar_show_dialog": "แสดงกล่องโต้ตอบการเผยแพร่",
"nav_button_subscribe": "สมัครรับหัวข้อ",
"nav_button_muted": "ปิดการแจ้งเตือน",
"nav_button_connecting": "การเชื่อมต่อ",
"nav_upgrade_banner_label": "อัพเกรดเป็น ntfy Pro"
}

View File

@@ -57,7 +57,7 @@
"prefs_notifications_delete_after_never": "Hiçbir zaman", "prefs_notifications_delete_after_never": "Hiçbir zaman",
"notifications_attachment_copy_url_button": "URL'yi kopyala", "notifications_attachment_copy_url_button": "URL'yi kopyala",
"notifications_attachment_open_button": "Eki aç", "notifications_attachment_open_button": "Eki aç",
"nav_button_documentation": "Belgelendirme", "nav_button_documentation": "Dokümantasyon",
"nav_button_publish_message": "Bildirim yayınla", "nav_button_publish_message": "Bildirim yayınla",
"alert_notification_permission_required_title": "Bildirimler devre dışı", "alert_notification_permission_required_title": "Bildirimler devre dışı",
"alert_notification_permission_required_description": "Tarayıcınıza masaüstü bildirimlerini görüntüleme izni verin", "alert_notification_permission_required_description": "Tarayıcınıza masaüstü bildirimlerini görüntüleme izni verin",
@@ -75,7 +75,7 @@
"notifications_click_open_button": "Bağlantıyı aç", "notifications_click_open_button": "Bağlantıyı aç",
"notifications_no_subscriptions_description": "Bir konu oluşturmak veya bir konuya abone olmak için \"{{linktext}}\" bağlantısına tıklayın. Bundan sonra PUT veya POST yoluyla mesaj gönderebilirsiniz ve buradan bildirimler alırsınız.", "notifications_no_subscriptions_description": "Bir konu oluşturmak veya bir konuya abone olmak için \"{{linktext}}\" bağlantısına tıklayın. Bundan sonra PUT veya POST yoluyla mesaj gönderebilirsiniz ve buradan bildirimler alırsınız.",
"notifications_example": "Örnek", "notifications_example": "Örnek",
"notifications_more_details": "Daha fazla bilgi için <websiteLink>web sitesine</websiteLink> veya <docsLink>belgelendirmeye</docsLink> bakın.", "notifications_more_details": "Daha fazla bilgi için <websiteLink>web sitesini</websiteLink> veya <docsLink>dokümantasyonu</docsLink> inceleyin.",
"publish_dialog_chip_attach_url_label": "URL ile dosya ekle", "publish_dialog_chip_attach_url_label": "URL ile dosya ekle",
"prefs_notifications_min_priority_default_and_higher": "Varsayılan öncelik ve üstü", "prefs_notifications_min_priority_default_and_higher": "Varsayılan öncelik ve üstü",
"prefs_notifications_delete_after_three_hours": "Üç saat sonra", "prefs_notifications_delete_after_three_hours": "Üç saat sonra",
@@ -108,7 +108,7 @@
"publish_dialog_button_cancel_sending": "Göndermeyi iptal et", "publish_dialog_button_cancel_sending": "Göndermeyi iptal et",
"prefs_notifications_delete_after_one_week": "Bir hafta sonra", "prefs_notifications_delete_after_one_week": "Bir hafta sonra",
"prefs_notifications_delete_after_one_month": "Bir ay sonra", "prefs_notifications_delete_after_one_month": "Bir ay sonra",
"publish_dialog_details_examples_description": "Örnekler ve tüm gönderme özelliklerinin ayrıntılııklaması için lütfen <docsLink>belgelendirmeye</docsLink> bakın.", "publish_dialog_details_examples_description": "Tüm gönderme özelliklerinin örnekleri ve ayrıntılııklamaları için lütfen <docsLink>dokümantasyona</docsLink> bakın.",
"emoji_picker_search_placeholder": "Emoji ara", "emoji_picker_search_placeholder": "Emoji ara",
"prefs_notifications_delete_after_title": "Bildirimleri sil", "prefs_notifications_delete_after_title": "Bildirimleri sil",
"prefs_notifications_delete_after_one_day": "Bir gün sonra", "prefs_notifications_delete_after_one_day": "Bir gün sonra",

View File

@@ -79,5 +79,23 @@
"notifications_attachment_file_app": "tập tin Android", "notifications_attachment_file_app": "tập tin Android",
"notifications_attachment_link_expires": "liên kết đã hết hạn {{date}}", "notifications_attachment_link_expires": "liên kết đã hết hạn {{date}}",
"alert_not_supported_context_description": "Thông báo chỉ được hỗ trợ qua giao thức HTTPS. Đây là hạn chế của <mdnLink>API thông báo</mdnLink>.", "alert_not_supported_context_description": "Thông báo chỉ được hỗ trợ qua giao thức HTTPS. Đây là hạn chế của <mdnLink>API thông báo</mdnLink>.",
"notifications_attachment_open_button": "Mở đính kèm" "notifications_attachment_open_button": "Mở đính kèm",
"message_bar_error_publishing": "Lỗi khi gửi thông báo",
"message_bar_show_dialog": "Hiện hộp thoại gửi thông báo",
"message_bar_publish": "Gửi thông báo",
"nav_topics_title": "Các topic đã đăng ký",
"nav_button_publish_message": "Gửi thông báo",
"nav_button_subscribe": "Đăng ký topic",
"nav_button_muted": "Đã tắt thông báo",
"nav_upgrade_banner_description": "Đặt trước topic, nhiều thông báo & email hơn, và tệp đính kèm dung lượng lớn hơn",
"action_bar_reservation_add": "Đặt trước topic",
"action_bar_reservation_edit": "Thay đổi thông tin đặt trước",
"action_bar_reservation_delete": "Huỷ đặt trước",
"notifications_none_for_topic_title": "Bạn chưa nhận được thông báo nào cho topic này.",
"notifications_none_for_topic_description": "Để gửi thông báo đến topic này, chỉ cần dùng PUT hoặc POST đến URL của topic.",
"notifications_none_for_any_title": "Bạn chưa nhận được thông báo nào.",
"notifications_none_for_any_description": "Để gửi thông báo đến một topic, bạn chỉ cần dùng PUT hoặc POST đến URL của topic. Dưới đây là một ví dụ với một trong các topic của bạn.",
"notifications_no_subscriptions_title": "Có vẻ như bạn chưa đăng ký topic nào.",
"notifications_no_subscriptions_description": "Bấm vào liên kết \"{{linktext}}\" để tạo hoặc đăng ký một chủ đề. Sau đó, bạn có thể gửi tin nhắn qua PUT hoặc POST và sẽ nhận thông báo tại đây.",
"notifications_example": "Ví dụ"
} }

View File

@@ -203,17 +203,17 @@
"error_boundary_description": "这显然不应该发生。对此非常抱歉。<br/>如果您有时间,请<githubLink>在GitHub</githubLink>上报告,或通过<discordLink>Discord</discordLink>或<matrixLink>Matrix</matrixLink>告诉我们。", "error_boundary_description": "这显然不应该发生。对此非常抱歉。<br/>如果您有时间,请<githubLink>在GitHub</githubLink>上报告,或通过<discordLink>Discord</discordLink>或<matrixLink>Matrix</matrixLink>告诉我们。",
"prefs_users_table": "用户表", "prefs_users_table": "用户表",
"prefs_users_edit_button": "编辑用户", "prefs_users_edit_button": "编辑用户",
"publish_dialog_tags_placeholder": "英文逗号分隔标记列表,例如 warning, srv1-backup", "publish_dialog_tags_placeholder": "英文逗号分隔的标签列表,例如 warning, srv1-backup",
"publish_dialog_details_examples_description": "有关所有发送功能的示例和详细说明,请参阅<docsLink>文档</docsLink>。", "publish_dialog_details_examples_description": "有关所有发送功能的示例和详细说明,请参阅<docsLink>文档</docsLink>。",
"subscribe_dialog_subscribe_description": "主题可能不受密码保护,因此请选择一个不容易被猜中的名字。订阅后,您可以使用 PUT/POST 通知。", "subscribe_dialog_subscribe_description": "主题可能不受密码保护,因此请选择一个不容易被猜中的名字。订阅后,您可以使用 PUT/POST 通知。",
"publish_dialog_delay_placeholder": "延期投递,例如 {{unixTimestamp}}、{{relativeTime}}或「{{naturalLanguage}}」(仅限英语)", "publish_dialog_delay_placeholder": "延期投递,例如 {{unixTimestamp}}、{{relativeTime}}或「{{naturalLanguage}}」(仅限英语)",
"account_usage_basis_ip_description": "此户的使用统计信息和限制基于您的 IP 地址,因此可能会与其他用户共享。上面显示的限制是基于现有速率限制的近似值。", "account_usage_basis_ip_description": "此户的使用统计信息和限制基于您的 IP 地址,因此可能会与其他用户共享。上面显示的限制是基于现有速率限制的近似值。",
"account_usage_cannot_create_portal_session": "无法打开计费门户", "account_usage_cannot_create_portal_session": "无法打开计费门户",
"account_delete_title": "删除户", "account_delete_title": "删除户",
"account_delete_description": "永久删除您的户", "account_delete_description": "永久删除您的户",
"signup_error_username_taken": "用户名 {{username}} 已被占用", "signup_error_username_taken": "用户名 {{username}} 已被占用",
"signup_error_creation_limit_reached": "已达到户创建限制", "signup_error_creation_limit_reached": "已达到户创建限制",
"login_title": "请登录你的 ntfy 户", "login_title": "请登录你的 ntfy 户",
"action_bar_change_display_name": "更改显示名称", "action_bar_change_display_name": "更改显示名称",
"action_bar_reservation_add": "保留主题", "action_bar_reservation_add": "保留主题",
"action_bar_reservation_delete": "移除保留", "action_bar_reservation_delete": "移除保留",
@@ -223,7 +223,7 @@
"action_bar_profile_logout": "登出", "action_bar_profile_logout": "登出",
"action_bar_sign_in": "登录", "action_bar_sign_in": "登录",
"action_bar_sign_up": "注册", "action_bar_sign_up": "注册",
"nav_button_account": "户", "nav_button_account": "户",
"nav_upgrade_banner_label": "升级到 ntfy Pro", "nav_upgrade_banner_label": "升级到 ntfy Pro",
"nav_upgrade_banner_description": "保留主题,更多消息和邮件,以及更大的附件", "nav_upgrade_banner_description": "保留主题,更多消息和邮件,以及更大的附件",
"alert_not_supported_context_description": "通知仅支持 HTTPS。这是 <mdnLink>Notifications API</mdnLink> 的限制。", "alert_not_supported_context_description": "通知仅支持 HTTPS。这是 <mdnLink>Notifications API</mdnLink> 的限制。",
@@ -233,7 +233,7 @@
"reserve_dialog_checkbox_label": "保留主题并配置访问", "reserve_dialog_checkbox_label": "保留主题并配置访问",
"subscribe_dialog_subscribe_button_generate_topic_name": "生成名称", "subscribe_dialog_subscribe_button_generate_topic_name": "生成名称",
"account_basics_username_description": "嘿,那是你 ❤", "account_basics_username_description": "嘿,那是你 ❤",
"account_basics_password_description": "更改您的户密码", "account_basics_password_description": "更改您的户密码",
"account_basics_password_dialog_title": "更改密码", "account_basics_password_dialog_title": "更改密码",
"account_basics_password_dialog_current_password_label": "当前密码", "account_basics_password_dialog_current_password_label": "当前密码",
"account_basics_password_dialog_new_password_label": "新密码", "account_basics_password_dialog_new_password_label": "新密码",
@@ -244,8 +244,8 @@
"account_usage_of_limit": "{{limit}} 的", "account_usage_of_limit": "{{limit}} 的",
"account_usage_unlimited": "无限", "account_usage_unlimited": "无限",
"account_usage_limits_reset_daily": "使用限制每天午夜 (UTC) 重置", "account_usage_limits_reset_daily": "使用限制每天午夜 (UTC) 重置",
"account_basics_tier_title": "户类型", "account_basics_tier_title": "户类型",
"account_basics_tier_description": "您户的权限级别", "account_basics_tier_description": "您户的权限级别",
"account_basics_tier_admin": "管理员", "account_basics_tier_admin": "管理员",
"account_basics_tier_admin_suffix_with_tier": "(有 {{tier}} 等级)", "account_basics_tier_admin_suffix_with_tier": "(有 {{tier}} 等级)",
"account_basics_tier_admin_suffix_no_tier": "(无等级)", "account_basics_tier_admin_suffix_no_tier": "(无等级)",
@@ -258,7 +258,7 @@
"account_usage_messages_title": "已发布消息", "account_usage_messages_title": "已发布消息",
"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_attachment_storage_title": "附件存储", "account_usage_attachment_storage_title": "附件存储",
"account_usage_attachment_storage_description": "每个文件 {{filesize}},在 {{expiry}} 后删除", "account_usage_attachment_storage_description": "每个文件 {{filesize}},在 {{expiry}} 后删除",
"account_upgrade_dialog_button_pay_now": "立即付款并订阅", "account_upgrade_dialog_button_pay_now": "立即付款并订阅",
@@ -276,7 +276,7 @@
"account_tokens_delete_dialog_title": "删除访问令牌", "account_tokens_delete_dialog_title": "删除访问令牌",
"account_tokens_delete_dialog_description": "在删除访问令牌之前,请确保没有应用程序或脚本正在活跃使用它。 <strong>此操作无法撤消</strong>。", "account_tokens_delete_dialog_description": "在删除访问令牌之前,请确保没有应用程序或脚本正在活跃使用它。 <strong>此操作无法撤消</strong>。",
"account_tokens_delete_dialog_submit_button": "永久删除令牌", "account_tokens_delete_dialog_submit_button": "永久删除令牌",
"prefs_users_description_no_sync": "用户和密码不会同步到您的户。", "prefs_users_description_no_sync": "用户和密码不会同步到您的户。",
"prefs_users_table_cannot_delete_or_edit": "无法删除或编辑已登录用户", "prefs_users_table_cannot_delete_or_edit": "无法删除或编辑已登录用户",
"prefs_reservations_title": "保留主题", "prefs_reservations_title": "保留主题",
"prefs_reservations_description": "您可以在此处保留主题名称供个人使用。保留主题使您拥有该主题的所有权,并允许您为其他用户定义对该主题的访问权限。", "prefs_reservations_description": "您可以在此处保留主题名称供个人使用。保留主题使您拥有该主题的所有权,并允许您为其他用户定义对该主题的访问权限。",
@@ -305,13 +305,13 @@
"reservation_delete_dialog_action_delete_title": "删除缓存的邮件和附件", "reservation_delete_dialog_action_delete_title": "删除缓存的邮件和附件",
"reservation_delete_dialog_action_delete_description": "缓存的邮件和附件将被永久删除。此操作无法撤消。", "reservation_delete_dialog_action_delete_description": "缓存的邮件和附件将被永久删除。此操作无法撤消。",
"reservation_delete_dialog_submit_button": "删除保留", "reservation_delete_dialog_submit_button": "删除保留",
"account_delete_dialog_description": "这将永久删除您的户,包括存储在服务器上的所有数据。删除后,您的用户名将在 7 天内不可用。如果您真的想继续,请在下面的框中使用您的密码进行确认。", "account_delete_dialog_description": "这将永久删除您的户,包括存储在服务器上的所有数据。删除后,您的用户名将在 7 天内不可用。如果您真的想继续,请在下面的框中使用您的密码进行确认。",
"account_delete_dialog_label": "密码", "account_delete_dialog_label": "密码",
"account_delete_dialog_button_cancel": "取消", "account_delete_dialog_button_cancel": "取消",
"account_delete_dialog_button_submit": "永久删除户", "account_delete_dialog_button_submit": "永久删除户",
"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>:在付费计划之间升级时,差价将被<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>中删除保留。",
@@ -322,30 +322,30 @@
"signup_form_confirm_password": "确认密码", "signup_form_confirm_password": "确认密码",
"signup_form_button_submit": "注册", "signup_form_button_submit": "注册",
"signup_form_toggle_password_visibility": "切换密码可见性", "signup_form_toggle_password_visibility": "切换密码可见性",
"signup_title": "创建一个 ntfy 户", "signup_title": "创建一个 ntfy 户",
"signup_form_username": "用户名", "signup_form_username": "用户名",
"signup_form_password": "密码", "signup_form_password": "密码",
"signup_already_have_account": "已有户?登录!", "signup_already_have_account": "已有户?登录!",
"signup_disabled": "注册已禁用", "signup_disabled": "注册已禁用",
"login_form_button_submit": "登录", "login_form_button_submit": "登录",
"login_link_signup": "注册", "login_link_signup": "注册",
"login_disabled": "登录已禁用", "login_disabled": "登录已禁用",
"action_bar_account": "户", "action_bar_account": "户",
"action_bar_reservation_edit": "更改保留", "action_bar_reservation_edit": "更改保留",
"subscribe_dialog_error_topic_already_reserved": "主题已保留", "subscribe_dialog_error_topic_already_reserved": "主题已保留",
"account_basics_title": "户", "account_basics_title": "户",
"account_basics_username_title": "用户名", "account_basics_username_title": "用户名",
"account_basics_username_admin_tooltip": "你是管理员", "account_basics_username_admin_tooltip": "你是管理员",
"account_basics_password_title": "密码", "account_basics_password_title": "密码",
"account_basics_tier_payment_overdue": "您的付款已逾期。请更新您的付款方式,否则您的户将很快被降级。", "account_basics_tier_payment_overdue": "您的付款已逾期。请更新您的付款方式,否则您的户将很快被降级。",
"account_basics_tier_canceled_subscription": "您的订阅已取消,并将在 {{date}} 降级为免费户。", "account_basics_tier_canceled_subscription": "您的订阅已取消,并将在 {{date}} 降级为免费户。",
"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_upgrade_dialog_tier_current_label": "当前", "account_upgrade_dialog_tier_current_label": "当前",
"account_upgrade_dialog_button_cancel": "取消", "account_upgrade_dialog_button_cancel": "取消",
"account_upgrade_dialog_button_redirect_signup": "立即注册", "account_upgrade_dialog_button_redirect_signup": "立即注册",
"account_tokens_title": "访问令牌", "account_tokens_title": "访问令牌",
"account_tokens_description": "通过 ntfy API 发布和订阅时使用访问令牌,因此您不必发送您的户凭据。查看<Link>文档</Link>以了解更多信息。", "account_tokens_description": "通过 ntfy API 发布和订阅时使用访问令牌,因此您不必发送您的户凭据。查看<Link>文档</Link>以了解更多信息。",
"account_tokens_table_token_header": "令牌", "account_tokens_table_token_header": "令牌",
"account_tokens_table_label_header": "标签", "account_tokens_table_label_header": "标签",
"account_tokens_table_last_access_header": "最后访问", "account_tokens_table_last_access_header": "最后访问",
@@ -403,5 +403,7 @@
"web_push_subscription_expiring_title": "通知将被暂停", "web_push_subscription_expiring_title": "通知将被暂停",
"web_push_subscription_expiring_body": "打开ntfy以继续接收通知", "web_push_subscription_expiring_body": "打开ntfy以继续接收通知",
"web_push_unknown_notification_title": "接收到未知通知", "web_push_unknown_notification_title": "接收到未知通知",
"web_push_unknown_notification_body": "你可能需要打开网页来更新ntfy" "web_push_unknown_notification_body": "你可能需要打开网页来更新ntfy",
"account_basics_cannot_edit_or_delete_provisioned_user": "已设置的用户无法被编辑或删除",
"account_tokens_table_cannot_delete_or_edit_provisioned_token": "无法编辑或删除已设置的令牌"
} }

View File

@@ -91,7 +91,7 @@
"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_current_label": "當前", "account_upgrade_dialog_tier_current_label": "當前",
"account_upgrade_dialog_tier_features_attachment_file_size": "每個文件 {{filesize}} ", "account_upgrade_dialog_tier_features_attachment_file_size": "每個文件 {{filesize}}",
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} 總存儲空間", "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} 總存儲空間",
"account_upgrade_dialog_tier_features_calls_one": "每日一通電話", "account_upgrade_dialog_tier_features_calls_one": "每日一通電話",
"account_upgrade_dialog_tier_features_calls_other": "每日{{calls}} 通電話", "account_upgrade_dialog_tier_features_calls_other": "每日{{calls}} 通電話",
@@ -145,13 +145,13 @@
"action_bar_unsubscribe": "取消訂閱", "action_bar_unsubscribe": "取消訂閱",
"alert_notification_ios_install_required_description": "要接收通知,請在 iOS 上點擊共享,然後添加到主屏幕", "alert_notification_ios_install_required_description": "要接收通知,請在 iOS 上點擊共享,然後添加到主屏幕",
"alert_notification_ios_install_required_title": "需要安裝 iOS 應用程式", "alert_notification_ios_install_required_title": "需要安裝 iOS 應用程式",
"alert_notification_permission_denied_description": "你已禁用通知。要重新啟用通知,請在瀏覽器設置中啟用通知", "alert_notification_permission_denied_description": "你已禁用通知。要重新啟用通知,請在瀏覽器設置中啟用通知",
"alert_notification_permission_denied_title": "已禁用通知", "alert_notification_permission_denied_title": "已禁用通知",
"alert_notification_permission_required_button": "現在授予", "alert_notification_permission_required_button": "現在授予",
"alert_notification_permission_required_description": "授予瀏覽器顯示桌面通知的權限", "alert_notification_permission_required_description": "授予瀏覽器顯示桌面通知的權限",
"alert_notification_permission_required_title": "已禁用通知", "alert_notification_permission_required_title": "已禁用通知",
"alert_not_supported_context_description": "通知僅支援 HTTPS。這是 <mdnLink>Notifications API</mdnLink> 的限制。", "alert_not_supported_context_description": "通知僅支援 HTTPS。這是 <mdnLink>Notifications API</mdnLink> 的限制。",
"alert_not_supported_description": "你的瀏覽器不支援通知", "alert_not_supported_description": "你的瀏覽器不支援通知",
"alert_not_supported_title": "不支援通知", "alert_not_supported_title": "不支援通知",
"common_add": "新增", "common_add": "新增",
"common_back": "返回", "common_back": "返回",
@@ -223,7 +223,7 @@
"notifications_none_for_topic_description": "要向此主題發送通知,只需使用 PUT 或 POST 到主題連結即可。", "notifications_none_for_topic_description": "要向此主題發送通知,只需使用 PUT 或 POST 到主題連結即可。",
"notifications_none_for_topic_title": "你尚未收到有關此主題的任何通知。", "notifications_none_for_topic_title": "你尚未收到有關此主題的任何通知。",
"notifications_no_subscriptions_description": "點擊 \"{{linktext}}\" 連結以建立或訂閱主題。之後,你可以使用 PUT 或 POST 發送訊息,你將在這裡收到通知。", "notifications_no_subscriptions_description": "點擊 \"{{linktext}}\" 連結以建立或訂閱主題。之後,你可以使用 PUT 或 POST 發送訊息,你將在這裡收到通知。",
"notifications_no_subscriptions_title": "看起來你還未有任何訂閱", "notifications_no_subscriptions_title": "看起來你還未有任何訂閱",
"notifications_priority_x": "優先級 {{priority}}", "notifications_priority_x": "優先級 {{priority}}",
"notifications_tags": "標記", "notifications_tags": "標記",
"prefs_appearance_language_title": "語言", "prefs_appearance_language_title": "語言",
@@ -261,7 +261,7 @@
"prefs_notifications_web_push_disabled_description": "當網頁程式在運行時將會收到通知 (透過 WebSocket)", "prefs_notifications_web_push_disabled_description": "當網頁程式在運行時將會收到通知 (透過 WebSocket)",
"prefs_notifications_web_push_disabled": "己暫用", "prefs_notifications_web_push_disabled": "己暫用",
"prefs_notifications_web_push_enabled_description": "即使網頁程式未有運街亦會收到通知 (via Web Push)", "prefs_notifications_web_push_enabled_description": "即使網頁程式未有運街亦會收到通知 (via Web Push)",
"prefs_notifications_web_push_enabled": "己為 {{server}} 啟用", "prefs_notifications_web_push_enabled": "己為 {{server}} 啟用",
"prefs_notifications_web_push_title": "背景通知", "prefs_notifications_web_push_title": "背景通知",
"prefs_reservations_add_button": "新增保留主題", "prefs_reservations_add_button": "新增保留主題",
"prefs_reservations_delete_button": "重置主題訪問", "prefs_reservations_delete_button": "重置主題訪問",
@@ -403,5 +403,7 @@
"web_push_subscription_expiring_body": "開啟ntfy以繼續接收通知", "web_push_subscription_expiring_body": "開啟ntfy以繼續接收通知",
"web_push_subscription_expiring_title": "通知會被暫停", "web_push_subscription_expiring_title": "通知會被暫停",
"web_push_unknown_notification_body": "你可能需要開啟網頁來更新ntfy", "web_push_unknown_notification_body": "你可能需要開啟網頁來更新ntfy",
"web_push_unknown_notification_title": "接收到不明通知" "web_push_unknown_notification_title": "接收到不明通知",
"account_basics_cannot_edit_or_delete_provisioned_user": "已佈建的使用者無法編輯或刪除",
"account_tokens_table_cannot_delete_or_edit_provisioned_token": "無法編輯或刪除已佈建的權杖"
} }

View File

@@ -77,7 +77,10 @@ export const maybeWithBearerAuth = (headers, token) => {
return headers; return headers;
}; };
export const withBasicAuth = (headers, username, password) => ({ ...headers, Authorization: basicAuth(username, password) }); export const withBasicAuth = (headers, username, password) => ({
...headers,
Authorization: basicAuth(username, password),
});
export const maybeWithAuth = (headers, user) => { export const maybeWithAuth = (headers, user) => {
if (user?.password) { if (user?.password) {
@@ -270,3 +273,21 @@ export const urlB64ToUint8Array = (base64String) => {
} }
return outputArray; return outputArray;
}; };
export const copyToClipboard = (text) => {
if (navigator.clipboard && window.isSecureContext) {
return navigator.clipboard.writeText(text);
}
// Fallback to the older method if clipboard API is not supported (or on HTTP)
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.setAttribute("readonly", ""); // Avoid mobile keyboards from popping up
textarea.style.position = "fixed"; // Avoid scroll jump
textarea.style.left = "-9999px";
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
document.execCommand("copy");
document.body.removeChild(textarea);
return Promise.resolve();
};

View File

@@ -45,7 +45,7 @@ import CloseIcon from "@mui/icons-material/Close";
import { ContentCopy, Public } from "@mui/icons-material"; import { ContentCopy, Public } from "@mui/icons-material";
import AddIcon from "@mui/icons-material/Add"; import AddIcon from "@mui/icons-material/Add";
import routes from "./routes"; import routes from "./routes";
import { formatBytes, formatShortDate, formatShortDateTime, openUrl } from "../app/utils"; import { copyToClipboard, formatBytes, formatShortDate, formatShortDateTime, openUrl } from "../app/utils";
import accountApi, { LimitBasis, Role, SubscriptionInterval, SubscriptionStatus } from "../app/AccountApi"; import accountApi, { LimitBasis, Role, SubscriptionInterval, SubscriptionStatus } from "../app/AccountApi";
import { Pref, PrefGroup } from "./Pref"; import { Pref, PrefGroup } from "./Pref";
import db from "../app/db"; import db from "../app/db";
@@ -100,15 +100,13 @@ const Username = () => {
<Pref labelId={labelId} title={t("account_basics_username_title")} description={t("account_basics_username_description")}> <Pref labelId={labelId} title={t("account_basics_username_title")} description={t("account_basics_username_description")}>
<div aria-labelledby={labelId}> <div aria-labelledby={labelId}>
{session.username()} {session.username()}
{account?.role === Role.ADMIN ? ( {account?.role === Role.ADMIN && (
<> <>
{" "} {" "}
<Tooltip title={t("account_basics_username_admin_tooltip")}> <Tooltip title={t("account_basics_username_admin_tooltip")}>
<span style={{ cursor: "default" }}>👑</span> <span style={{ cursor: "default" }}>👑</span>
</Tooltip> </Tooltip>
</> </>
) : (
""
)} )}
</div> </div>
</Pref> </Pref>
@@ -119,6 +117,7 @@ const ChangePassword = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const [dialogKey, setDialogKey] = useState(0); const [dialogKey, setDialogKey] = useState(0);
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const { account } = useContext(AccountContext);
const labelId = "prefChangePassword"; const labelId = "prefChangePassword";
const handleDialogOpen = () => { const handleDialogOpen = () => {
@@ -136,9 +135,19 @@ const ChangePassword = () => {
<Typography color="gray" sx={{ float: "left", fontSize: "0.7rem", lineHeight: "3.5" }}> <Typography color="gray" sx={{ float: "left", fontSize: "0.7rem", lineHeight: "3.5" }}>
</Typography> </Typography>
<IconButton onClick={handleDialogOpen} aria-label={t("account_basics_password_description")}> {!account?.provisioned ? (
<EditIcon /> <IconButton onClick={handleDialogOpen} aria-label={t("account_basics_password_description")}>
</IconButton> <EditIcon />
</IconButton>
) : (
<Tooltip title={t("account_basics_cannot_edit_or_delete_provisioned_user")}>
<span>
<IconButton disabled>
<EditIcon />
</IconButton>
</span>
</Tooltip>
)}
</div> </div>
<ChangePasswordDialog key={`changePasswordDialog${dialogKey}`} open={dialogOpen} onClose={handleDialogClose} /> <ChangePasswordDialog key={`changePasswordDialog${dialogKey}`} open={dialogOpen} onClose={handleDialogClose} />
</Pref> </Pref>
@@ -361,7 +370,7 @@ const PhoneNumbers = () => {
}; };
const handleCopy = (phoneNumber) => { const handleCopy = (phoneNumber) => {
navigator.clipboard.writeText(phoneNumber); copyToClipboard(phoneNumber);
setSnackOpen(true); setSnackOpen(true);
}; };
@@ -832,7 +841,7 @@ const TokensTable = (props) => {
}; };
const handleCopy = async (token) => { const handleCopy = async (token) => {
await navigator.clipboard.writeText(token); copyToClipboard(token);
setSnackOpen(true); setSnackOpen(true);
}; };
@@ -888,7 +897,7 @@ const TokensTable = (props) => {
</div> </div>
</TableCell> </TableCell>
<TableCell align="right" sx={{ whiteSpace: "nowrap" }}> <TableCell align="right" sx={{ whiteSpace: "nowrap" }}>
{token.token !== session.token() && ( {token.token !== session.token() && !token.provisioned && (
<> <>
<IconButton onClick={() => handleEditClick(token)} aria-label={t("account_tokens_dialog_title_edit")}> <IconButton onClick={() => handleEditClick(token)} aria-label={t("account_tokens_dialog_title_edit")}>
<EditIcon /> <EditIcon />
@@ -910,6 +919,18 @@ const TokensTable = (props) => {
</span> </span>
</Tooltip> </Tooltip>
)} )}
{token.provisioned && (
<Tooltip title={t("account_tokens_table_cannot_delete_or_edit_provisioned_token")}>
<span>
<IconButton disabled>
<EditIcon />
</IconButton>
<IconButton disabled>
<CloseIcon />
</IconButton>
</span>
</Tooltip>
)}
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
@@ -1048,6 +1069,7 @@ const DeleteAccount = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const [dialogKey, setDialogKey] = useState(0); const [dialogKey, setDialogKey] = useState(0);
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const { account } = useContext(AccountContext);
const handleDialogOpen = () => { const handleDialogOpen = () => {
setDialogKey((prev) => prev + 1); setDialogKey((prev) => prev + 1);
@@ -1061,9 +1083,19 @@ const DeleteAccount = () => {
return ( return (
<Pref title={t("account_delete_title")} description={t("account_delete_description")}> <Pref title={t("account_delete_title")} description={t("account_delete_description")}>
<div> <div>
<Button fullWidth={false} variant="outlined" color="error" startIcon={<DeleteOutlineIcon />} onClick={handleDialogOpen}> {!account?.provisioned ? (
{t("account_delete_title")} <Button fullWidth={false} variant="outlined" color="error" startIcon={<DeleteOutlineIcon />} onClick={handleDialogOpen}>
</Button> {t("account_delete_title")}
</Button>
) : (
<Tooltip title={t("account_basics_cannot_edit_or_delete_provisioned_user")}>
<span>
<Button fullWidth={false} variant="outlined" color="error" startIcon={<DeleteOutlineIcon />} disabled>
{t("account_delete_title")}
</Button>
</span>
</Tooltip>
)}
</div> </div>
<DeleteAccountDialog key={`deleteAccountDialog${dialogKey}`} open={dialogOpen} onClose={handleDialogClose} /> <DeleteAccountDialog key={`deleteAccountDialog${dialogKey}`} open={dialogOpen} onClose={handleDialogClose} />
</Pref> </Pref>

View File

@@ -23,6 +23,7 @@ import Account from "./Account";
import initI18n from "../app/i18n"; // Translations! import initI18n from "../app/i18n"; // Translations!
import prefs, { THEME } from "../app/Prefs"; import prefs, { THEME } from "../app/Prefs";
import RTLCacheProvider from "./RTLCacheProvider"; import RTLCacheProvider from "./RTLCacheProvider";
import session from "../app/Session";
initI18n(); initI18n();
@@ -45,7 +46,6 @@ const darkModeEnabled = (prefersDarkMode, themePreference) => {
const App = () => { const App = () => {
const { i18n } = useTranslation(); const { i18n } = useTranslation();
const languageDir = i18n.dir(); const languageDir = i18n.dir();
const [account, setAccount] = useState(null); const [account, setAccount] = useState(null);
const accountMemo = useMemo(() => ({ account, setAccount }), [account, setAccount]); const accountMemo = useMemo(() => ({ account, setAccount }), [account, setAccount]);
const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)"); const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)");
@@ -60,6 +60,12 @@ const App = () => {
document.dir = languageDir; document.dir = languageDir;
}, [i18n.language, languageDir]); }, [i18n.language, languageDir]);
useEffect(() => {
if (!session.exists() && config.require_login && window.location.pathname !== routes.login) {
window.location.href = routes.login;
}
}, []);
return ( return (
<Suspense fallback={<Loader />}> <Suspense fallback={<Loader />}>
<RTLCacheProvider> <RTLCacheProvider>

View File

@@ -2,6 +2,7 @@ import * as React from "react";
import StackTrace from "stacktrace-js"; import StackTrace from "stacktrace-js";
import { CircularProgress, Link, Button } from "@mui/material"; import { CircularProgress, Link, Button } from "@mui/material";
import { Trans, withTranslation } from "react-i18next"; import { Trans, withTranslation } from "react-i18next";
import { copyToClipboard } from "../app/utils";
class ErrorBoundaryImpl extends React.Component { class ErrorBoundaryImpl extends React.Component {
constructor(props) { constructor(props) {
@@ -64,7 +65,7 @@ class ErrorBoundaryImpl extends React.Component {
stack += `${this.state.niceStack}\n\n`; stack += `${this.state.niceStack}\n\n`;
} }
stack += `${this.state.originalStack}\n`; stack += `${this.state.originalStack}\n`;
navigator.clipboard.writeText(stack); copyToClipboard(stack);
} }
renderUnsupportedIndexedDB() { renderUnsupportedIndexedDB() {

View File

@@ -26,7 +26,16 @@ import { Trans, useTranslation } from "react-i18next";
import { useOutletContext } from "react-router-dom"; import { useOutletContext } from "react-router-dom";
import { useRemark } from "react-remark"; import { useRemark } from "react-remark";
import styled from "@emotion/styled"; import styled from "@emotion/styled";
import { formatBytes, formatShortDateTime, maybeActionErrors, openUrl, shortUrl, topicShortUrl, unmatchedTags } from "../app/utils"; import {
copyToClipboard,
formatBytes,
formatShortDateTime,
maybeActionErrors,
openUrl,
shortUrl,
topicShortUrl,
unmatchedTags,
} from "../app/utils";
import { formatMessage, formatTitle, isImage } from "../app/notificationUtils"; import { formatMessage, formatTitle, isImage } from "../app/notificationUtils";
import { LightboxBackdrop, Paragraph, VerticallyCenteredContainer } from "./styles"; import { LightboxBackdrop, Paragraph, VerticallyCenteredContainer } from "./styles";
import subscriptionManager from "../app/SubscriptionManager"; import subscriptionManager from "../app/SubscriptionManager";
@@ -249,7 +258,7 @@ const NotificationItem = (props) => {
}); });
}; };
const handleCopy = (s) => { const handleCopy = (s) => {
navigator.clipboard.writeText(s); copyToClipboard(s);
props.onShowSnack(); props.onShowSnack();
}; };
const expired = attachment && attachment.expires && attachment.expires < Date.now() / 1000; const expired = attachment && attachment.expires && attachment.expires < Date.now() / 1000;

View File

@@ -17,6 +17,13 @@ const baseThemeOptions = {
}, },
}, },
}, },
MuiCardActions: {
styleOverrides: {
root: {
overflowX: "auto",
},
},
},
}, },
}; };