mirror of
https://github.com/binwiederhier/ntfy.git
synced 2026-01-19 08:37:28 +01:00
Compare commits
81 Commits
auth-user-
...
cf-priorit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d556a675e9 | ||
|
|
bfc1fa5181 | ||
|
|
d9387dac99 | ||
|
|
4818ee57b6 | ||
|
|
addb5efebb | ||
|
|
8adb9ee633 | ||
|
|
418fc98d1a | ||
|
|
beffe4a1f2 | ||
|
|
e61c9fdde9 | ||
|
|
d2e2791729 | ||
|
|
528a67722b | ||
|
|
d29dc95962 | ||
|
|
fc3d4dcf5e | ||
|
|
3d4218324f | ||
|
|
f6fbb45978 | ||
|
|
dee16f543d | ||
|
|
9959d1aa43 | ||
|
|
76146c4e74 | ||
|
|
8a8023fcf8 | ||
|
|
4b0d1e448d | ||
|
|
6748a2f2f3 | ||
|
|
4c4d772a5f | ||
|
|
85740d810b | ||
|
|
2305ebca24 | ||
|
|
59bf388534 | ||
|
|
3066b95a6d | ||
|
|
1bd77a83bd | ||
|
|
d0b7336da7 | ||
|
|
c80f71bd9b | ||
|
|
15fa3b7d9f | ||
|
|
e2d7f2cf29 | ||
|
|
3031fb910f | ||
|
|
d999dbe0a0 | ||
|
|
60d5e66e34 | ||
|
|
c6964502c4 | ||
|
|
ca2633ff82 | ||
|
|
a1625c7f15 | ||
|
|
30a913c05c | ||
|
|
1d02933481 | ||
|
|
62c2ec0614 | ||
|
|
45ca20dec9 | ||
|
|
de362d2322 | ||
|
|
115e6e9cf8 | ||
|
|
f17538b7df | ||
|
|
6f68c8cd1f | ||
|
|
02dd72ba57 | ||
|
|
63629efae7 | ||
|
|
9015b27803 | ||
|
|
a5f0670f7f | ||
|
|
d7db395016 | ||
|
|
99eef493d2 | ||
|
|
0d395249ff | ||
|
|
5cf1da974a | ||
|
|
2f0ec88f40 | ||
|
|
d9d3c4a724 | ||
|
|
bc4d4f424a | ||
|
|
67459650d4 | ||
|
|
c31bce1e2d | ||
|
|
3e3b556108 | ||
|
|
723daf9497 | ||
|
|
f77958fc35 | ||
|
|
ea9f2c6e35 | ||
|
|
7d20238423 | ||
|
|
31131db756 | ||
|
|
17e634c563 | ||
|
|
a7dc3d84e0 | ||
|
|
b80aec90d0 | ||
|
|
8544733048 | ||
|
|
140fdcca81 | ||
|
|
2e08c48742 | ||
|
|
4800bb05d2 | ||
|
|
384cabede5 | ||
|
|
4e9eeb1fa1 | ||
|
|
a534cc9eca | ||
|
|
e52132c85b | ||
|
|
76667ffcf9 | ||
|
|
8ba4b72b37 | ||
|
|
81e1417ce5 | ||
|
|
c1576b5b19 | ||
|
|
86cc3b9607 | ||
|
|
c7f85e6283 |
@@ -164,14 +164,14 @@ dockers:
|
||||
- image_templates:
|
||||
- &arm64v8_image "binwiederhier/ntfy:{{ .Tag }}-arm64v8"
|
||||
use: buildx
|
||||
dockerfile: Dockerfile
|
||||
dockerfile: Dockerfile-arm
|
||||
goarch: arm64
|
||||
build_flag_templates:
|
||||
- "--platform=linux/arm64/v8"
|
||||
- image_templates:
|
||||
- &armv7_image "binwiederhier/ntfy:{{ .Tag }}-armv7"
|
||||
use: buildx
|
||||
dockerfile: Dockerfile
|
||||
dockerfile: Dockerfile-arm
|
||||
goarch: arm
|
||||
goarm: 7
|
||||
build_flag_templates:
|
||||
@@ -179,7 +179,7 @@ dockers:
|
||||
- image_templates:
|
||||
- &armv6_image "binwiederhier/ntfy:{{ .Tag }}-armv6"
|
||||
use: buildx
|
||||
dockerfile: Dockerfile
|
||||
dockerfile: Dockerfile-arm
|
||||
goarch: arm
|
||||
goarm: 6
|
||||
build_flag_templates:
|
||||
|
||||
@@ -9,6 +9,7 @@ LABEL org.opencontainers.image.licenses="Apache-2.0, GPL-2.0"
|
||||
LABEL org.opencontainers.image.title="ntfy"
|
||||
LABEL org.opencontainers.image.description="Send push notifications to your phone or desktop using PUT/POST"
|
||||
|
||||
RUN apk add --no-cache tzdata
|
||||
COPY ntfy /usr/bin
|
||||
|
||||
EXPOSE 80/tcp
|
||||
|
||||
18
Dockerfile-arm
Normal file
18
Dockerfile-arm
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM alpine
|
||||
|
||||
LABEL org.opencontainers.image.authors="philipp.heckel@gmail.com"
|
||||
LABEL org.opencontainers.image.url="https://ntfy.sh/"
|
||||
LABEL org.opencontainers.image.documentation="https://docs.ntfy.sh/"
|
||||
LABEL org.opencontainers.image.source="https://github.com/binwiederhier/ntfy"
|
||||
LABEL org.opencontainers.image.vendor="Philipp C. Heckel"
|
||||
LABEL org.opencontainers.image.licenses="Apache-2.0, GPL-2.0"
|
||||
LABEL org.opencontainers.image.title="ntfy"
|
||||
LABEL org.opencontainers.image.description="Send push notifications to your phone or desktop using PUT/POST"
|
||||
|
||||
# Alpine does not support adding "tzdata" on ARM anymore, see
|
||||
# https://github.com/binwiederhier/ntfy/issues/894
|
||||
|
||||
COPY ntfy /usr/bin
|
||||
|
||||
EXPOSE 80/tcp
|
||||
ENTRYPOINT ["ntfy"]
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.19-bullseye as builder
|
||||
FROM golang:1.20-bullseye as builder
|
||||
|
||||
ARG VERSION=dev
|
||||
ARG COMMIT=unknown
|
||||
|
||||
21
README.md
21
README.md
@@ -18,7 +18,7 @@ notification service. With ntfy, you can **send notifications to your phone or d
|
||||
**without having to sign up or pay any fees**. If you'd like to run your own instance of the service, you can easily do
|
||||
so since ntfy is open source.
|
||||
|
||||
You can access the free version of ntfy at **[ntfy.sh](https://ntfy.sh)**. There is also an [open source Android app](https://github.com/binwiederhier/ntfy-android)
|
||||
You can access the free version of ntfy at **[ntfy.sh](https://ntfy.sh)**. There is also an [open-source Android app](https://github.com/binwiederhier/ntfy-android)
|
||||
available on [Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy) or [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/),
|
||||
as well as an [open source iOS app](https://github.com/binwiederhier/ntfy-ios) available on the [App Store](https://apps.apple.com/us/app/ntfy/id1625396347).
|
||||
|
||||
@@ -41,7 +41,7 @@ I now offer paid plans for [ntfy.sh](https://ntfy.sh/) if you don't want to self
|
||||
[Install / Self-hosting](https://ntfy.sh/docs/install/) |
|
||||
[Building](https://ntfy.sh/docs/develop/)
|
||||
|
||||
## Chat / forum
|
||||
## Chat/forum
|
||||
There are a few ways to get in touch with me and/or the rest of the community. Feel free to use any of these methods. Whatever
|
||||
works best for you:
|
||||
|
||||
@@ -50,13 +50,13 @@ works best for you:
|
||||
* [Lemmy discussion board](https://discuss.ntfy.sh/c/ntfy) - asynchronous forum (_new as of June 2023_)
|
||||
* [GitHub issues](https://github.com/binwiederhier/ntfy/issues) - questions, features, bugs
|
||||
|
||||
## Announcements / beta testers
|
||||
## Announcements/beta testers
|
||||
For announcements of new releases and cutting-edge beta versions, please subscribe to the [ntfy.sh/announcements](https://ntfy.sh/announcements)
|
||||
topic. If you'd like to test the iOS app, join [TestFlight](https://testflight.apple.com/join/P1fFnAm9). For Android betas,
|
||||
join Discord/Matrix (I'll eventually make a testing channel in Google Play).
|
||||
|
||||
## Contributing
|
||||
I welcome any and all contributions. Just create a PR or an issue. For larger features/ideas, please reach out
|
||||
I welcome any contributions. Just create a PR or an issue. For larger features/ideas, please reach out
|
||||
on Discord/Matrix first to see if I'd accept them. To contribute code, check out the [build instructions](https://ntfy.sh/docs/develop/)
|
||||
for the server and the Android app. Or, if you'd like to help translate 🇩🇪 🇺🇸 🇧🇬, you can start immediately in
|
||||
[Hosted Weblate](https://hosted.weblate.org/projects/ntfy/).
|
||||
@@ -141,8 +141,17 @@ account costs. Even small donations are very much appreciated. A big fat **Thank
|
||||
<a href="https://github.com/darkdragon-001"><img src="https://github.com/darkdragon-001.png" width="40px" /></a>
|
||||
<a href="https://github.com/jonathan-kosgei"><img src="https://github.com/jonathan-kosgei.png" width="40px" /></a>
|
||||
<a href="https://github.com/KevinWang15"><img src="https://github.com/KevinWang15.png" width="40px" /></a>
|
||||
<a href="https://github.com/darkmattercoder"><img src="https://github.com/darkmattercoder.png" width="40px" /></a>
|
||||
<a href="https://github.com/bmcgonag"><img src="https://github.com/bmcgonag.png" width="40px" /></a>
|
||||
<a href="https://github.com/skorokithakis"><img src="https://github.com/skorokithakis.png" width="40px" /></a>
|
||||
<a href="https://github.com/eenturk"><img src="https://github.com/eenturk.png" width="40px" /></a>
|
||||
<a href="https://github.com/spirossi"><img src="https://github.com/spirossi.png" width="40px" /></a>
|
||||
<a href="https://github.com/teomarcdhio"><img src="https://github.com/teomarcdhio.png" width="40px" /></a>
|
||||
<a href="https://github.com/MarcMichalsky"><img src="https://github.com/MarcMichalsky.png" width="40px" /></a>
|
||||
<a href="https://github.com/LuckVintage"><img src="https://github.com/LuckVintage.png" width="40px" /></a>
|
||||
<a href="https://github.com/spartan"><img src="https://github.com/spartan.png" width="40px" /></a>
|
||||
|
||||
I'd also like to thank JetBrains for providing their awesome [IntelliJ IDEA](https://www.jetbrains.com/idea/) to me for free,
|
||||
I'd also like to thank JetBrains for their awesome [IntelliJ IDEA](https://www.jetbrains.com/idea/),
|
||||
and [DigitalOcean](https://m.do.co/c/442b929528db) (*referral link*) for supporting the project:
|
||||
|
||||
<a href="https://m.do.co/c/442b929528db"><img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="201px"></a>
|
||||
@@ -158,7 +167,7 @@ _Please be sure to read the complete [Code of Conduct](CODE_OF_CONDUCT.md)._
|
||||
Made with ❤️ by [Philipp C. Heckel](https://heckel.io).
|
||||
The project is dual licensed under the [Apache License 2.0](LICENSE) and the [GPLv2 License](LICENSE.GPLv2).
|
||||
|
||||
Third party libraries and resources:
|
||||
Third-party libraries and resources:
|
||||
* [github.com/urfave/cli](https://github.com/urfave/cli) (MIT) is used to drive the CLI
|
||||
* [Mixkit sounds](https://mixkit.co/free-sound-effects/notification/) (Mixkit Free License) are used as notification sounds
|
||||
* [Sounds from notificationsounds.com](https://notificationsounds.com) (Creative Commons Attribution) are used as notification sounds
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
|
||||
# Default credentials will be used with "ntfy publish" and "ntfy subscribe" if no other credentials are provided.
|
||||
# You can set a default token to use or a default user:password combination, but not both. For an empty password,
|
||||
# use empty double-quotes ("")
|
||||
# use empty double-quotes ("").
|
||||
#
|
||||
# To override the default user:password combination or default token for a particular subscription (e.g., to send
|
||||
# no Authorization header), set the user:pass/token for the subscription to empty double-quotes ("").
|
||||
|
||||
# default-token:
|
||||
|
||||
|
||||
@@ -23,9 +23,9 @@ type Config struct {
|
||||
// Subscribe is the struct for a Subscription within Config
|
||||
type Subscribe struct {
|
||||
Topic string `yaml:"topic"`
|
||||
User string `yaml:"user"`
|
||||
User *string `yaml:"user"`
|
||||
Password *string `yaml:"password"`
|
||||
Token string `yaml:"token"`
|
||||
Token *string `yaml:"token"`
|
||||
Command string `yaml:"command"`
|
||||
If map[string]string `yaml:"if"`
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ subscribe:
|
||||
require.Equal(t, 4, len(conf.Subscribe))
|
||||
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
|
||||
require.Equal(t, "", conf.Subscribe[0].Command)
|
||||
require.Equal(t, "phil", conf.Subscribe[0].User)
|
||||
require.Equal(t, "phil", *conf.Subscribe[0].User)
|
||||
require.Equal(t, "mypass", *conf.Subscribe[0].Password)
|
||||
require.Equal(t, "echo-this", conf.Subscribe[1].Topic)
|
||||
require.Equal(t, `echo "Message received: $message"`, conf.Subscribe[1].Command)
|
||||
@@ -67,7 +67,7 @@ subscribe:
|
||||
require.Equal(t, 1, len(conf.Subscribe))
|
||||
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
|
||||
require.Equal(t, "", conf.Subscribe[0].Command)
|
||||
require.Equal(t, "phil", conf.Subscribe[0].User)
|
||||
require.Equal(t, "phil", *conf.Subscribe[0].User)
|
||||
require.Equal(t, "", *conf.Subscribe[0].Password)
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ subscribe:
|
||||
require.Equal(t, 1, len(conf.Subscribe))
|
||||
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
|
||||
require.Equal(t, "", conf.Subscribe[0].Command)
|
||||
require.Equal(t, "phil", conf.Subscribe[0].User)
|
||||
require.Equal(t, "phil", *conf.Subscribe[0].User)
|
||||
require.Nil(t, conf.Subscribe[0].Password)
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ subscribe:
|
||||
require.Equal(t, 1, len(conf.Subscribe))
|
||||
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
|
||||
require.Equal(t, "", conf.Subscribe[0].Command)
|
||||
require.Equal(t, "phil", conf.Subscribe[0].User)
|
||||
require.Equal(t, "phil", *conf.Subscribe[0].User)
|
||||
require.Nil(t, conf.Subscribe[0].Password)
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ subscribe:
|
||||
require.Equal(t, "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", conf.DefaultToken)
|
||||
require.Equal(t, 1, len(conf.Subscribe))
|
||||
require.Equal(t, "mytopic", conf.Subscribe[0].Topic)
|
||||
require.Equal(t, "", conf.Subscribe[0].User)
|
||||
require.Nil(t, conf.Subscribe[0].User)
|
||||
require.Nil(t, conf.Subscribe[0].Password)
|
||||
require.Equal(t, "", conf.Subscribe[0].Token)
|
||||
require.Nil(t, conf.Subscribe[0].Token)
|
||||
}
|
||||
|
||||
@@ -97,6 +97,11 @@ func WithBearerAuth(token string) PublishOption {
|
||||
return WithHeader("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
}
|
||||
|
||||
// WithEmptyAuth clears the Authorization header
|
||||
func WithEmptyAuth() PublishOption {
|
||||
return RemoveHeader("Authorization")
|
||||
}
|
||||
|
||||
// WithNoCache instructs the server not to cache the message server-side
|
||||
func WithNoCache() PublishOption {
|
||||
return WithHeader("X-Cache", "no")
|
||||
@@ -187,3 +192,13 @@ func WithQueryParam(param, value string) RequestOption {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveHeader is a generic option to remove a header from a request
|
||||
func RemoveHeader(header string) RequestOption {
|
||||
return func(r *http.Request) error {
|
||||
if header != "" {
|
||||
delete(r.Header, header)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,12 +225,17 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic,
|
||||
}
|
||||
|
||||
func maybeAddAuthHeader(s client.Subscribe, conf *client.Config) client.SubscribeOption {
|
||||
// check for subscription token then subscription user:pass
|
||||
if s.Token != "" {
|
||||
return client.WithBearerAuth(s.Token)
|
||||
// if an explicit empty token or empty user:pass is given, exit without auth
|
||||
if (s.Token != nil && *s.Token == "") || (s.User != nil && *s.User == "" && s.Password != nil && *s.Password == "") {
|
||||
return client.WithEmptyAuth()
|
||||
}
|
||||
if s.User != "" && s.Password != nil {
|
||||
return client.WithBasicAuth(s.User, *s.Password)
|
||||
|
||||
// check for subscription token then subscription user:pass
|
||||
if s.Token != nil && *s.Token != "" {
|
||||
return client.WithBearerAuth(*s.Token)
|
||||
}
|
||||
if s.User != nil && *s.User != "" && s.Password != nil {
|
||||
return client.WithBasicAuth(*s.User, *s.Password)
|
||||
}
|
||||
|
||||
// if no subscription token nor subscription user:pass, check for default token then default user:pass
|
||||
|
||||
@@ -330,7 +330,7 @@ default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
||||
|
||||
app, _, stdout, _ := newTestApp()
|
||||
|
||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--config=" + filename, "mytopic"}))
|
||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "mytopic"}))
|
||||
|
||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||
}
|
||||
@@ -355,7 +355,63 @@ default-password: mypass
|
||||
|
||||
app, _, stdout, _ := newTestApp()
|
||||
|
||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--config=" + filename, "mytopic"}))
|
||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "mytopic"}))
|
||||
|
||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||
}
|
||||
|
||||
func TestCLI_Subscribe_Override_Default_UserPass_With_Empty_UserPass(t *testing.T) {
|
||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
||||
require.Equal(t, "", r.Header.Get("Authorization"))
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(message))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||
default-host: %s
|
||||
default-user: philipp
|
||||
default-password: mypass
|
||||
subscribe:
|
||||
- topic: mytopic
|
||||
user: ""
|
||||
password: ""
|
||||
`, server.URL)), 0600))
|
||||
|
||||
app, _, stdout, _ := newTestApp()
|
||||
|
||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
||||
|
||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||
}
|
||||
|
||||
func TestCLI_Subscribe_Override_Default_Token_With_Empty_Token(t *testing.T) {
|
||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
||||
require.Equal(t, "", r.Header.Get("Authorization"))
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(message))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||
default-host: %s
|
||||
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
||||
subscribe:
|
||||
- topic: mytopic
|
||||
token: ""
|
||||
`, server.URL)), 0600))
|
||||
|
||||
app, _, stdout, _ := newTestApp()
|
||||
|
||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
||||
|
||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||
}
|
||||
|
||||
@@ -44,6 +44,14 @@ Here are a few working sample configs:
|
||||
attachment-cache-dir: "/var/cache/ntfy/attachments"
|
||||
```
|
||||
|
||||
=== "server.yml (behind proxy, with cache + attachments)"
|
||||
``` yaml
|
||||
base-url: "http://ntfy.example.com"
|
||||
listen-http: ":2586"
|
||||
cache-file: "/var/cache/ntfy/cache.db"
|
||||
attachment-cache-dir: "/var/cache/ntfy/attachments"
|
||||
```
|
||||
|
||||
=== "server.yml (ntfy.sh config)"
|
||||
``` yaml
|
||||
# All the things: Behind a proxy, Firebase, cache, attachments,
|
||||
@@ -458,6 +466,31 @@ $ dig A mx1.ntfy.sh +short
|
||||
3.139.215.220
|
||||
```
|
||||
|
||||
### Local-only email
|
||||
If you want to send emails from an internal service on the same network as your ntfy instance, you do not need to
|
||||
worry about DNS records at all. Define a port for the SMTP server and pick an SMTP server domain (can be
|
||||
anything).
|
||||
|
||||
=== "/etc/ntfy/server.yml"
|
||||
``` yaml
|
||||
smtp-server-listen: ":25"
|
||||
smtp-server-domain: "example.com"
|
||||
smtp-server-addr-prefix: "ntfy-" # optional
|
||||
```
|
||||
|
||||
Then, in the email settings of your internal service, set the SMTP server address to the IP address of your
|
||||
ntfy instance. Set the port to the value you defined in `smtp-server-listen`. Leave any username and password
|
||||
fields empty. In the "From" address, pick anything (e.g., "alerts@ntfy.sh"); the value doesn't matter.
|
||||
In the "To" address, put in an email address that follows this pattern: `[topic]@[smtp-server-domain]` (or
|
||||
`[smtp-server-addr-prefix][topic]@[smtp-server-domain]` if you set `smtp-server-addr-prefix`).
|
||||
|
||||
So if you used `example.com` as the SMTP server domain, and you want to send a message to the `email-alerts`
|
||||
topic, set the "To" address to `email-alerts@example.com`. If the topic has access restrictions, you will need
|
||||
to include an access token in the "To" address, such as `email-alerts+tk_AbC123dEf456@example.com`.
|
||||
|
||||
If the internal service lets you use define an email "Subject", it will become the title of the notification.
|
||||
The body of the email will become the message of the notification.
|
||||
|
||||
## Behind a proxy (TLS, etc.)
|
||||
!!! warning
|
||||
If you are running ntfy behind a proxy, you must set the `behind-proxy` flag. Otherwise, all visitors are
|
||||
@@ -649,8 +682,8 @@ or the root domain:
|
||||
<VirtualHost *:80>
|
||||
ServerName ntfy.sh
|
||||
|
||||
# Proxy connections to ntfy (requires "a2enmod proxy")
|
||||
ProxyPass / http://127.0.0.1:2586/
|
||||
# Proxy connections to ntfy (requires "a2enmod proxy proxy_http")
|
||||
ProxyPass / http://127.0.0.1:2586/ upgrade=websocket
|
||||
ProxyPassReverse / http://127.0.0.1:2586/
|
||||
|
||||
SetEnv proxy-nokeepalive 1
|
||||
@@ -658,19 +691,13 @@ or the root domain:
|
||||
|
||||
# Higher than the max message size of 4096 bytes
|
||||
LimitRequestBody 102400
|
||||
|
||||
# Enable mod_rewrite (requires "a2enmod rewrite")
|
||||
RewriteEngine on
|
||||
|
||||
# WebSockets support (requires "a2enmod rewrite proxy_wstunnel")
|
||||
RewriteCond %{HTTP:Upgrade} websocket [NC]
|
||||
RewriteCond %{HTTP:Connection} upgrade [NC]
|
||||
RewriteRule ^/?(.*) "ws://127.0.0.1:2586/$1" [P,L]
|
||||
|
||||
# Redirect HTTP to HTTPS, but only for GET topic addresses, since we want
|
||||
# it to work with curl without the annoying https:// prefix
|
||||
RewriteCond %{REQUEST_METHOD} GET
|
||||
RewriteRule ^/([-_A-Za-z0-9]{0,64})$ https://%{SERVER_NAME}/$1 [R,L]
|
||||
# it to work with curl without the annoying https:// prefix (requires "a2enmod alias")
|
||||
<If "%{REQUEST_METHOD} == 'GET'">
|
||||
RedirectMatch permanent "^/([-_A-Za-z0-9]{0,64})$" "https://%{SERVER_NAME}/$1"
|
||||
</If>
|
||||
|
||||
</VirtualHost>
|
||||
|
||||
<VirtualHost *:443>
|
||||
@@ -681,8 +708,8 @@ or the root domain:
|
||||
SSLCertificateKeyFile /etc/letsencrypt/live/ntfy.sh/privkey.pem
|
||||
Include /etc/letsencrypt/options-ssl-apache.conf
|
||||
|
||||
# Proxy connections to ntfy (requires "a2enmod proxy")
|
||||
ProxyPass / http://127.0.0.1:2586/
|
||||
# Proxy connections to ntfy (requires "a2enmod proxy proxy_http")
|
||||
ProxyPass / http://127.0.0.1:2586/ upgrade=websocket
|
||||
ProxyPassReverse / http://127.0.0.1:2586/
|
||||
|
||||
SetEnv proxy-nokeepalive 1
|
||||
@@ -690,14 +717,7 @@ or the root domain:
|
||||
|
||||
# Higher than the max message size of 4096 bytes
|
||||
LimitRequestBody 102400
|
||||
|
||||
# Enable mod_rewrite (requires "a2enmod rewrite")
|
||||
RewriteEngine on
|
||||
|
||||
# WebSockets support (requires "a2enmod rewrite proxy_wstunnel")
|
||||
RewriteCond %{HTTP:Upgrade} websocket [NC]
|
||||
RewriteCond %{HTTP:Connection} upgrade [NC]
|
||||
RewriteRule ^/?(.*) "ws://127.0.0.1:2586/$1" [P,L]
|
||||
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
|
||||
12
docs/faq.md
12
docs/faq.md
@@ -76,6 +76,18 @@ However, if you still want to disable it, you can do so with the `web-root: disa
|
||||
Think of the ntfy web app like an Android/iOS app. It is freely available and accessible to anyone, yet useless without
|
||||
a proper backend. So as long as you secure your backend with ACLs, exposing the ntfy web app to the Internet is harmless.
|
||||
|
||||
## If topic names are public, could I not just brute force them?
|
||||
If you don't have [ACLs set up](config.md#access-control), the topic name is your password, it says so everywhere. If you
|
||||
choose a easy-to-guess/dumb topic name, people will be able to guess it. If you choose a randomly generated topic name,
|
||||
the topic is as good as a good password.
|
||||
|
||||
As for brute forcing: It's not possible to brute force a ntfy server for very long, as you'll get quickly rate limited.
|
||||
In the default configuration, you'll be able to do 60 requests as a burst, and then 1 request per 10 seconds. Assuming you
|
||||
choose a random 10 digit topic name using only A-Z, a-z, 0-9, _ and -, there are 64^10 possible topic names. Even if you
|
||||
could do hundreds of requests per seconds (which you cannot), it would take many years to brute force a topic name.
|
||||
|
||||
For ntfy.sh, there's even a fail2ban in place which will ban your IP pretty quickly.
|
||||
|
||||
## Where can I donate?
|
||||
I have just very recently started accepting donations via [GitHub Sponsors](https://github.com/sponsors/binwiederhier).
|
||||
I would be humbled if you helped me carry the server and developer account costs. Even small donations are very much
|
||||
|
||||
@@ -29,37 +29,37 @@ deb/rpm packages.
|
||||
|
||||
=== "x86_64/amd64"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.2/ntfy_2.6.2_linux_amd64.tar.gz
|
||||
tar zxvf ntfy_2.6.2_linux_amd64.tar.gz
|
||||
sudo cp -a ntfy_2.6.2_linux_amd64/ntfy /usr/local/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.6.2_linux_amd64/{client,server}/*.yml /etc/ntfy
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_amd64.tar.gz
|
||||
tar zxvf ntfy_2.7.0_linux_amd64.tar.gz
|
||||
sudo cp -a ntfy_2.7.0_linux_amd64/ntfy /usr/local/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.7.0_linux_amd64/{client,server}/*.yml /etc/ntfy
|
||||
sudo ntfy serve
|
||||
```
|
||||
|
||||
=== "armv6"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.2/ntfy_2.6.2_linux_armv6.tar.gz
|
||||
tar zxvf ntfy_2.6.2_linux_armv6.tar.gz
|
||||
sudo cp -a ntfy_2.6.2_linux_armv6/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.6.2_linux_armv6/{client,server}/*.yml /etc/ntfy
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_armv6.tar.gz
|
||||
tar zxvf ntfy_2.7.0_linux_armv6.tar.gz
|
||||
sudo cp -a ntfy_2.7.0_linux_armv6/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.7.0_linux_armv6/{client,server}/*.yml /etc/ntfy
|
||||
sudo ntfy serve
|
||||
```
|
||||
|
||||
=== "armv7/armhf"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.2/ntfy_2.6.2_linux_armv7.tar.gz
|
||||
tar zxvf ntfy_2.6.2_linux_armv7.tar.gz
|
||||
sudo cp -a ntfy_2.6.2_linux_armv7/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.6.2_linux_armv7/{client,server}/*.yml /etc/ntfy
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_armv7.tar.gz
|
||||
tar zxvf ntfy_2.7.0_linux_armv7.tar.gz
|
||||
sudo cp -a ntfy_2.7.0_linux_armv7/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.7.0_linux_armv7/{client,server}/*.yml /etc/ntfy
|
||||
sudo ntfy serve
|
||||
```
|
||||
|
||||
=== "arm64"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.2/ntfy_2.6.2_linux_arm64.tar.gz
|
||||
tar zxvf ntfy_2.6.2_linux_arm64.tar.gz
|
||||
sudo cp -a ntfy_2.6.2_linux_arm64/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.6.2_linux_arm64/{client,server}/*.yml /etc/ntfy
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_arm64.tar.gz
|
||||
tar zxvf ntfy_2.7.0_linux_arm64.tar.gz
|
||||
sudo cp -a ntfy_2.7.0_linux_arm64/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.7.0_linux_arm64/{client,server}/*.yml /etc/ntfy
|
||||
sudo ntfy serve
|
||||
```
|
||||
|
||||
@@ -109,7 +109,7 @@ Manually installing the .deb file:
|
||||
|
||||
=== "x86_64/amd64"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.2/ntfy_2.6.2_linux_amd64.deb
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_amd64.deb
|
||||
sudo dpkg -i ntfy_*.deb
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
@@ -117,7 +117,7 @@ Manually installing the .deb file:
|
||||
|
||||
=== "armv6"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.2/ntfy_2.6.2_linux_armv6.deb
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_armv6.deb
|
||||
sudo dpkg -i ntfy_*.deb
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
@@ -125,7 +125,7 @@ Manually installing the .deb file:
|
||||
|
||||
=== "armv7/armhf"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.2/ntfy_2.6.2_linux_armv7.deb
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_armv7.deb
|
||||
sudo dpkg -i ntfy_*.deb
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
@@ -133,7 +133,7 @@ Manually installing the .deb file:
|
||||
|
||||
=== "arm64"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.2/ntfy_2.6.2_linux_arm64.deb
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_arm64.deb
|
||||
sudo dpkg -i ntfy_*.deb
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
@@ -143,28 +143,28 @@ Manually installing the .deb file:
|
||||
|
||||
=== "x86_64/amd64"
|
||||
```bash
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.6.2/ntfy_2.6.2_linux_amd64.rpm
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_amd64.rpm
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
```
|
||||
|
||||
=== "armv6"
|
||||
```bash
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.6.2/ntfy_2.6.2_linux_armv6.rpm
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_armv6.rpm
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
```
|
||||
|
||||
=== "armv7/armhf"
|
||||
```bash
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.6.2/ntfy_2.6.2_linux_armv7.rpm
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_armv7.rpm
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
```
|
||||
|
||||
=== "arm64"
|
||||
```bash
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.6.2/ntfy_2.6.2_linux_arm64.rpm
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_arm64.rpm
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
```
|
||||
@@ -194,18 +194,18 @@ NixOS also supports [declarative setup of the ntfy server](https://search.nixos.
|
||||
|
||||
## macOS
|
||||
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on macOS as well.
|
||||
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.6.2/ntfy_2.6.2_darwin_all.tar.gz),
|
||||
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_darwin_all.tar.gz),
|
||||
extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`).
|
||||
|
||||
If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For all other users, it'll look for it at
|
||||
`~/Library/Application Support/ntfy/client.yml` (sample included in the tarball).
|
||||
|
||||
```bash
|
||||
curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.6.2/ntfy_2.6.2_darwin_all.tar.gz > ntfy_2.6.2_darwin_all.tar.gz
|
||||
tar zxvf ntfy_2.6.2_darwin_all.tar.gz
|
||||
sudo cp -a ntfy_2.6.2_darwin_all/ntfy /usr/local/bin/ntfy
|
||||
curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_darwin_all.tar.gz > ntfy_2.7.0_darwin_all.tar.gz
|
||||
tar zxvf ntfy_2.7.0_darwin_all.tar.gz
|
||||
sudo cp -a ntfy_2.7.0_darwin_all/ntfy /usr/local/bin/ntfy
|
||||
mkdir ~/Library/Application\ Support/ntfy
|
||||
cp ntfy_2.6.2_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
|
||||
cp ntfy_2.7.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
|
||||
ntfy --help
|
||||
```
|
||||
|
||||
@@ -223,7 +223,7 @@ brew install ntfy
|
||||
|
||||
## Windows
|
||||
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well.
|
||||
To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.6.2/ntfy_2.6.2_windows_amd64.zip),
|
||||
To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_windows_amd64.zip),
|
||||
extract it and place the `ntfy.exe` binary somewhere in your `%Path%`.
|
||||
|
||||
The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file).
|
||||
|
||||
@@ -23,6 +23,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had
|
||||
- [Platypush](https://docs.platypush.tech/platypush/plugins/ntfy.html) - Automation platform aimed to run on any device that can run Python
|
||||
- [diun](https://crazymax.dev/diun/) - Docker Image Update Notifier
|
||||
- [Cloudron](https://www.cloudron.io/store/sh.ntfy.cloudronapp.html) - Platform that makes it easy to manage web apps on your server
|
||||
- [Xitoring](https://xitoring.com/docs/notifications/notification-roles/ntfy/) - Server and Uptime monitoring
|
||||
|
||||
## Integration via HTTP/SMTP/etc.
|
||||
|
||||
@@ -56,6 +57,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had
|
||||
- [ntfy](https://github.com/ffflorian/ntfy) - Send notifications over ntfy (JS)
|
||||
- [ntfy_dart](https://github.com/jr1221/ntfy_dart) - Dart wrapper around the ntfy API (Dart)
|
||||
- [gotfy](https://github.com/AnthonyHewins/gotfy) - A Go wrapper for the ntfy API (Go)
|
||||
- [symfony/ntfy-notifier](https://symfony.com/components/NtfyNotifier) ⭐ - Symfony Notifier integration for ntfy (PHP)
|
||||
|
||||
## CLIs + GUIs
|
||||
|
||||
@@ -126,9 +128,14 @@ I've added a ⭐ to projects or posts that have a significant following, or had
|
||||
- [msgdrop](https://github.com/jbrubake/msgdrop) - Send and receive encrypted messages (Bash)
|
||||
- [vigilant](https://github.com/VerifiedJoseph/vigilant) - Monitor RSS/ATOM and JSON feeds, and send push notifications on new entries (PHP)
|
||||
- [ansible-role-ntfy-alertmanager](https://github.com/bleetube/ansible-role-ntfy-alertmanager) - Ansible role to install xenrox/ntfy-alertmanager
|
||||
|
||||
- [NtfyMe-Blender](https://github.com/NotNanook/NtfyMe-Blender) - Blender addon to send notifications to NtfyMe (Python)
|
||||
- [ntfy-ios-filesharing](https://www.icloud.com/shortcuts/fe948d151b2e4ae08fb2f9d6b27d680b) - An iOS shortcut that let's you share files from your share feed to a topic of your choice.
|
||||
|
||||
## Blog + forum posts
|
||||
|
||||
- [NetworkChunk - how did I NOT know about this?](https://www.youtube.com/watch?v=poDIT2ruQ9M) ⭐ - youtube.com - 8/2023
|
||||
- [Open Source Push Notifications! Get notified of any event you can imagine. Triggers abound!](https://www.youtube.com/watch?v=WJgwWXt79pE) ⭐ - youtube.com - 8/2023
|
||||
- [How to install and self host an Ntfy server on Linux](https://linuxconfig.org/how-to-install-and-self-host-an-ntfy-server-on-linux) - linuxconfig.org - 7/2023
|
||||
- [Basic website monitoring using cronjobs and ntfy.sh](https://burkhardt.dev/2023/website-monitoring-cron-ntfy/) - burkhardt.dev - 6/2023
|
||||
- [Pingdom alternative in one line of curl through ntfy.sh](https://piqoni.bearblog.dev/uptime-monitoring-in-one-line-of-curl/) - bearblog.dev - 6/2023
|
||||
- [#OpenSourceDiscovery 78: ntfy.sh](https://opensourcedisc.substack.com/p/opensourcediscovery-78-ntfysh) - opensourcedisc.substack.com - 6/2023
|
||||
@@ -155,6 +162,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had
|
||||
- [NTFY - système de notification hyper simple et complet](https://www.youtube.com/watch?v=UieZYWVVgA4) - youtube.com - 12/2022
|
||||
- [ntfy.sh](https://paramdeo.com/til/ntfy-sh) - paramdeo.com - 11/2022
|
||||
- [Using ntfy to warn me when my computer is discharging](https://ulysseszh.github.io/programming/2022/11/28/ntfy-warn-discharge.html) - ulysseszh.github.io - 11/2022
|
||||
- [Enabling SSH Login Notifications using Ntfy](https://paramdeo.com/blog/enabling-ssh-login-notifications-using-ntfy) - paramdeo.com - 11/2022
|
||||
- [ntfy - Push Notification Service](https://dizzytech.de/posts/ntfy/) - dizzytech.de - 11/2022
|
||||
- [Console #132](https://console.substack.com/p/console-132) ⭐ - console.substack.com - 11/2022
|
||||
- [How to make my phone buzz*](https://evbogue.com/howtomakemyphonebuzz) - evbogue.com - 11/2022
|
||||
|
||||
@@ -27,6 +27,13 @@ Be sure that in your selfhosted server:
|
||||
* Set `upstream-base-url: "https://ntfy.sh"` (**not your own hostname!**)
|
||||
* Ensure that the URL you set in `base-url` **matches exactly** what you set the Default Server in iOS to
|
||||
|
||||
## iOS app seeing "New message", but not real message content
|
||||
If you see `New message` notifications on iOS, your iPhone can likely not talk to your self-hosted server. Be sure that
|
||||
your iOS device and your ntfy server are either on the same network, or that your phone can actually reach the server.
|
||||
|
||||
Turn on tracing/debugging on the server (via `log-level: trace` or `log-level: debug`, see [troubleshooting](troubleshooting.md)),
|
||||
and read docs on [iOS instant notifications](https://docs.ntfy.sh/config/#ios-instant-notifications).
|
||||
|
||||
## Firefox on Android not automatically subscribing to web push (see [#789](https://github.com/binwiederhier/ntfy/issues/789))
|
||||
ntfy defaults to web-push based subscriptions when installed as a [progressive web app](./subscribe/pwa.md). Firefox
|
||||
Android has an [open bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1796434) where it reports the PWA mode incorrectly.
|
||||
|
||||
@@ -457,6 +457,7 @@ You can set the priority with the header `X-Priority` (or any of its aliases: `P
|
||||
=== "PowerShell"
|
||||
``` powershell
|
||||
$Request = @{
|
||||
Method = 'POST'
|
||||
URI = "https://ntfy.sh/phil_alerts"
|
||||
Headers = @{
|
||||
Priority = "5"
|
||||
@@ -1033,7 +1034,7 @@ is the only required one:
|
||||
$Request = @{
|
||||
Method = "POST"
|
||||
URI = "https://ntfy.sh"
|
||||
Body = @{
|
||||
Body = ConvertTo-JSON @{
|
||||
Topic = "mytopic"
|
||||
Title = "Low disk space alert"
|
||||
Message = "Disk space is low at 5.1 GB"
|
||||
@@ -1042,7 +1043,7 @@ is the only required one:
|
||||
FileName = "diskspace.jpg"
|
||||
Tags = @("warning", "cd")
|
||||
Click = "https://homecamera.lan/xasds1h2xsSsa/"
|
||||
Actions = ConvertTo-JSON @(
|
||||
Actions = @(
|
||||
@{
|
||||
Action = "view"
|
||||
Label = "Admin panel"
|
||||
@@ -1919,10 +1920,10 @@ And the same example using [JSON publishing](#publish-as-json):
|
||||
$Request = @{
|
||||
Method = "POST"
|
||||
URI = "https://ntfy.sh"
|
||||
Body = @{
|
||||
Body = ConvertTo-Json -Depth 3 @{
|
||||
Topic = "wifey"
|
||||
Message = "Your wife requested you send a picture of yourself."
|
||||
Actions = ConvertTo-Json -Depth 3 @(
|
||||
Actions = @(
|
||||
@{
|
||||
Action = "broadcast"
|
||||
Label = "Take picture"
|
||||
@@ -2072,7 +2073,7 @@ Here's an example using the [`X-Actions` header](#using-a-header):
|
||||
'method' => 'POST',
|
||||
'header' =>
|
||||
"Content-Type: text/plain\r\n" .
|
||||
"Actions: http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}",
|
||||
'Actions: http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}',
|
||||
'content' => 'Garage door has been open for 15 minutes. Close it?'
|
||||
]
|
||||
]));
|
||||
@@ -2199,10 +2200,10 @@ And the same example using [JSON publishing](#publish-as-json):
|
||||
$Request = @{
|
||||
Method = "POST"
|
||||
URI = "https://ntfy.sh"
|
||||
Body = @{
|
||||
Body = ConvertTo-Json -Depth 3 @{
|
||||
Topic = "myhome"
|
||||
Message = "Garage door has been open for 15 minutes. Close it?"
|
||||
Actions = ConvertTo-Json -Depth 3 @(
|
||||
Actions = @(
|
||||
@{
|
||||
Action = "http"
|
||||
Label = "Close door"
|
||||
|
||||
@@ -2,6 +2,38 @@
|
||||
Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases)
|
||||
and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).
|
||||
|
||||
## ntfy server v2.7.0
|
||||
Released August 17, 2023
|
||||
|
||||
This release ships Markdown support for the web app (not in the Android app yet), and adds support for
|
||||
right-to-left languages (RTL) in the web app. It also fixes a few issues around date/time formatting,
|
||||
internationalization support, a CLI auth bug.
|
||||
|
||||
Furthermore, it fixes a security issue around access tokens getting erroneously deleted for other users
|
||||
in a specific scenario. This was a denial-of-service-type security issue, since it **effectively allowed a
|
||||
single user to deny access to all other users of a ntfy instance**. Please note that while tokens were
|
||||
erroneously deleted, **nobody but the token owner ever had access to it.** Please refer to [the ticket](https://github.com/binwiederhier/ntfy/issues/838)
|
||||
for details. **Please upgrade your ntfy instance if you run a multi-user system.**
|
||||
|
||||
**Features:**
|
||||
|
||||
* Add support for [Markdown formatting](publish.md#markdown-formatting) in web app ([#310](https://github.com/binwiederhier/ntfy/issues/310), thanks to [@nihalgonsalves](https://github.com/nihalgonsalves))
|
||||
* Add support for right-to-left languages (RTL) in the web app ([#663](https://github.com/binwiederhier/ntfy/issues/663), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
||||
|
||||
**Security:** ⚠️
|
||||
|
||||
* Fixes issue with access tokens getting deleted ([#838](https://github.com/binwiederhier/ntfy/issues/838))
|
||||
|
||||
**Bug fixes + maintenance:**
|
||||
|
||||
* Fix issues with date/time with different locales ([#700](https://github.com/binwiederhier/ntfy/issues/700), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
||||
* Re-init i18n on each service worker message to avoid missing translations ([#817](https://github.com/binwiederhier/ntfy/pull/817), thanks to [@nihalgonsalves](https://github.com/nihalgonsalves))
|
||||
* You can now unset the default user:pass/token in `client.yml` for an individual subscription to remove the Authorization header ([#829](https://github.com/binwiederhier/ntfy/issues/829), thanks to [@tomeon](https://github.com/tomeon) for reporting and to [@wunter8](https://github.com/wunter8) for fixing)
|
||||
|
||||
**Documentation:**
|
||||
|
||||
* Update docs for Apache config ([#819](https://github.com/binwiederhier/ntfy/pull/819), thanks to [@nisbet-hubbard](https://github.com/nisbet-hubbard))
|
||||
|
||||
## ntfy server v2.6.2
|
||||
Released June 30, 2023
|
||||
|
||||
@@ -78,7 +110,7 @@ if you use promo code `MYTOPIC`). ntfy will always remain open source.
|
||||
## ntfy server v2.4.0
|
||||
Released Apr 26, 2023
|
||||
|
||||
This release adds a tiny `v1/stats` endpoint to expose how many messages have been published, and adds suport to encode the `X-Title`,
|
||||
This release adds a tiny `v1/stats` endpoint to expose how many messages have been published, and adds support to encode the `X-Title`,
|
||||
`X-Message` and `X-Tags` header as RFC 2047. It's a pretty small release, and mainly enables the release of the new ntfy.sh website.
|
||||
|
||||
❤️ If you like ntfy, **please consider sponsoring me** via [GitHub Sponsors](https://github.com/sponsors/binwiederhier)
|
||||
@@ -1251,16 +1283,13 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
|
||||
|
||||
## Not released yet
|
||||
|
||||
### ntfy server v2.7.0 (UNRELEASED)
|
||||
|
||||
**Features:**
|
||||
|
||||
* Add support for [Markdown formatting](publish.md#markdown-formatting) in web app ([#310](https://github.com/binwiederhier/ntfy/issues/310), thanks to [@nihalgonsalves](https://github.com/nihalgonsalves))
|
||||
* Add support for right-to-left languages (RTL) in the web app ([#663](https://github.com/binwiederhier/ntfy/issues/663), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
||||
### ntfy server v2.8.0 (UNRELEASED)
|
||||
|
||||
**Bug fixes + maintenance:**
|
||||
|
||||
* Fix issues with date/time with different locales ([#700](https://github.com/binwiederhier/ntfy/issues/700), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
||||
* Fix ACL issue with topic patterns containing underscores ([#840](https://github.com/binwiederhier/ntfy/issues/840), thanks to [@Joe-0237](https://github.com/Joe-0237) for reporting)
|
||||
* Re-add `tzdata` to Docker images for amd64 image ([#894](https://github.com/binwiederhier/ntfy/issues/894), [#307](https://github.com/binwiederhier/ntfy/pull/307))
|
||||
* Add special logic to ignore `Priority` header if it resembled a RFC 9218 value ([#851](https://github.com/binwiederhier/ntfy/pull/851)/[#895](https://github.com/binwiederhier/ntfy/pull/895), thanks to [@gusdleon](https://github.com/gusdleon), see also [#351](https://github.com/binwiederhier/ntfy/issues/351), [#353](https://github.com/binwiederhier/ntfy/issues/353), [#461](https://github.com/binwiederhier/ntfy/issues/461))
|
||||
|
||||
### ntfy Android app v1.16.1 (UNRELEASED)
|
||||
|
||||
|
||||
52
go.mod
52
go.mod
@@ -3,40 +3,42 @@ module heckel.io/ntfy
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
cloud.google.com/go/firestore v1.11.0 // indirect
|
||||
cloud.google.com/go/storage v1.31.0 // indirect
|
||||
cloud.google.com/go/firestore v1.13.0 // indirect
|
||||
cloud.google.com/go/storage v1.33.0 // indirect
|
||||
github.com/BurntSushi/toml v1.3.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/emersion/go-smtp v0.17.0
|
||||
github.com/emersion/go-smtp v0.18.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.2
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/mattn/go-sqlite3 v1.14.17
|
||||
github.com/olebedev/when v1.0.0
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/urfave/cli/v2 v2.25.7
|
||||
golang.org/x/crypto v0.11.0
|
||||
golang.org/x/oauth2 v0.10.0 // indirect
|
||||
golang.org/x/crypto v0.13.0
|
||||
golang.org/x/oauth2 v0.12.0 // indirect
|
||||
golang.org/x/sync v0.3.0
|
||||
golang.org/x/term v0.10.0
|
||||
golang.org/x/term v0.12.0
|
||||
golang.org/x/time v0.3.0
|
||||
google.golang.org/api v0.130.0
|
||||
google.golang.org/api v0.142.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
replace github.com/emersion/go-smtp => github.com/emersion/go-smtp v0.17.0 // Pin version due to breaking changes, see #839
|
||||
|
||||
require github.com/pkg/errors v0.9.1 // indirect
|
||||
|
||||
require (
|
||||
firebase.google.com/go/v4 v4.11.0
|
||||
firebase.google.com/go/v4 v4.12.0
|
||||
github.com/SherClockHolmes/webpush-go v1.2.0
|
||||
github.com/prometheus/client_golang v1.16.0
|
||||
github.com/stripe/stripe-go/v74 v74.25.0
|
||||
github.com/stripe/stripe-go/v74 v74.30.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.110.4 // indirect
|
||||
cloud.google.com/go/compute v1.20.1 // indirect
|
||||
cloud.google.com/go v0.110.8 // indirect
|
||||
cloud.google.com/go/compute v1.23.0 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
cloud.google.com/go/iam v1.1.1 // indirect
|
||||
cloud.google.com/go/iam v1.1.2 // indirect
|
||||
cloud.google.com/go/longrunning v0.5.1 // indirect
|
||||
github.com/AlekSi/pointer v1.2.0 // indirect
|
||||
github.com/MicahParks/keyfunc v1.9.0 // indirect
|
||||
@@ -49,30 +51,30 @@ require (
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/s2a-go v0.1.4 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/google/uuid v1.3.1 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.1 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.4.0 // indirect
|
||||
github.com/prometheus/common v0.44.0 // indirect
|
||||
github.com/prometheus/procfs v0.11.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/net v0.12.0 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
golang.org/x/text v0.11.0 // indirect
|
||||
golang.org/x/net v0.15.0 // indirect
|
||||
golang.org/x/sys v0.12.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/appengine/v2 v2.0.3 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130 // indirect
|
||||
google.golang.org/grpc v1.56.2 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/appengine/v2 v2.0.5 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230920204549-e6e6cdab5c13 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230920204549-e6e6cdab5c13 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 // indirect
|
||||
google.golang.org/grpc v1.58.2 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
156
go.sum
156
go.sum
@@ -1,23 +1,20 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.110.3 h1:wwearW+L7sAPSomPIgJ3bVn6Ck00HGQnn5HMLwf0azo=
|
||||
cloud.google.com/go v0.110.3/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI=
|
||||
cloud.google.com/go v0.110.4 h1:1JYyxKMN9hd5dR2MYTPWkGUgcoxVVhg0LKNKEo0qvmk=
|
||||
cloud.google.com/go v0.110.4/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI=
|
||||
cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZNbg=
|
||||
cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
|
||||
cloud.google.com/go v0.110.8 h1:tyNdfIxjzaWctIiLYOTalaLKZ17SI44SKFW26QbOhME=
|
||||
cloud.google.com/go v0.110.8/go.mod h1:Iz8AkXJf1qmxC3Oxoep8R1T36w8B92yU29PcBhHO5fk=
|
||||
cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY=
|
||||
cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
|
||||
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||
cloud.google.com/go/firestore v1.11.0 h1:PPgtwcYUOXV2jFe1bV3nda3RCrOa8cvBjTOn2MQVfW8=
|
||||
cloud.google.com/go/firestore v1.11.0/go.mod h1:b38dKhgzlmNNGTNZZwe7ZRFEuRab1Hay3/DBsIGKKy4=
|
||||
cloud.google.com/go/iam v1.1.1 h1:lW7fzj15aVIXYHREOqjRBV9PsH0Z6u8Y46a1YGvQP4Y=
|
||||
cloud.google.com/go/iam v1.1.1/go.mod h1:A5avdyVL2tCppe4unb0951eI9jreack+RJ0/d+KUZOU=
|
||||
cloud.google.com/go/firestore v1.13.0 h1:/3S4RssUV4GO/kvgJZB+tayjhOfyAHs+KcpJgRVu/Qk=
|
||||
cloud.google.com/go/firestore v1.13.0/go.mod h1:QojqqOh8IntInDUSTAh0c8ZsPYAr68Ma8c5DWOy8xb8=
|
||||
cloud.google.com/go/iam v1.1.2 h1:gacbrBdWcoVmGLozRuStX45YKvJtzIjJdAolzUs1sm4=
|
||||
cloud.google.com/go/iam v1.1.2/go.mod h1:A5avdyVL2tCppe4unb0951eI9jreack+RJ0/d+KUZOU=
|
||||
cloud.google.com/go/longrunning v0.5.1 h1:Fr7TXftcqTudoyRJa113hyaqlGdiBQkp0Gq7tErFDWI=
|
||||
cloud.google.com/go/longrunning v0.5.1/go.mod h1:spvimkwdz6SPWKEt/XBij79E9fiTkHSQl/fRUUQJYJc=
|
||||
cloud.google.com/go/storage v1.31.0 h1:+S3LjjEN2zZ+L5hOwj4+1OkGCsLVe0NzpXKQ1pSdTCI=
|
||||
cloud.google.com/go/storage v1.31.0/go.mod h1:81ams1PrhW16L4kF7qg+4mTq7SRs5HsbDTM0bWvrwJ0=
|
||||
firebase.google.com/go/v4 v4.11.0 h1:szjBoiF33A2FavRLIDZjW1mw+OsW/XAtHoYNIqWOjRk=
|
||||
firebase.google.com/go/v4 v4.11.0/go.mod h1:60c36dWLK4+j05Vw5XMllek3b3PCynU3BfI46OSwsUE=
|
||||
cloud.google.com/go/storage v1.33.0 h1:PVrDOkIC8qQVa1P3SXGpQvfuJhN2LHOoyZvWs8D2X5M=
|
||||
cloud.google.com/go/storage v1.33.0/go.mod h1:Hhh/dogNRGca7IWv1RC2YqEn0c0G77ctA/OxflYkiD8=
|
||||
firebase.google.com/go/v4 v4.12.0 h1:I6dCkcWUMFNkFdWgzlf8SLWecQnKdFgJhMv5fT9l1qI=
|
||||
firebase.google.com/go/v4 v4.12.0/go.mod h1:60c36dWLK4+j05Vw5XMllek3b3PCynU3BfI46OSwsUE=
|
||||
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
|
||||
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
@@ -27,20 +24,13 @@ github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID
|
||||
github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
|
||||
github.com/SherClockHolmes/webpush-go v1.2.0 h1:sGv0/ZWCvb1HUH+izLqrb2i68HuqD/0Y+AmGQfyqKJA=
|
||||
github.com/SherClockHolmes/webpush-go v1.2.0/go.mod h1:w6X47YApe/B9wUz2Wh8xukxlyupaxSSEbu6yKJcHN2w=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
|
||||
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
@@ -50,19 +40,14 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
|
||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-smtp v0.16.0 h1:eB9CY9527WdEZSs5sWisTmilDX7gG+Q/2IdRcmubpa8=
|
||||
github.com/emersion/go-smtp v0.16.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||
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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
@@ -74,16 +59,13 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
@@ -99,20 +81,17 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
|
||||
github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc=
|
||||
github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
|
||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.5 h1:UR4rDjcgpgEnqpIEvkiqTYKBCKLNmlge2eVjoZfySzM=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.5/go.mod h1:RxW0N9901Cko1VOCW3SXCpWP+mlIEkk2tP7jnHy9a3w=
|
||||
github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cUUI8Ki4=
|
||||
github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI=
|
||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.1 h1:SBWmZhjUDRorQxrN0nwzf+AHBxnbFjViHQS4P0yVpmQ=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.1/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
|
||||
github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
@@ -133,9 +112,8 @@ github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUo
|
||||
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
|
||||
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
|
||||
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
||||
github.com/prometheus/procfs v0.11.0 h1:5EAgkfkMl659uZPbe9AS2N68a7Cc1TJbPEuGzFuRbyk=
|
||||
github.com/prometheus/procfs v0.11.0/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
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=
|
||||
@@ -143,16 +121,13 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stripe/stripe-go/v74 v74.24.0 h1:h+hXEI5avC5moAh2YLtphMFTBnp11TfXTcP4suuWDLk=
|
||||
github.com/stripe/stripe-go/v74 v74.24.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw=
|
||||
github.com/stripe/stripe-go/v74 v74.25.0 h1:mGJp9L1ymxjFvq5MlmG6ynv/fAGX6LLU8MyMVsiRAMY=
|
||||
github.com/stripe/stripe-go/v74 v74.25.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw=
|
||||
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/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
|
||||
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||
@@ -160,16 +135,12 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsr
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
|
||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
@@ -177,28 +148,19 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
|
||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
||||
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
|
||||
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.9.0 h1:BPpt2kU7oMRq3kCHAA1tbSEshXRw1LpG2ztgDwrzuAs=
|
||||
golang.org/x/oauth2 v0.9.0/go.mod h1:qYgFZaFiu6Wg24azG8bdV52QJXJGbZzIIsRCdVKzbLw=
|
||||
golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
|
||||
golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
|
||||
golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4=
|
||||
golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -209,29 +171,25 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28=
|
||||
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
|
||||
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
|
||||
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
||||
golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -243,47 +201,32 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
google.golang.org/api v0.129.0 h1:2XbdjjNfFPXQyufzQVwPf1RRnHH8Den2pfNE2jw7L8w=
|
||||
google.golang.org/api v0.129.0/go.mod h1:dFjiXlanKwWE3612X97llhsoI36FAoIiRj3aTl5b/zE=
|
||||
google.golang.org/api v0.130.0 h1:A50ujooa1h9iizvfzA4rrJr2B7uRmWexwbekQ2+5FPQ=
|
||||
google.golang.org/api v0.130.0/go.mod h1:J/LCJMYSDFvAVREGCbrESb53n4++NMBDetSHGL5I5RY=
|
||||
google.golang.org/api v0.142.0 h1:mf+7EJ94fi5ZcnpPy+m0Yv2dkz8bKm+UL0snTCuwXlY=
|
||||
google.golang.org/api v0.142.0/go.mod h1:zJAN5o6HRqR7O+9qJUFOWrZkYE66RH+efPBdTLA4xBA=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine/v2 v2.0.3 h1:AyY/mipuqiyCIAqOevfmu5fMDc5/9P/QggWfCQYdkSA=
|
||||
google.golang.org/appengine/v2 v2.0.3/go.mod h1:2Z0TTdcXxnHdXzmp8drrmOExUDM2WQgyT33c6JDUlJM=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||
google.golang.org/appengine/v2 v2.0.5 h1:4C+F3Cd3L2nWEfSmFEZDPjQvDwL8T0YCeZBysZifP3k=
|
||||
google.golang.org/appengine/v2 v2.0.5/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20230629202037-9506855d4529 h1:9JucMWR7sPvCxUFd6UsOUNmA5kCcWOfORaT3tpAsKQs=
|
||||
google.golang.org/genproto v0.0.0-20230629202037-9506855d4529/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64=
|
||||
google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130 h1:Au6te5hbKUV8pIYWHqOUZ1pva5qK/rwbIhoXEUB9Lu8=
|
||||
google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:O9kGHb51iE/nOGvQaDUuadVYqovW56s5emA88lQnj6Y=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230629202037-9506855d4529 h1:s5YSX+ZH5b5vS9rnpGymvIyMpLRJizowqDlOuyjXnTk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230629202037-9506855d4529/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130 h1:XVeBY8d/FaK4848myy41HBqnDwvxeV3zMZhwN1TvAMU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:mPBs5jNgx2GuQGvFwUvVKqtn6HsUw9nP64BedgvqEsQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230629202037-9506855d4529 h1:DEH99RbiLZhMxrpEJCZ0A+wdTe0EOgou/poSLx9vWf4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230629202037-9506855d4529/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130 h1:2FZP5XuJY9zQyGM5N0rtovnoXjiMUEIUMvw0m9wlpLc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:8mL13HKkDa+IuJ8yruA3ci0q+0vsUz4m//+ottjwS5o=
|
||||
google.golang.org/genproto v0.0.0-20230920204549-e6e6cdab5c13 h1:vlzZttNJGVqTsRFU9AmdnrcO1Znh8Ew9kCD//yjigk0=
|
||||
google.golang.org/genproto v0.0.0-20230920204549-e6e6cdab5c13/go.mod h1:CCviP9RmpZ1mxVr8MUjCnSiY09IbAXZxhLE6EhHIdPU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230920204549-e6e6cdab5c13 h1:U7+wNaVuSTaUqNvK2+osJ9ejEZxbjHHk8F2b6Hpx0AE=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230920204549-e6e6cdab5c13/go.mod h1:RdyHbowztCGQySiCvQPgWQWgWhGnouTdCflKoDBt32U=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 h1:N3bU/SQDCDyD6R528GJ/PwW9KjYcJA3dgyH+MovAkIM=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13/go.mod h1:KSqppvjFjtoCI+KGd4PELB0qLNxdJHRGqRI09mB6pQA=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
|
||||
google.golang.org/grpc v1.56.1 h1:z0dNfjIl0VpaZ9iSVjA6daGatAYwPGstTjt5vkRMFkQ=
|
||||
google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
|
||||
google.golang.org/grpc v1.56.2 h1:fVRFRnXvU+x6C4IlHZewvJOVHoOv1TUuQyoRsYnB4bI=
|
||||
google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
|
||||
google.golang.org/grpc v1.58.2 h1:SXUpjxeVF3FKrTYQI4f4KvbGD5u2xccdYdurwowix5I=
|
||||
google.golang.org/grpc v1.58.2/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@@ -295,12 +238,11 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -342,6 +342,10 @@
|
||||
# - "field -> level" to match any value, e.g. "time_taken_ms -> debug"
|
||||
# Warning: Using log-level-overrides has a performance penalty. Only use it for temporary debugging.
|
||||
#
|
||||
# Check your permissions:
|
||||
# If you are running ntfy with systemd, make sure this log file is owned by the
|
||||
# ntfy user and group by running: chown ntfy.ntfy <filename>.
|
||||
#
|
||||
# Example (good for production):
|
||||
# log-level: info
|
||||
# log-format: json
|
||||
|
||||
@@ -329,6 +329,27 @@ func TestServer_PublishPriority(t *testing.T) {
|
||||
require.Equal(t, 40007, toHTTPError(t, response.Body.String()).Code)
|
||||
}
|
||||
|
||||
func TestServer_PublishPriority_SpecialHTTPHeader(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
response := request(t, s, "POST", "/mytopic", "test", map[string]string{
|
||||
"Priority": "u=4",
|
||||
"X-Priority": "5",
|
||||
})
|
||||
require.Equal(t, 5, toMessage(t, response.Body.String()).Priority)
|
||||
|
||||
response = request(t, s, "POST", "/mytopic?priority=4", "test", map[string]string{
|
||||
"Priority": "u=9",
|
||||
})
|
||||
require.Equal(t, 4, toMessage(t, response.Body.String()).Priority)
|
||||
|
||||
response = request(t, s, "POST", "/mytopic", "test", map[string]string{
|
||||
"p": "2",
|
||||
"priority": "u=9, i",
|
||||
})
|
||||
require.Equal(t, 2, toMessage(t, response.Body.String()).Priority)
|
||||
}
|
||||
|
||||
func TestServer_PublishGETOnlyOneTopic(t *testing.T) {
|
||||
// This tests a bug that allowed publishing topics with a comma in the name (no ticket)
|
||||
|
||||
|
||||
@@ -8,10 +8,14 @@ import (
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var mimeDecoder mime.WordDecoder
|
||||
var (
|
||||
mimeDecoder mime.WordDecoder
|
||||
priorityHeaderIgnoreRegex = regexp.MustCompile(`^u=\d,\s*(i|\d)$|^u=\d$`)
|
||||
)
|
||||
|
||||
func readBoolParam(r *http.Request, defaultValue bool, names ...string) bool {
|
||||
value := strings.ToLower(readParam(r, names...))
|
||||
@@ -50,9 +54,9 @@ func readParam(r *http.Request, names ...string) string {
|
||||
|
||||
func readHeaderParam(r *http.Request, names ...string) string {
|
||||
for _, name := range names {
|
||||
value := maybeDecodeHeader(r.Header.Get(name))
|
||||
value := strings.TrimSpace(maybeDecodeHeader(name, r.Header.Get(name)))
|
||||
if value != "" {
|
||||
return strings.TrimSpace(value)
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
@@ -126,10 +130,26 @@ func fromContext[T any](r *http.Request, key contextKey) (T, error) {
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func maybeDecodeHeader(header string) string {
|
||||
decoded, err := mimeDecoder.DecodeHeader(header)
|
||||
// maybeDecodeHeader decodes the given header value if it is MIME encoded, e.g. "=?utf-8?q?Hello_World?=",
|
||||
// or returns the original header value if it is not MIME encoded. It also calls maybeIgnoreSpecialHeader
|
||||
// to ignore new HTTP "Priority" header.
|
||||
func maybeDecodeHeader(name, value string) string {
|
||||
decoded, err := mimeDecoder.DecodeHeader(value)
|
||||
if err != nil {
|
||||
return header
|
||||
return maybeIgnoreSpecialHeader(name, value)
|
||||
}
|
||||
return decoded
|
||||
return maybeIgnoreSpecialHeader(name, decoded)
|
||||
}
|
||||
|
||||
// maybeIgnoreSpecialHeader ignores new HTTP "Priority" header (see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-priority)
|
||||
//
|
||||
// Cloudflare (and potentially other providers) add this to requests when forwarding to the backend (ntfy),
|
||||
// so we just ignore it. If the "Priority" header is set to "u=*, i" or "u=*" (by Cloudflare), the header will be ignored.
|
||||
// Returning an empty string will allow the rest of the logic to continue searching for another header (x-priority, prio, p),
|
||||
// or in the Query parameters.
|
||||
func maybeIgnoreSpecialHeader(name, value string) string {
|
||||
if strings.ToLower(name) == "priority" && priorityHeaderIgnoreRegex.MatchString(strings.TrimSpace(value)) {
|
||||
return ""
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -75,3 +75,16 @@ Accept: */*
|
||||
(peeked bytes not UTF-8, peek limit of 4096 bytes reached, hex: ` + fmt.Sprintf("%x", body[:4096]) + ` ...)`
|
||||
require.Equal(t, expected, renderHTTPRequest(r))
|
||||
}
|
||||
|
||||
func TestMaybeIgnoreSpecialHeader(t *testing.T) {
|
||||
require.Empty(t, maybeIgnoreSpecialHeader("priority", "u=1"))
|
||||
require.Empty(t, maybeIgnoreSpecialHeader("Priority", "u=1"))
|
||||
require.Empty(t, maybeIgnoreSpecialHeader("Priority", "u=1, i"))
|
||||
}
|
||||
|
||||
func TestMaybeDecodeHeaders(t *testing.T) {
|
||||
r, _ := http.NewRequest("GET", "http://ntfy.sh/mytopic/json?since=all", nil)
|
||||
r.Header.Set("Priority", "u=1") // Cloudflare priority header
|
||||
r.Header.Set("X-Priority", "5") // ntfy priority header
|
||||
require.Equal(t, "5", readHeaderParam(r, "x-priority", "priority", "p"))
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ const (
|
||||
SELECT read, write
|
||||
FROM user_access a
|
||||
JOIN user u ON u.id = a.user_id
|
||||
WHERE (u.user = ? OR u.user = ?) AND ? LIKE a.topic
|
||||
WHERE (u.user = ? OR u.user = ?) AND ? LIKE a.topic ESCAPE '\'
|
||||
ORDER BY u.user DESC
|
||||
`
|
||||
|
||||
@@ -235,7 +235,7 @@ const (
|
||||
selectOtherAccessCountQuery = `
|
||||
SELECT COUNT(*)
|
||||
FROM user_access
|
||||
WHERE (topic = ? OR ? LIKE topic)
|
||||
WHERE (topic = ? OR ? LIKE topic ESCAPE '\')
|
||||
AND (owner_user_id IS NULL OR owner_user_id != (SELECT id FROM user WHERE user = ?))
|
||||
`
|
||||
deleteAllAccessQuery = `DELETE FROM user_access`
|
||||
@@ -262,7 +262,8 @@ const (
|
||||
deleteExpiredTokensQuery = `DELETE FROM user_token WHERE expires > 0 AND expires < ?`
|
||||
deleteExcessTokensQuery = `
|
||||
DELETE FROM user_token
|
||||
WHERE (user_id, token) NOT IN (
|
||||
WHERE user_id = ?
|
||||
AND (user_id, token) NOT IN (
|
||||
SELECT user_id, token
|
||||
FROM user_token
|
||||
WHERE user_id = ?
|
||||
@@ -311,7 +312,7 @@ const (
|
||||
|
||||
// Schema management queries
|
||||
const (
|
||||
currentSchemaVersion = 4
|
||||
currentSchemaVersion = 5
|
||||
insertSchemaVersion = `INSERT INTO schemaVersion VALUES (1, ?)`
|
||||
updateSchemaVersion = `UPDATE schemaVersion SET version = ? WHERE id = 1`
|
||||
selectSchemaVersionQuery = `SELECT version FROM schemaVersion WHERE id = 1`
|
||||
@@ -421,6 +422,11 @@ const (
|
||||
FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE
|
||||
);
|
||||
`
|
||||
|
||||
// 4 -> 5
|
||||
migrate4To5UpdateQueries = `
|
||||
UPDATE user_access SET topic = REPLACE(topic, '_', '\_');
|
||||
`
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -428,6 +434,7 @@ var (
|
||||
1: migrateFrom1,
|
||||
2: migrateFrom2,
|
||||
3: migrateFrom3,
|
||||
4: migrateFrom4,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -534,7 +541,7 @@ func (a *Manager) CreateToken(userID, label string, expires time.Time, origin ne
|
||||
if tokenCount >= tokenMaxCount {
|
||||
// This pruning logic is done in two queries for efficiency. The SELECT above is a lookup
|
||||
// on two indices, whereas the query below is a full table scan.
|
||||
if _, err := tx.Exec(deleteExcessTokensQuery, userID, tokenMaxCount); err != nil {
|
||||
if _, err := tx.Exec(deleteExcessTokensQuery, userID, userID, tokenMaxCount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -1122,7 +1129,7 @@ func (a *Manager) Reservations(username string) ([]Reservation, error) {
|
||||
return nil, err
|
||||
}
|
||||
reservations = append(reservations, Reservation{
|
||||
Topic: topic,
|
||||
Topic: unescapeUnderscore(topic),
|
||||
Owner: NewPermission(ownerRead, ownerWrite),
|
||||
Everyone: NewPermission(everyoneRead.Bool, everyoneWrite.Bool), // false if null
|
||||
})
|
||||
@@ -1132,7 +1139,7 @@ func (a *Manager) Reservations(username string) ([]Reservation, error) {
|
||||
|
||||
// HasReservation returns true if the given topic access is owned by the user
|
||||
func (a *Manager) HasReservation(username, topic string) (bool, error) {
|
||||
rows, err := a.db.Query(selectUserHasReservationQuery, username, topic)
|
||||
rows, err := a.db.Query(selectUserHasReservationQuery, username, escapeUnderscore(topic))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -1167,7 +1174,7 @@ func (a *Manager) ReservationsCount(username string) (int64, error) {
|
||||
// ReservationOwner returns user ID of the user that owns this topic, or an
|
||||
// empty string if it's not owned by anyone
|
||||
func (a *Manager) ReservationOwner(topic string) (string, error) {
|
||||
rows, err := a.db.Query(selectUserReservationsOwnerQuery, topic)
|
||||
rows, err := a.db.Query(selectUserReservationsOwnerQuery, escapeUnderscore(topic))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -1262,7 +1269,7 @@ func (a *Manager) AllowReservation(username string, topic string) error {
|
||||
if (!AllowedUsername(username) && username != Everyone) || !AllowedTopic(topic) {
|
||||
return ErrInvalidArgument
|
||||
}
|
||||
rows, err := a.db.Query(selectOtherAccessCountQuery, topic, topic, username)
|
||||
rows, err := a.db.Query(selectOtherAccessCountQuery, escapeUnderscore(topic), escapeUnderscore(topic), username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1327,10 +1334,10 @@ func (a *Manager) AddReservation(username string, topic string, everyone Permiss
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
if _, err := tx.Exec(upsertUserAccessQuery, username, topic, true, true, username, username); err != nil {
|
||||
if _, err := tx.Exec(upsertUserAccessQuery, username, escapeUnderscore(topic), true, true, username, username); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(upsertUserAccessQuery, Everyone, topic, everyone.IsRead(), everyone.IsWrite(), username, username); err != nil {
|
||||
if _, err := tx.Exec(upsertUserAccessQuery, Everyone, escapeUnderscore(topic), everyone.IsRead(), everyone.IsWrite(), username, username); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
@@ -1353,10 +1360,10 @@ func (a *Manager) RemoveReservations(username string, topics ...string) error {
|
||||
}
|
||||
defer tx.Rollback()
|
||||
for _, topic := range topics {
|
||||
if _, err := tx.Exec(deleteTopicAccessQuery, username, username, topic); err != nil {
|
||||
if _, err := tx.Exec(deleteTopicAccessQuery, username, username, escapeUnderscore(topic)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(deleteTopicAccessQuery, Everyone, Everyone, topic); err != nil {
|
||||
if _, err := tx.Exec(deleteTopicAccessQuery, Everyone, Everyone, escapeUnderscore(topic)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -1483,12 +1490,24 @@ func (a *Manager) Close() error {
|
||||
return a.db.Close()
|
||||
}
|
||||
|
||||
// toSQLWildcard converts a wildcard string to a SQL wildcard string. It only allows '*' as wildcards,
|
||||
// and escapes '_', assuming '\' as escape character.
|
||||
func toSQLWildcard(s string) string {
|
||||
return strings.ReplaceAll(s, "*", "%")
|
||||
return escapeUnderscore(strings.ReplaceAll(s, "*", "%"))
|
||||
}
|
||||
|
||||
// fromSQLWildcard converts a SQL wildcard string to a wildcard string. It converts '%' to '*',
|
||||
// and removes the '\_' escape character.
|
||||
func fromSQLWildcard(s string) string {
|
||||
return strings.ReplaceAll(s, "%", "*")
|
||||
return strings.ReplaceAll(unescapeUnderscore(s), "%", "*")
|
||||
}
|
||||
|
||||
func escapeUnderscore(s string) string {
|
||||
return strings.ReplaceAll(s, "_", "\\_")
|
||||
}
|
||||
|
||||
func unescapeUnderscore(s string) string {
|
||||
return strings.ReplaceAll(s, "\\_", "_")
|
||||
}
|
||||
|
||||
func runStartupQueries(db *sql.DB, startupQueries string) error {
|
||||
@@ -1626,6 +1645,22 @@ func migrateFrom3(db *sql.DB) error {
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func migrateFrom4(db *sql.DB) error {
|
||||
log.Tag(tag).Info("Migrating user database schema: from 4 to 5")
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
if _, err := tx.Exec(migrate4To5UpdateQueries); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(updateSchemaVersion, 5); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func nullString(s string) sql.NullString {
|
||||
if s == "" {
|
||||
return sql.NullString{}
|
||||
|
||||
@@ -330,7 +330,7 @@ func TestManager_Reservations(t *testing.T) {
|
||||
a := newTestManager(t, PermissionDenyAll)
|
||||
require.Nil(t, a.AddUser("phil", "phil", RoleUser))
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
|
||||
require.Nil(t, a.AddReservation("ben", "ztopic", PermissionDenyAll))
|
||||
require.Nil(t, a.AddReservation("ben", "ztopic_", PermissionDenyAll))
|
||||
require.Nil(t, a.AddReservation("ben", "readme", PermissionRead))
|
||||
require.Nil(t, a.AllowAccess("ben", "something-else", PermissionRead))
|
||||
|
||||
@@ -343,7 +343,7 @@ func TestManager_Reservations(t *testing.T) {
|
||||
Everyone: PermissionRead,
|
||||
}, reservations[0])
|
||||
require.Equal(t, Reservation{
|
||||
Topic: "ztopic",
|
||||
Topic: "ztopic_",
|
||||
Owner: PermissionReadWrite,
|
||||
Everyone: PermissionDenyAll,
|
||||
}, reservations[1])
|
||||
@@ -352,6 +352,14 @@ func TestManager_Reservations(t *testing.T) {
|
||||
require.Nil(t, err)
|
||||
require.True(t, b)
|
||||
|
||||
b, err = a.HasReservation("ben", "ztopic_")
|
||||
require.Nil(t, err)
|
||||
require.True(t, b)
|
||||
|
||||
b, err = a.HasReservation("ben", "ztopicX") // _ != X (used to be a SQL wildcard issue)
|
||||
require.Nil(t, err)
|
||||
require.False(t, b)
|
||||
|
||||
b, err = a.HasReservation("notben", "readme")
|
||||
require.Nil(t, err)
|
||||
require.False(t, b)
|
||||
@@ -371,11 +379,17 @@ func TestManager_Reservations(t *testing.T) {
|
||||
err = a.AllowReservation("phil", "readme")
|
||||
require.Equal(t, errTopicOwnedByOthers, err)
|
||||
|
||||
err = a.AllowReservation("phil", "ztopic_")
|
||||
require.Equal(t, errTopicOwnedByOthers, err)
|
||||
|
||||
err = a.AllowReservation("phil", "ztopicX")
|
||||
require.Nil(t, err)
|
||||
|
||||
err = a.AllowReservation("phil", "not-reserved")
|
||||
require.Nil(t, err)
|
||||
|
||||
// Now remove them again
|
||||
require.Nil(t, a.RemoveReservations("ben", "ztopic", "readme"))
|
||||
require.Nil(t, a.RemoveReservations("ben", "ztopic_", "readme"))
|
||||
|
||||
count, err = a.ReservationsCount("ben")
|
||||
require.Nil(t, err)
|
||||
@@ -580,46 +594,80 @@ func TestManager_Token_Extend(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestManager_Token_MaxCount_AutoDelete(t *testing.T) {
|
||||
// Tests that tokens are automatically deleted when the maximum number of tokens is reached
|
||||
|
||||
a := newTestManager(t, PermissionDenyAll)
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
|
||||
require.Nil(t, a.AddUser("phil", "phil", RoleUser))
|
||||
|
||||
// Try to extend token for user without token
|
||||
u, err := a.User("ben")
|
||||
ben, err := a.User("ben")
|
||||
require.Nil(t, err)
|
||||
|
||||
// Tokens
|
||||
phil, err := a.User("phil")
|
||||
require.Nil(t, err)
|
||||
|
||||
// Create 2 tokens for phil
|
||||
philTokens := make([]string, 0)
|
||||
token, err := a.CreateToken(phil.ID, "", time.Now().Add(72*time.Hour), netip.IPv4Unspecified())
|
||||
require.Nil(t, err)
|
||||
require.NotEmpty(t, token.Value)
|
||||
philTokens = append(philTokens, token.Value)
|
||||
|
||||
token, err = a.CreateToken(phil.ID, "", time.Unix(0, 0), netip.IPv4Unspecified())
|
||||
require.Nil(t, err)
|
||||
require.NotEmpty(t, token.Value)
|
||||
philTokens = append(philTokens, token.Value)
|
||||
|
||||
// Create 22 tokens for ben (only 20 allowed!)
|
||||
baseTime := time.Now().Add(24 * time.Hour)
|
||||
tokens := make([]string, 0)
|
||||
for i := 0; i < 22; i++ {
|
||||
token, err := a.CreateToken(u.ID, "", time.Now().Add(72*time.Hour), netip.IPv4Unspecified())
|
||||
benTokens := make([]string, 0)
|
||||
for i := 0; i < 22; i++ { //
|
||||
token, err := a.CreateToken(ben.ID, "", time.Now().Add(72*time.Hour), netip.IPv4Unspecified())
|
||||
require.Nil(t, err)
|
||||
require.NotEmpty(t, token.Value)
|
||||
tokens = append(tokens, token.Value)
|
||||
benTokens = append(benTokens, token.Value)
|
||||
|
||||
// Manually modify expiry date to avoid sorting issues (this is a hack)
|
||||
_, err = a.db.Exec(`UPDATE user_token SET expires=? WHERE token=?`, baseTime.Add(time.Duration(i)*time.Minute).Unix(), token.Value)
|
||||
require.Nil(t, err)
|
||||
}
|
||||
|
||||
_, err = a.AuthenticateToken(tokens[0])
|
||||
// Ben: The first 2 tokens should have been wiped and should not work anymore!
|
||||
_, err = a.AuthenticateToken(benTokens[0])
|
||||
require.Equal(t, ErrUnauthenticated, err)
|
||||
|
||||
_, err = a.AuthenticateToken(tokens[1])
|
||||
_, err = a.AuthenticateToken(benTokens[1])
|
||||
require.Equal(t, ErrUnauthenticated, err)
|
||||
|
||||
// Ben: The other tokens should still work
|
||||
for i := 2; i < 22; i++ {
|
||||
userWithToken, err := a.AuthenticateToken(tokens[i])
|
||||
require.Nil(t, err, "token[%d]=%s failed", i, tokens[i])
|
||||
userWithToken, err := a.AuthenticateToken(benTokens[i])
|
||||
require.Nil(t, err, "token[%d]=%s failed", i, benTokens[i])
|
||||
require.Equal(t, "ben", userWithToken.Name)
|
||||
require.Equal(t, tokens[i], userWithToken.Token)
|
||||
require.Equal(t, benTokens[i], userWithToken.Token)
|
||||
}
|
||||
|
||||
var count int
|
||||
rows, err := a.db.Query(`SELECT COUNT(*) FROM user_token`)
|
||||
// Phil: All tokens should still work
|
||||
for i := 0; i < 2; i++ {
|
||||
userWithToken, err := a.AuthenticateToken(philTokens[i])
|
||||
require.Nil(t, err, "token[%d]=%s failed", i, philTokens[i])
|
||||
require.Equal(t, "phil", userWithToken.Name)
|
||||
require.Equal(t, philTokens[i], userWithToken.Token)
|
||||
}
|
||||
|
||||
var benCount int
|
||||
rows, err := a.db.Query(`SELECT COUNT(*) FROM user_token WHERE user_id=?`, ben.ID)
|
||||
require.Nil(t, err)
|
||||
require.True(t, rows.Next())
|
||||
require.Nil(t, rows.Scan(&count))
|
||||
require.Equal(t, 20, count)
|
||||
require.Nil(t, rows.Scan(&benCount))
|
||||
require.Equal(t, 20, benCount)
|
||||
|
||||
var philCount int
|
||||
rows, err = a.db.Query(`SELECT COUNT(*) FROM user_token WHERE user_id=?`, phil.ID)
|
||||
require.Nil(t, err)
|
||||
require.True(t, rows.Next())
|
||||
require.Nil(t, rows.Scan(&philCount))
|
||||
require.Equal(t, 2, philCount)
|
||||
}
|
||||
|
||||
func TestManager_EnqueueStats_ResetStats(t *testing.T) {
|
||||
@@ -944,7 +992,44 @@ func TestUser_PhoneNumberAdd_Multiple_Users_Same_Number(t *testing.T) {
|
||||
require.Nil(t, a.AddPhoneNumber(ben.ID, "+1234567890"))
|
||||
}
|
||||
|
||||
func TestSqliteCache_Migration_From1(t *testing.T) {
|
||||
func TestManager_Topic_Wildcard_With_Asterisk_Underscore(t *testing.T) {
|
||||
f := filepath.Join(t.TempDir(), "user.db")
|
||||
a := newTestManagerFromFile(t, f, "", PermissionDenyAll, DefaultUserPasswordBcryptCost, DefaultUserStatsQueueWriterInterval)
|
||||
require.Nil(t, a.AllowAccess(Everyone, "*_", PermissionRead))
|
||||
require.Nil(t, a.AllowAccess(Everyone, "__*_", PermissionRead))
|
||||
require.Nil(t, a.Authorize(nil, "allowed_", PermissionRead))
|
||||
require.Nil(t, a.Authorize(nil, "__allowed_", PermissionRead))
|
||||
require.Nil(t, a.Authorize(nil, "_allowed_", PermissionRead)) // The "%" in "%\_" matches the first "_"
|
||||
require.Equal(t, ErrUnauthorized, a.Authorize(nil, "notallowed", PermissionRead))
|
||||
require.Equal(t, ErrUnauthorized, a.Authorize(nil, "_notallowed", PermissionRead))
|
||||
require.Equal(t, ErrUnauthorized, a.Authorize(nil, "__notallowed", PermissionRead))
|
||||
}
|
||||
|
||||
func TestManager_Topic_Wildcard_With_Underscore(t *testing.T) {
|
||||
f := filepath.Join(t.TempDir(), "user.db")
|
||||
a := newTestManagerFromFile(t, f, "", PermissionDenyAll, DefaultUserPasswordBcryptCost, DefaultUserStatsQueueWriterInterval)
|
||||
require.Nil(t, a.AllowAccess(Everyone, "mytopic_", PermissionReadWrite))
|
||||
require.Nil(t, a.Authorize(nil, "mytopic_", PermissionRead))
|
||||
require.Nil(t, a.Authorize(nil, "mytopic_", PermissionWrite))
|
||||
require.Equal(t, ErrUnauthorized, a.Authorize(nil, "mytopicX", PermissionRead))
|
||||
require.Equal(t, ErrUnauthorized, a.Authorize(nil, "mytopicX", PermissionWrite))
|
||||
}
|
||||
|
||||
func TestToFromSQLWildcard(t *testing.T) {
|
||||
require.Equal(t, "up%", toSQLWildcard("up*"))
|
||||
require.Equal(t, "up\\_%", toSQLWildcard("up_*"))
|
||||
require.Equal(t, "foo", toSQLWildcard("foo"))
|
||||
|
||||
require.Equal(t, "up*", fromSQLWildcard("up%"))
|
||||
require.Equal(t, "up_*", fromSQLWildcard("up\\_%"))
|
||||
require.Equal(t, "foo", fromSQLWildcard("foo"))
|
||||
|
||||
require.Equal(t, "up*", fromSQLWildcard(toSQLWildcard("up*")))
|
||||
require.Equal(t, "up_*", fromSQLWildcard(toSQLWildcard("up_*")))
|
||||
require.Equal(t, "foo", fromSQLWildcard(toSQLWildcard("foo")))
|
||||
}
|
||||
|
||||
func TestMigrationFrom1(t *testing.T) {
|
||||
filename := filepath.Join(t.TempDir(), "user.db")
|
||||
db, err := sql.Open("sqlite3", filename)
|
||||
require.Nil(t, err)
|
||||
@@ -1029,6 +1114,152 @@ func TestSqliteCache_Migration_From1(t *testing.T) {
|
||||
require.Equal(t, PermissionRead, everyoneGrants[0].Allow)
|
||||
}
|
||||
|
||||
func TestMigrationFrom4(t *testing.T) {
|
||||
filename := filepath.Join(t.TempDir(), "user.db")
|
||||
db, err := sql.Open("sqlite3", filename)
|
||||
require.Nil(t, err)
|
||||
|
||||
// Create "version 4" schema
|
||||
_, err = db.Exec(`
|
||||
BEGIN;
|
||||
CREATE TABLE IF NOT EXISTS tier (
|
||||
id TEXT PRIMARY KEY,
|
||||
code TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
messages_limit INT NOT NULL,
|
||||
messages_expiry_duration INT NOT NULL,
|
||||
emails_limit INT NOT NULL,
|
||||
calls_limit INT NOT NULL,
|
||||
reservations_limit INT NOT NULL,
|
||||
attachment_file_size_limit INT NOT NULL,
|
||||
attachment_total_size_limit INT NOT NULL,
|
||||
attachment_expiry_duration INT NOT NULL,
|
||||
attachment_bandwidth_limit INT NOT NULL,
|
||||
stripe_monthly_price_id TEXT,
|
||||
stripe_yearly_price_id TEXT
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_tier_code ON tier (code);
|
||||
CREATE UNIQUE INDEX idx_tier_stripe_monthly_price_id ON tier (stripe_monthly_price_id);
|
||||
CREATE UNIQUE INDEX idx_tier_stripe_yearly_price_id ON tier (stripe_yearly_price_id);
|
||||
CREATE TABLE IF NOT EXISTS user (
|
||||
id TEXT PRIMARY KEY,
|
||||
tier_id TEXT,
|
||||
user TEXT NOT NULL,
|
||||
pass TEXT NOT NULL,
|
||||
role TEXT CHECK (role IN ('anonymous', 'admin', 'user')) NOT NULL,
|
||||
prefs JSON NOT NULL DEFAULT '{}',
|
||||
sync_topic TEXT NOT NULL,
|
||||
stats_messages INT NOT NULL DEFAULT (0),
|
||||
stats_emails INT NOT NULL DEFAULT (0),
|
||||
stats_calls INT NOT NULL DEFAULT (0),
|
||||
stripe_customer_id TEXT,
|
||||
stripe_subscription_id TEXT,
|
||||
stripe_subscription_status TEXT,
|
||||
stripe_subscription_interval TEXT,
|
||||
stripe_subscription_paid_until INT,
|
||||
stripe_subscription_cancel_at INT,
|
||||
created INT NOT NULL,
|
||||
deleted INT,
|
||||
FOREIGN KEY (tier_id) REFERENCES tier (id)
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_user ON user (user);
|
||||
CREATE UNIQUE INDEX idx_user_stripe_customer_id ON user (stripe_customer_id);
|
||||
CREATE UNIQUE INDEX idx_user_stripe_subscription_id ON user (stripe_subscription_id);
|
||||
CREATE TABLE IF NOT EXISTS user_access (
|
||||
user_id TEXT NOT NULL,
|
||||
topic TEXT NOT NULL,
|
||||
read INT NOT NULL,
|
||||
write INT NOT NULL,
|
||||
owner_user_id INT,
|
||||
PRIMARY KEY (user_id, topic),
|
||||
FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (owner_user_id) REFERENCES user (id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS user_token (
|
||||
user_id TEXT NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
label TEXT NOT NULL,
|
||||
last_access INT NOT NULL,
|
||||
last_origin TEXT NOT NULL,
|
||||
expires INT NOT NULL,
|
||||
PRIMARY KEY (user_id, token),
|
||||
FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS user_phone (
|
||||
user_id TEXT NOT NULL,
|
||||
phone_number TEXT NOT NULL,
|
||||
PRIMARY KEY (user_id, phone_number),
|
||||
FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS schemaVersion (
|
||||
id INT PRIMARY KEY,
|
||||
version INT NOT NULL
|
||||
);
|
||||
INSERT INTO user (id, user, pass, role, sync_topic, created)
|
||||
VALUES ('u_everyone', '*', '', 'anonymous', '', UNIXEPOCH())
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
INSERT INTO schemaVersion (id, version) VALUES (1, 4);
|
||||
COMMIT;
|
||||
`)
|
||||
require.Nil(t, err)
|
||||
|
||||
// Insert a few ACL entries
|
||||
_, err = db.Exec(`
|
||||
BEGIN;
|
||||
INSERT INTO user_access (user_id, topic, read, write) values ('u_everyone', 'mytopic_', 1, 1);
|
||||
INSERT INTO user_access (user_id, topic, read, write) values ('u_everyone', 'up%', 1, 1);
|
||||
INSERT INTO user_access (user_id, topic, read, write) values ('u_everyone', 'down_%', 1, 1);
|
||||
COMMIT;
|
||||
`)
|
||||
require.Nil(t, err)
|
||||
|
||||
// Create manager to trigger migration
|
||||
a := newTestManagerFromFile(t, filename, "", PermissionDenyAll, bcrypt.MinCost, DefaultUserStatsQueueWriterInterval)
|
||||
checkSchemaVersion(t, a.db)
|
||||
|
||||
// Add another
|
||||
require.Nil(t, a.AllowAccess(Everyone, "left_*", PermissionReadWrite))
|
||||
|
||||
// Check "external view" of grants
|
||||
everyoneGrants, err := a.Grants(Everyone)
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, 4, len(everyoneGrants))
|
||||
require.Equal(t, "down_*", everyoneGrants[0].TopicPattern)
|
||||
require.Equal(t, "left_*", everyoneGrants[1].TopicPattern)
|
||||
require.Equal(t, "mytopic_", everyoneGrants[2].TopicPattern)
|
||||
require.Equal(t, "up*", everyoneGrants[3].TopicPattern)
|
||||
|
||||
// Check they are stored correctly in the database
|
||||
rows, err := db.Query(`SELECT topic FROM user_access WHERE user_id = 'u_everyone' ORDER BY topic`)
|
||||
require.Nil(t, err)
|
||||
topicPatterns := make([]string, 0)
|
||||
for rows.Next() {
|
||||
var topicPattern string
|
||||
require.Nil(t, rows.Scan(&topicPattern))
|
||||
topicPatterns = append(topicPatterns, topicPattern)
|
||||
}
|
||||
require.Nil(t, rows.Close())
|
||||
require.Equal(t, 4, len(topicPatterns))
|
||||
require.Equal(t, "down\\_%", topicPatterns[0])
|
||||
require.Equal(t, "left\\_%", topicPatterns[1])
|
||||
require.Equal(t, "mytopic\\_", topicPatterns[2])
|
||||
require.Equal(t, "up%", topicPatterns[3])
|
||||
|
||||
// Check that ACL works as excepted
|
||||
require.Nil(t, a.Authorize(nil, "down_123", PermissionRead))
|
||||
require.Equal(t, ErrUnauthorized, a.Authorize(nil, "downX123", PermissionRead))
|
||||
|
||||
require.Nil(t, a.Authorize(nil, "left_abc", PermissionRead))
|
||||
require.Equal(t, ErrUnauthorized, a.Authorize(nil, "leftX123", PermissionRead))
|
||||
|
||||
require.Nil(t, a.Authorize(nil, "mytopic_", PermissionRead))
|
||||
require.Equal(t, ErrUnauthorized, a.Authorize(nil, "mytopicX", PermissionRead))
|
||||
|
||||
require.Nil(t, a.Authorize(nil, "up123", PermissionRead))
|
||||
require.Nil(t, a.Authorize(nil, "up", PermissionRead)) // % matches 0 or more characters
|
||||
}
|
||||
|
||||
func checkSchemaVersion(t *testing.T, db *sql.DB) {
|
||||
rows, err := db.Query(`SELECT version FROM schemaVersion`)
|
||||
require.Nil(t, err)
|
||||
|
||||
@@ -161,11 +161,6 @@ func ParsePriority(priority string) (int, error) {
|
||||
case "5", "max", "urgent":
|
||||
return 5, nil
|
||||
default:
|
||||
// Ignore new HTTP Priority header (see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-priority)
|
||||
// Cloudflare adds this to requests when forwarding to the backend (ntfy), so we just ignore it.
|
||||
if strings.HasPrefix(p, "u=") {
|
||||
return 3, nil
|
||||
}
|
||||
return 0, errInvalidPriority
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,15 +87,6 @@ func TestParsePriority_Invalid(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePriority_HTTPSpecPriority(t *testing.T) {
|
||||
priorities := []string{"u=1", "u=3", "u=7, i"} // see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-priority
|
||||
for _, priority := range priorities {
|
||||
actual, err := ParsePriority(priority)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 3, actual) // Always expect 3!
|
||||
}
|
||||
}
|
||||
|
||||
func TestPriorityString(t *testing.T) {
|
||||
priorities := []int{0, 1, 2, 3, 4, 5}
|
||||
expected := []string{"default", "min", "low", "default", "high", "max"}
|
||||
|
||||
1850
web/package-lock.json
generated
1850
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -152,7 +152,7 @@
|
||||
"publish_dialog_chip_delay_label": "تأخير التسليم",
|
||||
"subscribe_dialog_login_description": "هذا الموضوع محمي بكلمة مرور. الرجاء إدخال اسم المستخدم وكلمة المرور للاشتراك.",
|
||||
"subscribe_dialog_subscribe_button_cancel": "إلغاء",
|
||||
"common_back": "العودة",
|
||||
"common_back": "الرجوع",
|
||||
"prefs_notifications_sound_play": "تشغيل الصوت المحدد",
|
||||
"prefs_notifications_min_priority_title": "أولوية دنيا",
|
||||
"prefs_notifications_min_priority_max_only": "الأولوية القصوى فقط",
|
||||
|
||||
@@ -310,7 +310,7 @@
|
||||
"prefs_reservations_delete_button": "Zugriff auf Thema zurücksetzen",
|
||||
"prefs_reservations_table": "Übersicht reservierter Themen",
|
||||
"prefs_reservations_table_topic_header": "Thema",
|
||||
"prefs_reservations_table_everyone_deny_all": "Nur kann veröffentlichen und lesen",
|
||||
"prefs_reservations_table_everyone_deny_all": "Nur ich kann veröffentlichen und lesen",
|
||||
"prefs_reservations_table_everyone_write_only": "Ich kann veröffentlichen und lesen, jeder kann veröffentlichen",
|
||||
"prefs_reservations_table_not_subscribed": "Nicht abonniert",
|
||||
"prefs_reservations_table_click_to_subscribe": "Klicken um zu abonnieren",
|
||||
|
||||
@@ -272,7 +272,7 @@
|
||||
"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_upgrade_dialog_title": "Changer le tarif du compte",
|
||||
"account_upgrade_dialog_proration_info": "<strong>Facturation</strong> : Lors d'un changement entre un plan payant et un autre, la différence de prix sera créditée ou remboursée sur la prochaine facture. Vous ne recevrez pas d'autre facture avant la fin de la prochaine période de facturation.",
|
||||
"account_upgrade_dialog_proration_info": "<strong>Facturation</strong> : Lors d'un changement 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_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_messages_other": "{{messages}} messages journaliers",
|
||||
@@ -368,8 +368,17 @@
|
||||
"account_basics_phone_numbers_dialog_code_placeholder": "Ex : 123456",
|
||||
"account_basics_phone_numbers_dialog_check_verification_button": "Code de confirmarion",
|
||||
"account_basics_phone_numbers_dialog_channel_sms": "SMS",
|
||||
"account_basics_phone_numbers_dialog_channel_call": "Appel",
|
||||
"account_basics_phone_numbers_dialog_channel_call": "Appeler",
|
||||
"account_usage_calls_none": "Aucun appels téléphoniques ne peut être fait avec ce compte",
|
||||
"publish_dialog_call_reset": "Supprimer les appels téléphoniques",
|
||||
"publish_dialog_chip_call_label": "Appel téléphonique"
|
||||
"publish_dialog_chip_call_label": "Appel téléphonique",
|
||||
"account_upgrade_dialog_tier_features_messages_one": "{{messages}} message journalier",
|
||||
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} mail journalier",
|
||||
"account_upgrade_dialog_tier_features_calls_other": "{{calls}} appels journaliers",
|
||||
"account_upgrade_dialog_tier_features_no_calls": "Aucun appel",
|
||||
"publish_dialog_call_item": "Appeler le numéro {{number}}",
|
||||
"publish_dialog_chip_call_no_verified_numbers_tooltip": "Aucun numéro de téléphone vérifié",
|
||||
"account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} sujet réservé",
|
||||
"account_upgrade_dialog_tier_features_calls_one": "{{calls}} appels journaliers",
|
||||
"account_usage_calls_title": "Appels téléphoniques passés"
|
||||
}
|
||||
|
||||
@@ -1 +1,164 @@
|
||||
{}
|
||||
{
|
||||
"common_cancel": "Cancelar",
|
||||
"common_save": "Gardar",
|
||||
"common_add": "Engadir",
|
||||
"signup_disabled": "O rexistro está desactivado",
|
||||
"signup_error_username_taken": "O identificador {{username}} xa está collido",
|
||||
"login_title": "Accede á túa conta ntfy",
|
||||
"action_bar_send_test_notification": "Enviar notificación de proba",
|
||||
"action_bar_clear_notifications": "Limpar todas as notificacións",
|
||||
"action_bar_unsubscribe": "Retirar subscrición",
|
||||
"action_bar_profile_settings": "Axustes",
|
||||
"message_bar_type_message": "Escribe aquí a mensaxe",
|
||||
"notifications_copied_to_clipboard": "Copiada ao portapapeis",
|
||||
"notifications_attachment_image": "Imaxe anexa",
|
||||
"notifications_attachment_copy_url_title": "Copiar URL do anexo ao portapapeis",
|
||||
"notifications_attachment_copy_url_button": "Copiar URL",
|
||||
"notifications_attachment_open_title": "Ir a {{url}}",
|
||||
"notifications_attachment_file_audio": "ficheiro de audio",
|
||||
"notifications_attachment_file_app": "ficheiro de app Android",
|
||||
"notifications_attachment_file_document": "outro documento",
|
||||
"notifications_click_copy_url_title": "Copiar URL da ligazón ao portapapeis",
|
||||
"notifications_click_copy_url_button": "Copiar ligazón",
|
||||
"notifications_actions_open_url_title": "Ir a {{url}}",
|
||||
"notifications_none_for_topic_description": "Para enviar notificacións a este tema, simplemente usa PUT ou POST co URL do tema.",
|
||||
"notifications_no_subscriptions_description": "Preme en \"{{linktext}} para crear ou subscribirte a un tema. Após, podes enviar mensaxes vía PUT ou POST e recibirás aquí as notificacións.",
|
||||
"display_name_dialog_description": "Establecer un nome alternativo para o tema que será mostrado na lista de subscrición. Isto axudará a identificar os temas que teñan nomes complicados.",
|
||||
"publish_dialog_tags_label": "Etiquetas",
|
||||
"publish_dialog_tags_placeholder": "Lista de etiquetas separadas por vírgulas, ex. aviso, tarefa1",
|
||||
"publish_dialog_priority_label": "Prioridade",
|
||||
"publish_dialog_click_label": "URL a premer",
|
||||
"publish_dialog_click_placeholder": "URL que se abre ao premer na notificación",
|
||||
"publish_dialog_click_reset": "Desbotar o URL a premer",
|
||||
"common_back": "Atrás",
|
||||
"common_copy_to_clipboard": "Copiar ao portapapeis",
|
||||
"signup_title": "Crear unha conta ntfy",
|
||||
"signup_form_username": "Identificador",
|
||||
"signup_form_password": "Contrasinal",
|
||||
"signup_form_confirm_password": "Confirmar contrasinal",
|
||||
"signup_form_button_submit": "Crear conta",
|
||||
"login_form_button_submit": "Acceder",
|
||||
"login_link_signup": "Crear conta",
|
||||
"login_disabled": "O acceso está desactivado",
|
||||
"action_bar_show_menu": "Mostrar menú",
|
||||
"action_bar_toggle_mute": "Acalar/Reactivar as notificacións",
|
||||
"message_bar_error_publishing": "Erro ao publicar a notificación",
|
||||
"message_bar_publish": "Publicar mensaxe",
|
||||
"nav_topics_title": "Temas subscritos",
|
||||
"nav_button_documentation": "Documentación",
|
||||
"nav_button_publish_message": "Publicar notificación",
|
||||
"nav_button_subscribe": "Subscribirse ao tema",
|
||||
"nav_button_muted": "Notificacións acaladas",
|
||||
"nav_button_connecting": "conectando",
|
||||
"nav_upgrade_banner_label": "Mellorar a ntfy Pro",
|
||||
"alert_not_supported_description": "O teu navegador non ten soporte para notificacións.",
|
||||
"notifications_priority_x": "Prioridade {{priority}}",
|
||||
"notifications_attachment_link_expires": "a ligazón caduca o {{date}}",
|
||||
"notifications_attachment_link_expired": "a ligazón de descarga caducou",
|
||||
"notifications_attachment_file_image": "ficheiro de imaxe",
|
||||
"notifications_attachment_file_video": "ficheiro de vídeo",
|
||||
"notifications_actions_not_supported": "Acción non soportada na aplicación web",
|
||||
"notifications_actions_http_request_title": "Enviar HTTP {{method}} a {{url}}",
|
||||
"notifications_none_for_topic_title": "Aínda non recibiches ningunha notificación para este tema.",
|
||||
"reserve_dialog_checkbox_label": "Reservar tema e configurar acceso",
|
||||
"notifications_loading": "Cargando notificacións…",
|
||||
"publish_dialog_base_url_placeholder": "URL de servizo, ex. https://exemplo.com",
|
||||
"publish_dialog_topic_label": "Nome do tema",
|
||||
"publish_dialog_topic_placeholder": "Nome do tema, ex. alertas_equipo",
|
||||
"publish_dialog_topic_reset": "Restablecer tema",
|
||||
"publish_dialog_title_label": "Título",
|
||||
"publish_dialog_title_placeholder": "Título das notificacións, ex. Alerta de reunión",
|
||||
"publish_dialog_message_label": "Mensaxe",
|
||||
"publish_dialog_message_placeholder": "Escribe aquí a mensaxe",
|
||||
"publish_dialog_email_label": "Correo electrónico",
|
||||
"signup_form_toggle_password_visibility": "Cambiar visibilidade do contrasinal",
|
||||
"signup_already_have_account": "Xa tes unha conta? Accede!",
|
||||
"signup_error_creation_limit_reached": "Acadouse o límite de creación de contas",
|
||||
"action_bar_logo_alt": "logo ntfy",
|
||||
"action_bar_settings": "Axustes",
|
||||
"action_bar_account": "Conta",
|
||||
"action_bar_change_display_name": "Cambiar nome público",
|
||||
"action_bar_reservation_add": "Reservar tema",
|
||||
"action_bar_reservation_edit": "Cambiar a reserva",
|
||||
"action_bar_reservation_delete": "Desbotar a reserva",
|
||||
"action_bar_reservation_limit_reached": "Acadouse o límite",
|
||||
"action_bar_toggle_action_menu": "Abrir/Pechar menú de accións",
|
||||
"action_bar_profile_title": "Perfil",
|
||||
"action_bar_profile_logout": "Pechar sesión",
|
||||
"action_bar_sign_in": "Acceder",
|
||||
"action_bar_sign_up": "Crear conta",
|
||||
"message_bar_show_dialog": "Mostrar diálogo para publicar",
|
||||
"nav_button_all_notifications": "Todas as notificacións",
|
||||
"nav_button_account": "Conta",
|
||||
"nav_button_settings": "Axustes",
|
||||
"nav_upgrade_banner_description": "Reserva temas, máis mensaxes e correos electrónicos así como anexos máis grandes",
|
||||
"alert_grant_title": "As notificacións están desactivadas",
|
||||
"alert_grant_description": "Concede permiso no navegador para mostrar notificacións de escritorio.",
|
||||
"alert_grant_button": "Conceder agora",
|
||||
"alert_not_supported_title": "Non hai soporte para notificacións",
|
||||
"alert_not_supported_context_description": "Só hai soporte para notificacións ao usar HTTPS. Esta é unha limitación da <mdnLink>API de Notificacións</mdnLink>.",
|
||||
"notifications_list": "Lista de notificacións",
|
||||
"notifications_list_item": "Notificación",
|
||||
"notifications_mark_read": "Marcar como lida",
|
||||
"notifications_delete": "Eliminar",
|
||||
"notifications_tags": "Etiquetas",
|
||||
"notifications_new_indicator": "Nova notificación",
|
||||
"notifications_attachment_open_button": "Abrir anexo",
|
||||
"notifications_click_open_button": "Abrir ligazón",
|
||||
"notifications_none_for_any_title": "Non recibiches ningunha notificación.",
|
||||
"notifications_none_for_any_description": "Para enviar notificacións ao tema, simplemente usa PUT ou POST ao URL do tema. Aquí tes un exemplo usando un dos teus temas.",
|
||||
"notifications_no_subscriptions_title": "Semella que aínda non tes subscricións.",
|
||||
"notifications_example": "Exemplo",
|
||||
"display_name_dialog_title": "Cambiar nonme público",
|
||||
"display_name_dialog_placeholder": "Nome público",
|
||||
"publish_dialog_title_topic": "Publicar en {{topic}}",
|
||||
"publish_dialog_title_no_topic": "Publicar notificación",
|
||||
"publish_dialog_progress_uploading": "Enviando…",
|
||||
"publish_dialog_progress_uploading_detail": "Enviando {{loaded}}/{{total}} ({{percent}}%) …",
|
||||
"publish_dialog_message_published": "Notificación publicada",
|
||||
"publish_dialog_attachment_limits_file_and_quota_reached": "supera o límite de ficheiros e cota {{fileSizeLimit}}, quedan {{remainingBytes}}",
|
||||
"publish_dialog_attachment_limits_file_reached": "supera o límite para ficheiros {{fileSizeLimit}}",
|
||||
"publish_dialog_attachment_limits_quota_reached": "supera a cota, quedan {{remainingBytes}}",
|
||||
"publish_dialog_emoji_picker_show": "Elixe emoji",
|
||||
"publish_dialog_priority_min": "Prioridade Mínima",
|
||||
"publish_dialog_priority_low": "Prioridade baixa",
|
||||
"publish_dialog_priority_default": "Prioridade por defecto",
|
||||
"publish_dialog_priority_high": "Prioridade alta",
|
||||
"publish_dialog_priority_max": "Prioridade Máxima",
|
||||
"publish_dialog_base_url_label": "URL de servizo",
|
||||
"notifications_more_details": "Para máis información, visita o <websiteLink>sitio web</websiteLink> ou le a <docsLink>documentación</docsLink>.",
|
||||
"publish_dialog_call_label": "Chamada de teléfono",
|
||||
"publish_dialog_call_reset": "Retirar chamada de teléfono",
|
||||
"publish_dialog_delay_placeholder": "Adiar a entrega, ex. {{unixTimestamp}}, {{relativeTime}}, ou \"{{naturalLanguage}}\" (Só en inglés)",
|
||||
"publish_dialog_other_features": "Outras características:",
|
||||
"publish_dialog_chip_click_label": "Premer en URL",
|
||||
"publish_dialog_chip_email_label": "Reenvío por correo",
|
||||
"publish_dialog_chip_call_label": "Chamada de teléfono",
|
||||
"publish_dialog_chip_attach_url_label": "Anexar ficheiro por URL",
|
||||
"publish_dialog_button_cancel_sending": "Cancelar o envío",
|
||||
"publish_dialog_button_cancel": "Cancelar",
|
||||
"publish_dialog_button_send": "Enviar",
|
||||
"publish_dialog_attached_file_title": "Ficheiro anexo:",
|
||||
"publish_dialog_attached_file_filename_placeholder": "Nome do ficheiro anexo",
|
||||
"publish_dialog_drop_file_here": "Soltar aquí o ficheiro",
|
||||
"emoji_picker_search_placeholder": "Buscar emoji",
|
||||
"subscribe_dialog_subscribe_title": "Subscribirse a un tema",
|
||||
"publish_dialog_call_item": "Número de teléfono {{number}}",
|
||||
"publish_dialog_email_placeholder": "Enderezo ao que reenviar a notificación, ex. xoana@exemplo.com",
|
||||
"publish_dialog_email_reset": "Retirar reenvío ao correo",
|
||||
"publish_dialog_attach_label": "URL do anexo",
|
||||
"publish_dialog_attach_placeholder": "Anexa un ficheiro por URL, ex. https://f-droid.org/F-Droid.apk",
|
||||
"publish_dialog_attach_reset": "Retirar URL do anexo",
|
||||
"publish_dialog_filename_placeholder": "Nome do ficheiro anexo",
|
||||
"publish_dialog_filename_label": "Nome do ficheiro",
|
||||
"publish_dialog_delay_label": "Adiar",
|
||||
"publish_dialog_delay_reset": "Retirar o adiadamento da entrega",
|
||||
"publish_dialog_chip_attach_file_label": "Anexar ficheiro local",
|
||||
"publish_dialog_chip_delay_label": "Entrega adiada",
|
||||
"publish_dialog_chip_topic_label": "Cambiar tema",
|
||||
"publish_dialog_details_examples_description": "Para ver exemplos e unha descrición polo miúdo das ferramentas de envío, le a <docsLink>documentación</docsLink>.",
|
||||
"publish_dialog_checkbox_publish_another": "Publicar outra",
|
||||
"emoji_picker_search_clear": "Limpar busca",
|
||||
"publish_dialog_chip_call_no_verified_numbers_tooltip": "Números de teléfono non verificados",
|
||||
"publish_dialog_attached_file_remove": "Retirar ficheiro anexo"
|
||||
}
|
||||
|
||||
@@ -267,5 +267,6 @@
|
||||
"publish_dialog_chip_call_label": "Chiamata telefonica",
|
||||
"publish_dialog_chip_call_no_verified_numbers_tooltip": "Nessun numero verificato",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -190,5 +190,10 @@
|
||||
"error_boundary_unsupported_indexeddb_title": "Privat surfing støttes ikke",
|
||||
"action_bar_account": "Konto",
|
||||
"action_bar_profile_settings": "Innstillinger",
|
||||
"nav_button_account": "Konto"
|
||||
"nav_button_account": "Konto",
|
||||
"signup_title": "Opprett en ntfy konto",
|
||||
"signup_form_username": "Brukernavn",
|
||||
"signup_form_password": "Passord",
|
||||
"signup_form_button_submit": "Meld deg på",
|
||||
"signup_form_confirm_password": "Bekreft passord"
|
||||
}
|
||||
|
||||
@@ -191,10 +191,33 @@
|
||||
"error_boundary_unsupported_indexeddb_description": "O ntfy web app precisa do IndexedDB para funcionar, e seu navegador não suporta IndexedDB no modo de navegação privada.<br/><br/>Embora isso seja lamentável, também não faz muito sentido usar o ntfy web app no modo de navegação privada de qualquer maneira, porque tudo é armazenado no armazenamento do navegador. Você pode ler mais sobre isso <githubLink>nesta edição do GitHub</githubLink>, ou falar conosco em <discordLink>Discord</discordLink> ou <matrixLink>Matrix</matrixLink>.",
|
||||
"action_bar_reservation_add": "Reserve topic",
|
||||
"action_bar_reservation_edit": "Change reservation",
|
||||
"signup_disabled": "Signup is disabled",
|
||||
"signup_error_username_taken": "Username {{username}} is already taken",
|
||||
"signup_error_creation_limit_reached": "Account creation limit reached",
|
||||
"action_bar_reservation_delete": "N",
|
||||
"action_bar_account": "Account",
|
||||
"action_bar_change_display_name": "Change display name"
|
||||
"signup_disabled": "Registrar está desativado",
|
||||
"signup_error_username_taken": "Usuário {{username}} já existe",
|
||||
"signup_error_creation_limit_reached": "Limite de criação de contas atingido",
|
||||
"action_bar_reservation_delete": "Remover reserva",
|
||||
"action_bar_account": "Conta",
|
||||
"action_bar_change_display_name": "Change display name",
|
||||
"common_copy_to_clipboard": "Copiar para área de transferência",
|
||||
"login_link_signup": "Registrar",
|
||||
"login_title": "Entrar na sua conta ntfy",
|
||||
"login_form_button_submit": "Entrar",
|
||||
"login_disabled": "Login está desabilitado",
|
||||
"action_bar_reservation_limit_reached": "Limite atingido",
|
||||
"action_bar_profile_title": "Perfil",
|
||||
"action_bar_profile_settings": "Configurações",
|
||||
"action_bar_profile_logout": "Sair",
|
||||
"action_bar_sign_in": "Entrar",
|
||||
"action_bar_sign_up": "Registrar",
|
||||
"nav_button_account": "Conta",
|
||||
"signup_title": "Criar uma conta ntfy",
|
||||
"signup_form_username": "Usuário",
|
||||
"signup_form_password": "Senha",
|
||||
"signup_form_confirm_password": "Confirmar senha",
|
||||
"signup_form_button_submit": "Registrar",
|
||||
"account_basics_phone_numbers_title": "Telefones",
|
||||
"signup_form_toggle_password_visibility": "Ativar visibilidade de senha",
|
||||
"signup_already_have_account": "Já possui uma conta? Entrar!",
|
||||
"nav_upgrade_banner_label": "Atualizar para ntfy Pro",
|
||||
"account_basics_phone_numbers_dialog_description": "Para usar o recurso de notificação de chamada, é necessários adicionar e verificar pelo menos um número de telefone. A verificação pode ser feita por SMS ou chamada telefônica.",
|
||||
"account_basics_phone_numbers_description": "Para notificações de chamada telefônica"
|
||||
}
|
||||
|
||||
@@ -352,5 +352,16 @@
|
||||
"account_upgrade_dialog_tier_price_billed_monthly": "{{price}} в год. Оплата помесячно.",
|
||||
"account_upgrade_dialog_tier_price_billed_yearly": "{{price}} ежегодно. Сэкономьте {{save}}.",
|
||||
"account_upgrade_dialog_billing_contact_email": "По вопросам оплаты, пожалуйста <Link>свяжитесь с нами</Link>.",
|
||||
"account_upgrade_dialog_billing_contact_website": "По вопросам оплаты, пожалуйста обратитесь к нашему <Link>сайту</Link>."
|
||||
"account_upgrade_dialog_billing_contact_website": "По вопросам оплаты, пожалуйста обратитесь к нашему <Link>сайту</Link>.",
|
||||
"publish_dialog_call_reset": "Удалить вызов",
|
||||
"account_basics_phone_numbers_dialog_description": "Для использования уведомлений необходимо добавить и подтвердить хотя бы один номер телефона. Проверить можно используя SMS или звонок.",
|
||||
"account_basics_phone_numbers_dialog_title": "Добавить номер телефона",
|
||||
"account_basics_phone_numbers_dialog_number_placeholder": "например +1222333444",
|
||||
"account_basics_phone_numbers_dialog_code_placeholder": "например 123456",
|
||||
"account_basics_phone_numbers_dialog_verify_button_sms": "Отправить SMS",
|
||||
"account_usage_calls_title": "Совершённые вызовы",
|
||||
"account_usage_calls_none": "Невозможно совершать вызовы с этим аккаунтом",
|
||||
"publish_dialog_chip_call_no_verified_numbers_tooltip": "Нет проверенных номеров",
|
||||
"account_basics_phone_numbers_copied_to_clipboard": "Номер телефона скопирован в буфер обмена",
|
||||
"account_upgrade_dialog_tier_features_no_calls": "Нет вызовов"
|
||||
}
|
||||
|
||||
177
web/public/static/langs/sk.json
Normal file
177
web/public/static/langs/sk.json
Normal file
@@ -0,0 +1,177 @@
|
||||
{
|
||||
"common_save": "Uložiť",
|
||||
"common_back": "Späť",
|
||||
"common_copy_to_clipboard": "Kopírovať do schránky",
|
||||
"signup_title": "Vytvoriť ntfy účet",
|
||||
"signup_form_username": "Používateľské meno",
|
||||
"signup_form_confirm_password": "Potvrdenie hesla",
|
||||
"signup_form_button_submit": "Zaregistrovať sa",
|
||||
"signup_form_toggle_password_visibility": "Prepnúť viditeľnosť hesla",
|
||||
"signup_error_username_taken": "Používateľské meno {{username}} je už obsadené",
|
||||
"login_form_button_submit": "Prihlásiť sa",
|
||||
"login_disabled": "Prihlásenie je zakázané",
|
||||
"action_bar_logo_alt": "ntfy logo",
|
||||
"action_bar_settings": "Nastavenia",
|
||||
"action_bar_account": "Účet",
|
||||
"action_bar_sign_in": "Prihlásiť sa",
|
||||
"action_bar_profile_settings": "Nastavenia",
|
||||
"action_bar_reservation_edit": "Zmeniť rezerváciu",
|
||||
"action_bar_unsubscribe": "Odhlásiť odber",
|
||||
"action_bar_toggle_mute": "Stlmiť/zrušiť stlmenie upozornení",
|
||||
"action_bar_toggle_action_menu": "Otvoriť/zavrieť akčné menu",
|
||||
"action_bar_profile_title": "Profil",
|
||||
"nav_button_settings": "Nastavenia",
|
||||
"nav_button_account": "Účet",
|
||||
"message_bar_show_dialog": "Zobraziť okno pre odosielanie oznámení",
|
||||
"message_bar_publish": "Zverejniť správu",
|
||||
"nav_topics_title": "Odoberané témy",
|
||||
"nav_button_all_notifications": "Všetky oznámenia",
|
||||
"alert_grant_description": "Udeliť prehliadaču povolenie na zobrazovanie oznámení na ploche.",
|
||||
"alert_not_supported_context_description": "Oznámenia sú podporované len cez HTTPS. Ide o obmedzenie rozhrania <mdnLink>Notifications API</mdnLink>.",
|
||||
"notifications_list": "Zoznam oznámení",
|
||||
"notifications_list_item": "Oznámenie",
|
||||
"notifications_mark_read": "Označiť ako prečítané",
|
||||
"notifications_delete": "Zmazať",
|
||||
"notifications_copied_to_clipboard": "Skopírované do schránky",
|
||||
"notifications_tags": "Štítky",
|
||||
"notifications_priority_x": "Priorita {{priority}}",
|
||||
"notifications_new_indicator": "Nové oznámenie",
|
||||
"notifications_attachment_image": "Obrázok prílohy",
|
||||
"notifications_attachment_link_expired": "odkaz na stiahnutie vypršal",
|
||||
"notifications_attachment_file_image": "súbor s obrázkom",
|
||||
"notifications_attachment_file_video": "video súbor",
|
||||
"notifications_attachment_file_audio": "zvukový súbor",
|
||||
"notifications_attachment_file_app": "Súbor aplikácie pre Android",
|
||||
"notifications_attachment_file_document": "iný dokument",
|
||||
"notifications_click_copy_url_title": "Skopírovať URL adresu odkazu do schránky",
|
||||
"notifications_click_copy_url_button": "Kopírovať odkaz",
|
||||
"notifications_click_open_button": "Otvoriť odkaz",
|
||||
"notifications_actions_not_supported": "Akcia nie je podporovaná vo webovej aplikácii",
|
||||
"notifications_none_for_topic_title": "K tejto téme ste zatiaľ nedostali žiadne upozornenia.",
|
||||
"notifications_none_for_any_title": "Nedostali ste žiadne upozornenia.",
|
||||
"notifications_none_for_any_description": "Ak chcete posielať oznámenia do témy, jednoducho zadajte adresu PUT alebo POST na adresu URL témy. Tu je príklad s použitím jednej z vašich tém.",
|
||||
"notifications_no_subscriptions_title": "Zdá sa, že zatiaľ nemáte žiadne prihlásenia na odber.",
|
||||
"display_name_dialog_title": "Zmeniť zobrazovaný názov",
|
||||
"notifications_no_subscriptions_description": "Kliknutím na odkaz \"{{text odkazu}}\" vytvoríte tému alebo sa na ňu prihlásite. Potom môžete posielať správy prostredníctvom PUT alebo POST a budete tu dostávať oznámenia.",
|
||||
"notifications_example": "Príklad",
|
||||
"notifications_more_details": "Ďalšie informácie nájdete na <websiteLink>webovej stránke</websiteLink> alebo v <docsLink>dokumentácií</docsLink>.",
|
||||
"display_name_dialog_placeholder": "Zobrazený názov",
|
||||
"reserve_dialog_checkbox_label": "Rezervovať tému a nakonfigurovať prístup",
|
||||
"notifications_loading": "Načítavanie oznámení …",
|
||||
"publish_dialog_title_no_topic": "Zverejniť oznámenie",
|
||||
"publish_dialog_title_topic": "Zverejniť v {{topic}}",
|
||||
"publish_dialog_progress_uploading": "Nahrávanie…",
|
||||
"publish_dialog_progress_uploading_detail": "Nahrávanie {{loaded}}/{{total}} ({{percent}}%) …",
|
||||
"publish_dialog_message_published": "Oznámenie zverejnené",
|
||||
"publish_dialog_attachment_limits_file_and_quota_reached": "prekročí {{fileSizeLimit}} limit súboru a kvótu, {{remainingBytes}} zostáva",
|
||||
"publish_dialog_attachment_limits_file_reached": "prekračuje {{fileSizeLimit}} limit súboru",
|
||||
"publish_dialog_attachment_limits_quota_reached": "prekračuje kvótu, {{remainingBytes}} zostáva",
|
||||
"publish_dialog_emoji_picker_show": "Vyberte emoji",
|
||||
"publish_dialog_priority_min": "Min. priorita",
|
||||
"publish_dialog_priority_low": "Nízka priorita",
|
||||
"publish_dialog_priority_default": "Predvolená priorita",
|
||||
"publish_dialog_priority_high": "Vysoká priorita",
|
||||
"publish_dialog_priority_max": "Max. priorita",
|
||||
"publish_dialog_base_url_label": "URL Adresa služby",
|
||||
"publish_dialog_base_url_placeholder": "URL adresa služby, napr. https://example.com",
|
||||
"publish_dialog_topic_label": "Názov témy",
|
||||
"publish_dialog_topic_placeholder": "Názov témy, napr. phil_alerts",
|
||||
"publish_dialog_topic_reset": "Resetovať tému",
|
||||
"publish_dialog_title_label": "Názov",
|
||||
"publish_dialog_title_placeholder": "Názov oznámenia, napr. Upozornenie na miesto na disku",
|
||||
"publish_dialog_tags_label": "Štítky",
|
||||
"publish_dialog_message_label": "Správa",
|
||||
"publish_dialog_priority_label": "Priorita",
|
||||
"publish_dialog_click_label": "Kliknite na URL",
|
||||
"publish_dialog_click_placeholder": "URL adresa sa otvorí po kliknutí na oznámenie",
|
||||
"publish_dialog_email_label": "Email",
|
||||
"publish_dialog_email_placeholder": "Emailová adresa, na ktorú sa má oznámenie zaslať, napr. phil@example.com",
|
||||
"publish_dialog_call_label": "Telefonovať",
|
||||
"publish_dialog_call_item": "Zavolať na telefónne číslo {{number}}",
|
||||
"publish_dialog_call_reset": "Odstrániť telefón",
|
||||
"publish_dialog_attach_label": "URL prílohy",
|
||||
"publish_dialog_attach_reset": "Odstrániť URL prílohy",
|
||||
"publish_dialog_filename_label": "Názov súboru",
|
||||
"publish_dialog_filename_placeholder": "Názov súboru prílohy",
|
||||
"publish_dialog_delay_label": "Oneskorenie",
|
||||
"publish_dialog_delay_placeholder": "Oneskorenie doručenia, napr. {{unixTimestamp}}, {{relativeTime}} alebo \"{{naturalLanguage}}\" (len v angličtine)",
|
||||
"publish_dialog_delay_reset": "Odstrániť oneskorené doručenie",
|
||||
"publish_dialog_chip_call_label": "Telefonovať",
|
||||
"publish_dialog_other_features": "Ďalšie funkcie:",
|
||||
"publish_dialog_chip_call_no_verified_numbers_tooltip": "Žiadne overené telefónne čísla",
|
||||
"publish_dialog_chip_attach_url_label": "Pripojiť súbor pomocou adresy URL",
|
||||
"publish_dialog_chip_delay_label": "Oneskoriť doručenie",
|
||||
"publish_dialog_chip_topic_label": "Zmeniť tému",
|
||||
"publish_dialog_button_cancel_sending": "Zrušiť odosielanie",
|
||||
"publish_dialog_button_send": "Odoslať",
|
||||
"publish_dialog_checkbox_publish_another": "Zverejniť ďalšie",
|
||||
"publish_dialog_attached_file_title": "Priložený súbor:",
|
||||
"subscribe_dialog_subscribe_button_cancel": "Zrušiť",
|
||||
"subscribe_dialog_subscribe_title": "Odoberať tému",
|
||||
"subscribe_dialog_subscribe_base_url_label": "URL Adresa služby",
|
||||
"subscribe_dialog_subscribe_topic_placeholder": "Názov témy, napr. phil_alerts",
|
||||
"publish_dialog_attached_file_filename_placeholder": "Názov súboru prílohy",
|
||||
"publish_dialog_attached_file_remove": "Odstrániť priložený súbor",
|
||||
"publish_dialog_drop_file_here": "Vložiť súbor",
|
||||
"subscribe_dialog_login_password_label": "Heslo",
|
||||
"account_basics_password_dialog_confirm_password_label": "Potvrdenie hesla",
|
||||
"account_basics_title": "Účet",
|
||||
"account_delete_dialog_button_cancel": "Zrušiť",
|
||||
"account_delete_dialog_label": "Heslo",
|
||||
"prefs_reservations_dialog_title_add": "Rezervovať tému",
|
||||
"publish_dialog_button_cancel": "Zrušiť",
|
||||
"account_upgrade_dialog_button_cancel": "Zrušiť",
|
||||
"account_tokens_dialog_button_cancel": "Zrušiť",
|
||||
"common_cancel": "Zrušiť",
|
||||
"common_add": "Pridať",
|
||||
"account_basics_username_title": "Používateľské meno",
|
||||
"signup_form_password": "Heslo",
|
||||
"signup_error_creation_limit_reached": "Dosiahnutý limit na vytvorenie konta",
|
||||
"account_basics_password_title": "Heslo",
|
||||
"action_bar_change_display_name": "Zmeniť zobrazovaný názov",
|
||||
"prefs_users_dialog_password_label": "Heslo",
|
||||
"action_bar_sign_up": "Zaregistrovať sa",
|
||||
"login_link_signup": "Zaregistrovať sa",
|
||||
"signup_already_have_account": "Už máte účet? Prihláste sa!",
|
||||
"signup_disabled": "Registrácia je vypnutá",
|
||||
"login_title": "Prihláste sa do svojho konta ntfy",
|
||||
"action_bar_show_menu": "Zobraziť menu",
|
||||
"action_bar_reservation_add": "Rezervovať tému",
|
||||
"action_bar_reservation_delete": "Odstrániť rezerváciu",
|
||||
"action_bar_reservation_limit_reached": "Dosiahnutý limit",
|
||||
"action_bar_send_test_notification": "Odoslať testovacie oznámenie",
|
||||
"action_bar_clear_notifications": "Vymazať všetky oznámenia",
|
||||
"publish_dialog_message_placeholder": "Sem napíšte správu",
|
||||
"action_bar_profile_logout": "Odhlásiť sa",
|
||||
"message_bar_type_message": "Sem napíšte správu",
|
||||
"message_bar_error_publishing": "Chyba pri zverejňovaní oznámenia",
|
||||
"nav_button_documentation": "Dokumentácia",
|
||||
"nav_button_publish_message": "Zverejniť oznámenie",
|
||||
"nav_button_subscribe": "Odoberať tému",
|
||||
"nav_button_muted": "Oznámenia stlmené",
|
||||
"nav_button_connecting": "pripájanie",
|
||||
"nav_upgrade_banner_description": "Rezervovať témy, viac správ a e-mailov a väčšie prílohy",
|
||||
"nav_upgrade_banner_label": "Vylepšiť na ntfy Pro",
|
||||
"alert_grant_title": "Oznámenia sú vypnuté",
|
||||
"alert_grant_button": "Prideliť teraz",
|
||||
"alert_not_supported_title": "Oznámenia nie sú podporované",
|
||||
"alert_not_supported_description": "Oznámenia nie sú vo vašom prehliadači podporované.",
|
||||
"notifications_attachment_copy_url_title": "Kopírovať URL adresu prílohy do schránky",
|
||||
"notifications_attachment_copy_url_button": "Kopírovať adresu URL",
|
||||
"notifications_attachment_open_title": "Prejsť na {{url}}",
|
||||
"notifications_actions_open_url_title": "Prejsť na {{url}}",
|
||||
"notifications_attachment_open_button": "Otvoriť prílohu",
|
||||
"notifications_attachment_link_expires": "platnosť odkazu vyprší {{date}}",
|
||||
"notifications_none_for_topic_description": "Ak chcete posielať oznámenia do tejto témy, jednoducho zadajte adresu PUT alebo POST na URL adresu témy.",
|
||||
"notifications_actions_http_request_title": "Odoslať HTTP {{method}} na {{url}}",
|
||||
"display_name_dialog_description": "Nastavenie alternatívneho názvu témy, ktorá sa zobrazuje v zozname odberov. Pomáha to ľahšie identifikovať témy so zložitými názvami.",
|
||||
"prefs_users_table_base_url_header": "URL Adresa služby",
|
||||
"publish_dialog_tags_placeholder": "Zoznam štítkov oddelených čiarkou, napr. varovanie, srv1-backup",
|
||||
"publish_dialog_chip_click_label": "Kliknite na URL",
|
||||
"publish_dialog_email_reset": "Odstrániť email na preposielanie",
|
||||
"publish_dialog_click_reset": "Odobrať URL kliknutím",
|
||||
"publish_dialog_attach_placeholder": "Pripojiť súbor pomocou URL adresy, napr. https://f-droid.org/F-Droid.apk",
|
||||
"publish_dialog_chip_email_label": "Preposlanie na email",
|
||||
"publish_dialog_chip_attach_file_label": "Pripojiť miestny súbor",
|
||||
"publish_dialog_details_examples_description": "Príklady a podrobný opis všetkých funkcií odosielania nájdete v <docsLink>dokumentácii</docsLink>."
|
||||
}
|
||||
@@ -277,7 +277,7 @@
|
||||
"publish_dialog_priority_low": "Låg prioritet",
|
||||
"publish_dialog_priority_default": "Standard prioritet",
|
||||
"publish_dialog_priority_high": "Hög prioritet",
|
||||
"publish_dialog_priority_max": "Högsta prioritet",
|
||||
"publish_dialog_priority_max": "Max. prioritet",
|
||||
"publish_dialog_base_url_label": "Service-URL",
|
||||
"publish_dialog_email_label": "E-post",
|
||||
"publish_dialog_attach_reset": "Ta bort URL för bifogade filer",
|
||||
|
||||
21
web/public/static/langs/vi.json
Normal file
21
web/public/static/langs/vi.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"common_add": "Thêm",
|
||||
"common_back": "Quay lại",
|
||||
"signup_title": "Tạo tài khoản ntfy",
|
||||
"signup_form_toggle_password_visibility": "Hiện mật khẩu",
|
||||
"login_form_button_submit": "Đăng nhập",
|
||||
"common_copy_to_clipboard": "Lưu vào clipboard",
|
||||
"signup_form_username": "Tên user",
|
||||
"signup_already_have_account": "Đã có tài khoản? Đăng nhập!",
|
||||
"signup_disabled": "Đăng kí bị đóng",
|
||||
"signup_error_username_taken": "Tên {{username}} đã được sử dụng",
|
||||
"signup_error_creation_limit_reached": "Đã bị giới hạn tạo tài khoản",
|
||||
"login_title": "Đăng nhập vào tài khoản ntfy",
|
||||
"login_link_signup": "Đăng kí",
|
||||
"login_disabled": "Đăng nhập bị đóng",
|
||||
"action_bar_show_menu": "Hiện menu",
|
||||
"signup_form_password": "Mật khẩu",
|
||||
"action_bar_settings": "Cài đặt",
|
||||
"signup_form_confirm_password": "Xác nhận mật khẩu",
|
||||
"signup_form_button_submit": "Đăng kí"
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
{
|
||||
"action_bar_show_menu": "显示菜单",
|
||||
"action_bar_logo_alt": "ntfy图标",
|
||||
"action_bar_mute_notifications": "静音",
|
||||
"action_bar_settings": "设置",
|
||||
"action_bar_send_test_notification": "发送测试通知",
|
||||
"action_bar_clear_notifications": "清除所有通知",
|
||||
"action_bar_unsubscribe": "取消订阅",
|
||||
"action_bar_toggle_action_menu": "开启或关闭操作菜单",
|
||||
"action_bar_unmute_notifications": "取消静音",
|
||||
"message_bar_type_message": "在此处输入消息",
|
||||
"message_bar_show_dialog": "显示发布对话框",
|
||||
"message_bar_publish": "发布消息",
|
||||
@@ -20,6 +22,10 @@
|
||||
"alert_notification_permission_required_button": "现在授予",
|
||||
"alert_not_supported_title": "不支持通知",
|
||||
"alert_not_supported_description": "您的浏览器不支持通知。",
|
||||
"alert_notification_ios_install_required_description": "要接收通知,请在iOS上点击分享图标,然后添加到主屏幕。",
|
||||
"alert_notification_ios_install_required_title": "需要安装iOS应用程序",
|
||||
"alert_notification_permission_denied_description": "你已禁用通知。要重新启用通知,请在浏览器设置中启用通知。",
|
||||
"alert_notification_permission_denied_title": "已禁用通知",
|
||||
"notifications_list": "通知列表",
|
||||
"notifications_list_item": "通知",
|
||||
"notifications_mark_read": "标记为已读",
|
||||
@@ -117,9 +123,9 @@
|
||||
"prefs_notifications_min_priority_description_x_or_higher": "仅显示优先级为{{number}}({{name}})或以上的通知",
|
||||
"prefs_notifications_min_priority_description_max": "仅显示最高优先级的通知",
|
||||
"prefs_notifications_min_priority_any": "任意优先级",
|
||||
"prefs_notifications_min_priority_low_and_higher": "低优先级和更高优先级",
|
||||
"prefs_notifications_min_priority_default_and_higher": "默认优先级和更高优先级",
|
||||
"prefs_notifications_min_priority_high_and_higher": "高优先级和更高优先级",
|
||||
"prefs_notifications_min_priority_low_and_higher": "低优先级或更高",
|
||||
"prefs_notifications_min_priority_default_and_higher": "默认优先级或更高",
|
||||
"prefs_notifications_min_priority_high_and_higher": "高优先级或更高",
|
||||
"prefs_notifications_min_priority_max_only": "仅最高优先级",
|
||||
"prefs_notifications_delete_after_never": "从不",
|
||||
"prefs_notifications_delete_after_one_month": "一月后",
|
||||
@@ -129,6 +135,11 @@
|
||||
"prefs_notifications_delete_after_one_day_description": "一天后自动删除通知",
|
||||
"prefs_notifications_delete_after_one_week_description": "一周后自动删除通知",
|
||||
"prefs_notifications_delete_after_one_month_description": "一月后后自动删除通知",
|
||||
"prefs_notifications_web_push_disabled": "已暂用",
|
||||
"prefs_notifications_web_push_disabled_description": "当网页程序在运行时将会收到通知 (透过 WebSocket)",
|
||||
"prefs_notifications_web_push_enabled": "已为 {{server}} 启用",
|
||||
"prefs_notifications_web_push_enabled_description": "即使网页程序未有运行亦会收到通知 (via Web Push)",
|
||||
"prefs_notifications_web_push_title": "背景通知",
|
||||
"prefs_users_title": "管理用户",
|
||||
"prefs_users_description": "在此处添加/删除受保护主题的用户。请注意,用户名和密码存储在浏览器的本地存储中。",
|
||||
"prefs_users_add_button": "添加用户",
|
||||
@@ -140,6 +151,10 @@
|
||||
"common_save": "保存",
|
||||
"prefs_appearance_title": "外观",
|
||||
"prefs_appearance_language_title": "语言",
|
||||
"prefs_appearance_theme_title": "主題",
|
||||
"prefs_appearance_theme_system": "系統 (預設)",
|
||||
"prefs_appearance_theme_dark": "黑暗模式",
|
||||
"prefs_appearance_theme_light": "光亮模式",
|
||||
"priority_min": "最低",
|
||||
"priority_low": "低",
|
||||
"priority_default": "默认",
|
||||
@@ -149,6 +164,7 @@
|
||||
"prefs_users_table_base_url_header": "服务链接地址",
|
||||
"prefs_users_dialog_base_url_label": "服务链接地址,例如 https://ntfy.sh",
|
||||
"error_boundary_button_copy_stack_trace": "复制堆栈跟踪",
|
||||
"error_boundary_button_reload_ntfy": "重新加载 ntfy",
|
||||
"error_boundary_stack_trace": "堆栈跟踪",
|
||||
"error_boundary_gathering_info": "收集更多信息……",
|
||||
"error_boundary_unsupported_indexeddb_title": "不支持隐私浏览",
|
||||
@@ -160,6 +176,7 @@
|
||||
"notifications_attachment_copy_url_button": "复制链接地址",
|
||||
"notifications_attachment_open_title": "转到 {{url}}",
|
||||
"notifications_actions_http_request_title": "发送 HTTP {{method}} 到 {{url}}",
|
||||
"notifications_actions_failed_notification": "通知失败",
|
||||
"notifications_actions_open_url_title": "转到 {{url}}",
|
||||
"notifications_none_for_topic_description": "要向此主题发送通知,只需使用 PUT 或 POST 到主题链接即可。",
|
||||
"subscribe_dialog_subscribe_topic_placeholder": "主题名,例如 phil_alerts",
|
||||
@@ -168,12 +185,14 @@
|
||||
"publish_dialog_title_placeholder": "主题标题,例如 磁盘空间告警",
|
||||
"publish_dialog_email_label": "电子邮件",
|
||||
"publish_dialog_button_send": "发送",
|
||||
"publish_dialog_checkbox_markdown": "格式化为 Markdown",
|
||||
"publish_dialog_attachment_limits_quota_reached": "超过配额,剩余 {{remainingBytes}}",
|
||||
"publish_dialog_attach_label": "附件链接地址",
|
||||
"publish_dialog_click_reset": "移除点击连接地址",
|
||||
"publish_dialog_button_cancel": "取消",
|
||||
"subscribe_dialog_subscribe_button_cancel": "取消",
|
||||
"subscribe_dialog_subscribe_base_url_label": "服务地址地址",
|
||||
"subscribe_dialog_subscribe_use_another_background_info": "当网页程序未开启, 将不会收到来自其他服务器的通知",
|
||||
"prefs_notifications_min_priority_description_any": "显示所有通知,无论优先级如何",
|
||||
"prefs_notifications_delete_after_title": "删除通知",
|
||||
"prefs_notifications_delete_after_three_hours": "三小时后",
|
||||
@@ -359,5 +378,30 @@
|
||||
"publish_dialog_chip_call_no_verified_numbers_tooltip": "未验证的手机号",
|
||||
"account_basics_phone_numbers_title": "电话号码",
|
||||
"account_basics_phone_numbers_description": "电话通知",
|
||||
"account_basics_phone_numbers_dialog_description": "要使用来电通知功能,您需要添加并验证至少一个电话号码。可以通过短信或电话进行验证。"
|
||||
"account_basics_phone_numbers_dialog_description": "要使用来电通知功能,您需要添加并验证至少一个电话号码。可以通过短信或电话进行验证。",
|
||||
"account_basics_phone_numbers_dialog_code_label": "验证码",
|
||||
"account_basics_phone_numbers_dialog_code_placeholder": "例如:123456",
|
||||
"account_basics_phone_numbers_dialog_check_verification_button": "确认码",
|
||||
"account_basics_phone_numbers_dialog_channel_sms": "短信",
|
||||
"account_basics_phone_numbers_dialog_channel_call": "拨打",
|
||||
"publish_dialog_call_reset": "清空拨号",
|
||||
"account_basics_phone_numbers_no_phone_numbers_yet": "无可执行的电话号码",
|
||||
"account_basics_phone_numbers_dialog_title": "添加电话号码",
|
||||
"account_basics_phone_numbers_copied_to_clipboard": "电话号码已复制到剪贴板",
|
||||
"account_basics_phone_numbers_dialog_number_label": "电话号码",
|
||||
"account_basics_phone_numbers_dialog_number_placeholder": "例如:+1222333444",
|
||||
"account_usage_calls_title": "已拨打电话",
|
||||
"account_usage_calls_none": "此帐号无法拨打电话",
|
||||
"account_upgrade_dialog_tier_features_reservations_one": "一条保留主题",
|
||||
"account_upgrade_dialog_tier_features_emails_one": "一封每日邮件",
|
||||
"account_upgrade_dialog_tier_features_calls_one": "一通每日电话",
|
||||
"account_basics_phone_numbers_dialog_verify_button_sms": "发送信息",
|
||||
"account_basics_phone_numbers_dialog_verify_button_call": "拨打电话",
|
||||
"account_upgrade_dialog_tier_features_messages_one": "一条每日消息",
|
||||
"account_upgrade_dialog_tier_features_calls_other": "{{calls}} 通每日电话",
|
||||
"account_upgrade_dialog_tier_features_no_calls": "无电话呼叫",
|
||||
"web_push_subscription_expiring_title": "通知将被暂停",
|
||||
"web_push_subscription_expiring_body": "打开ntfy以继续接收通知",
|
||||
"web_push_unknown_notification_title": "接收到未知通知",
|
||||
"web_push_unknown_notification_body": "你可能需要打开网页来更新ntfy"
|
||||
}
|
||||
|
||||
@@ -1,220 +1,408 @@
|
||||
{
|
||||
"action_bar_show_menu": "顯示選單",
|
||||
"action_bar_logo_alt": "ntfy 標識",
|
||||
"action_bar_unsubscribe": "取消訂閱",
|
||||
"action_bar_toggle_mute": "通知靜音/解除通知靜音",
|
||||
"action_bar_toggle_action_menu": "開啟/關閉操作選單",
|
||||
"message_bar_type_message": "在這輸入訊息",
|
||||
"alert_notification_permission_required_description": "允許瀏覽器權限以顯示桌面通知。",
|
||||
"alert_notification_permission_required_button": "允許",
|
||||
"notifications_list": "通知清單",
|
||||
"notifications_list_item": "通知",
|
||||
"notifications_mark_read": "標示已讀",
|
||||
"notifications_attachment_image": "附加圖片",
|
||||
"notifications_attachment_copy_url_title": "複製附件 URL 到剪貼簿",
|
||||
"notifications_attachment_copy_url_button": "複製 URL",
|
||||
"notifications_attachment_open_title": "前往 {{url}}",
|
||||
"notifications_attachment_open_button": "開啟附件",
|
||||
"notifications_attachment_link_expired": "下載連結已過期",
|
||||
"notifications_attachment_file_video": "影片檔案",
|
||||
"notifications_attachment_file_app": "Android 應用程式檔案",
|
||||
"notifications_attachment_file_document": "其他文件",
|
||||
"notifications_click_copy_url_title": "複製連結 URL 到剪貼板",
|
||||
"notifications_click_copy_url_button": "複製連結",
|
||||
"notifications_click_open_button": "開啟連結",
|
||||
"notifications_actions_not_supported": "網頁程式無法支援該動作",
|
||||
"notifications_actions_http_request_title": "傳送 HTTP {{method}} 到 {{url}}",
|
||||
"notifications_none_for_topic_title": "尚未收到任何此主題的通知。",
|
||||
"notifications_none_for_topic_description": "如要寄送通知到此主題,請使用 PUT 或 POST 到此主題URL。",
|
||||
"notifications_none_for_any_title": "尚未收到任何通知。",
|
||||
"action_bar_mute_notifications": "靜音",
|
||||
"action_bar_settings": "設定",
|
||||
"action_bar_send_test_notification": "發送測試通知",
|
||||
"action_bar_clear_notifications": "清除所有通知",
|
||||
"action_bar_show_menu": "顯示選單",
|
||||
"nav_button_documentation": "文件",
|
||||
"nav_button_publish_message": "發佈通知",
|
||||
"nav_button_muted": "通知已靜音",
|
||||
"notifications_copied_to_clipboard": "已複製到剪貼簿",
|
||||
"message_bar_publish": "發佈訊息",
|
||||
"message_bar_show_dialog": "顯示發佈對話框",
|
||||
"message_bar_error_publishing": "發佈通知時發生錯誤",
|
||||
"action_bar_unsubscribe": "取消訂閱",
|
||||
"action_bar_toggle_action_menu": "開啟或關閉操作選單",
|
||||
"action_bar_unmute_notifications": "取消靜音",
|
||||
"message_bar_type_message": "在此處輸入訊息",
|
||||
"message_bar_show_dialog": "顯示發布對話框",
|
||||
"message_bar_publish": "發布訊息",
|
||||
"nav_topics_title": "訂閱主題",
|
||||
"nav_button_all_notifications": "所有通知",
|
||||
"nav_button_settings": "設定",
|
||||
"nav_button_all_notifications": "全部通知",
|
||||
"nav_button_documentation": "文檔",
|
||||
"nav_button_publish_message": "發布通知",
|
||||
"nav_button_subscribe": "訂閱主題",
|
||||
"nav_button_connecting": "連線中",
|
||||
"alert_notification_permission_required_title": "通知已關閉",
|
||||
"alert_not_supported_title": "不支援通知",
|
||||
"alert_not_supported_description": "瀏覽器不支援通知。",
|
||||
"notifications_tags": "標籤",
|
||||
"notifications_priority_x": "優先度 {{priority}}",
|
||||
"nav_button_connecting": "正在連接",
|
||||
"alert_notification_permission_required_title": "已禁用通知",
|
||||
"alert_notification_permission_required_description": "授予瀏覽器顯示桌面通知的權限。",
|
||||
"alert_notification_permission_required_button": "現在授予",
|
||||
"alert_not_supported_title": "不支援通知",
|
||||
"alert_not_supported_description": "你的瀏覽器不支援通知。",
|
||||
"alert_notification_ios_install_required_description": "要接收通知,請在 iOS 上點擊共享,然後添加到主屏幕",
|
||||
"alert_notification_ios_install_required_title": "需要安裝 iOS 應用程式",
|
||||
"alert_notification_permission_denied_description": "你已禁用通知。要重新啟用通知,請在瀏覽器設置中啟用通知。",
|
||||
"alert_notification_permission_denied_title": "已禁用通知",
|
||||
"notifications_list": "通知列表",
|
||||
"notifications_list_item": "通知",
|
||||
"notifications_mark_read": "標記為已讀",
|
||||
"notifications_copied_to_clipboard": "複製到剪貼板",
|
||||
"notifications_tags": "標記",
|
||||
"notifications_priority_x": "優先級 {{priority}}",
|
||||
"notifications_new_indicator": "新通知",
|
||||
"notifications_attachment_file_audio": "聲音檔案",
|
||||
"notifications_delete": "刪除",
|
||||
"notifications_attachment_open_button": "打開附件",
|
||||
"notifications_attachment_link_expires": "連結在 {{date}} 過期",
|
||||
"notifications_attachment_file_image": "圖片檔案",
|
||||
"notifications_actions_open_url_title": "前往 {{url}}",
|
||||
"notifications_no_subscriptions_title": "你尚未有任何訂閱。",
|
||||
"notifications_example": "範例",
|
||||
"notifications_more_details": "你可以在 <websiteLink>ntfy 網站</websiteLink>或者<docsLink>技術文件</docsLink>中查看更多資訊。",
|
||||
"notifications_loading": "載入中…",
|
||||
"publish_dialog_title_topic": "發佈到 {{topic}}",
|
||||
"publish_dialog_title_no_topic": "發佈通知",
|
||||
"publish_dialog_progress_uploading": "上傳中…",
|
||||
"publish_dialog_priority_label": "優先度",
|
||||
"publish_dialog_email_label": "電郵地址",
|
||||
"publish_dialog_filename_label": "檔案名稱",
|
||||
"publish_dialog_button_cancel": "取消",
|
||||
"publish_dialog_button_send": "傳送",
|
||||
"publish_dialog_button_cancel_sending": "取消傳送",
|
||||
"subscribe_dialog_subscribe_button_cancel": "取消",
|
||||
"subscribe_dialog_subscribe_button_subscribe": "訂閱",
|
||||
"emoji_picker_search_clear": "清除",
|
||||
"subscribe_dialog_login_password_label": "密碼",
|
||||
"common_back": "返回",
|
||||
"subscribe_dialog_login_button_login": "登入",
|
||||
"prefs_notifications_delete_after_never": "從不",
|
||||
"prefs_users_add_button": "新增使用者",
|
||||
"prefs_users_dialog_password_label": "密碼",
|
||||
"prefs_users_dialog_title_add": "新增使用者",
|
||||
"common_save": "儲存",
|
||||
"common_cancel": "取消",
|
||||
"error_boundary_title": "歐買尬,ntfy 壞掉了",
|
||||
"notifications_none_for_any_description": "要開始發送通知到一個主題,只需要對主題 URL 發送 HTTP PUT 或者 POST,例如:",
|
||||
"notifications_no_subscriptions_description": "點選 「{{linktext}}」 連結以建立或訂閱主題。完成後,你就可以使用 HTTP PUT 或者 POST 發送通知到這裡了!",
|
||||
"error_boundary_description": "很抱歉 ntfy 發生錯誤了。<br/>如果你有時間,煩請到<githubLink> Github </githubLink>回報錯誤,或者到<discordLink> Discord </discordLink>或者<matrixLink> Matrix 聊天室</matrixLink>裡面告訴我們。",
|
||||
"publish_dialog_tags_placeholder": "逗號分隔的標籤,例如 e.g. warning, srv1-backup",
|
||||
"publish_dialog_click_label": "點擊網址",
|
||||
"publish_dialog_attach_placeholder": "從網址新增附件,例如 https://f-droid.org/F-Droid.apk",
|
||||
"publish_dialog_attach_reset": "移除附件網址",
|
||||
"publish_dialog_attach_label": "附件網址",
|
||||
"publish_dialog_delay_reset": "移除延遲傳送",
|
||||
"publish_dialog_delay_label": "延遲",
|
||||
"publish_dialog_other_features": "其他功能:",
|
||||
"publish_dialog_filename_placeholder": "附件檔案名稱",
|
||||
"publish_dialog_delay_placeholder": "延遲傳送,例如 {{unixTimestamp}}, {{relativeTime}} 或 \"{{naturalLanguage}}\" (僅限英文)",
|
||||
"publish_dialog_chip_click_label": "點擊網址",
|
||||
"publish_dialog_chip_email_label": "轉發到電郵",
|
||||
"publish_dialog_chip_attach_url_label": "從網址新增附件",
|
||||
"emoji_picker_search_placeholder": "搜尋 emoji",
|
||||
"subscribe_dialog_subscribe_title": "訂閱主題",
|
||||
"subscribe_dialog_error_user_not_authorized": "用戶 {{username}} 沒有權限",
|
||||
"subscribe_dialog_error_user_anonymous": "匿名",
|
||||
"login_title": "登入 ntfy 帳戶",
|
||||
"action_bar_reservation_add": "保留主題",
|
||||
"action_bar_profile_logout": "登出",
|
||||
"alert_not_supported_context_description": "訊息只支援 HTTPS. 這是受 <mdnLink>Notifications API</mdnLink> 的限制",
|
||||
"publish_dialog_base_url_placeholder": "服務網址,例如 https://example.com",
|
||||
"signup_title": "創建 ntfy 賬戶",
|
||||
"signup_form_username": "用戶名稱",
|
||||
"signup_form_password": "密碼",
|
||||
"signup_form_button_submit": "註冊",
|
||||
"signup_form_toggle_password_visibility": "顯示/隱藏密碼",
|
||||
"signup_disabled": "註冊已停止",
|
||||
"signup_error_username_taken": "用戶名稱 {{username}} 已被取用",
|
||||
"signup_error_creation_limit_reached": "註冊賬戶限制",
|
||||
"login_form_button_submit": "登入",
|
||||
"login_link_signup": "註冊",
|
||||
"signup_already_have_account": "已有帳戶? 立即登入!",
|
||||
"login_disabled": "登入已停止",
|
||||
"action_bar_account": "帳戶",
|
||||
"action_bar_change_display_name": "改變顯示名稱",
|
||||
"action_bar_reservation_edit": "改變已保留",
|
||||
"action_bar_reservation_delete": "移除保留",
|
||||
"action_bar_reservation_limit_reached": "達到限制",
|
||||
"action_bar_profile_title": "簡介",
|
||||
"action_bar_profile_settings": "設置",
|
||||
"action_bar_sign_in": "登入",
|
||||
"action_bar_sign_up": "註冊",
|
||||
"nav_button_account": "帳戶",
|
||||
"nav_upgrade_banner_label": "升級到 ntfy 專業版",
|
||||
"nav_upgrade_banner_description": "保留主題,更多信息電郵及附件",
|
||||
"display_name_dialog_title": "改變顯示名稱",
|
||||
"display_name_dialog_description": "為主題新增在訂閱清單顯示的第二名稱, 這會令尋找複雜主題時更方便。",
|
||||
"display_name_dialog_placeholder": "顯示名稱",
|
||||
"reserve_dialog_checkbox_label": "保留主題及設置權限",
|
||||
"publish_dialog_progress_uploading_detail": "上載中 {{loaded}}/{{total}} ({{percent}}%) …",
|
||||
"publish_dialog_message_published": "已公佈通訊",
|
||||
"publish_dialog_attachment_limits_file_reached": "超出檔案限制 {fileSizeLimit}}",
|
||||
"publish_dialog_attachment_limits_quota_reached": "超出限制, 尚餘 {{remainingBytes}}",
|
||||
"publish_dialog_emoji_picker_show": "選擇 emoji",
|
||||
"publish_dialog_priority_min": "最低優先",
|
||||
"publish_dialog_priority_low": "較低優先",
|
||||
"publish_dialog_priority_default": "正常優先",
|
||||
"publish_dialog_priority_high": "高度優先",
|
||||
"publish_dialog_priority_max": "最高優先",
|
||||
"publish_dialog_base_url_label": "服務網址",
|
||||
"notifications_attachment_link_expired": "下載連結已過期",
|
||||
"notifications_attachment_file_image": "圖片文件",
|
||||
"notifications_attachment_image": "附件圖片",
|
||||
"notifications_attachment_file_video": "影片文件",
|
||||
"notifications_attachment_file_audio": "聲音文件",
|
||||
"notifications_attachment_file_app": "安卓應用程式",
|
||||
"notifications_attachment_file_document": "其他文件",
|
||||
"notifications_click_copy_url_title": "複製鏈結地址到剪貼板",
|
||||
"notifications_click_copy_url_button": "複製鏈結",
|
||||
"notifications_click_open_button": "打開鏈結",
|
||||
"action_bar_toggle_mute": "通知靜音/解除通知靜音",
|
||||
"nav_button_muted": "已暫停通知",
|
||||
"notifications_actions_not_supported": "網頁應用程序不支援此操作",
|
||||
"notifications_none_for_topic_title": "你尚未收到有關此主題的任何通知。",
|
||||
"notifications_none_for_any_title": "你尚未收到任何通知。",
|
||||
"notifications_none_for_any_description": "要向此主題發送通知,只需使用 PUT 或 POST 到主題鏈結即可。以下是使用你的主題的示例。",
|
||||
"notifications_no_subscriptions_title": "看起來你還未有任何訂閱",
|
||||
"notifications_example": "示例",
|
||||
"notifications_more_details": "有關更多資訊,請查看<websiteLink>網站</websiteLink>或<docsLink>文檔</docsLink>。",
|
||||
"notifications_loading": "正在加載通知……",
|
||||
"publish_dialog_title_topic": "發布到 {{topic}}",
|
||||
"publish_dialog_title_no_topic": "發布通知",
|
||||
"publish_dialog_progress_uploading": "正在上傳……",
|
||||
"publish_dialog_progress_uploading_detail": "正在上傳 {{loaded}}/{{total}} ({{percent}}%) ……",
|
||||
"publish_dialog_message_published": "已發布通知",
|
||||
"publish_dialog_attachment_limits_file_and_quota_reached": "超過 {{fileSizeLimit}} 文件限制和配額,剩餘 {{remainingBytes}}",
|
||||
"publish_dialog_emoji_picker_show": "選擇表情符號",
|
||||
"publish_dialog_priority_min": "最低優先級",
|
||||
"publish_dialog_priority_low": "低優先級",
|
||||
"publish_dialog_priority_default": "默認優先級",
|
||||
"publish_dialog_priority_high": "高優先級",
|
||||
"publish_dialog_priority_max": "最高優先級",
|
||||
"publish_dialog_topic_label": "主題名稱",
|
||||
"publish_dialog_topic_placeholder": "主題名稱,例如 phil_alerts",
|
||||
"publish_dialog_topic_reset": "重置主題",
|
||||
"publish_dialog_title_label": "標題",
|
||||
"publish_dialog_title_placeholder": "通訊標題,例如 Disk space alert",
|
||||
"publish_dialog_title_label": "主題",
|
||||
"publish_dialog_message_label": "訊息",
|
||||
"publish_dialog_message_placeholder": "這裏輸入訊息",
|
||||
"publish_dialog_tags_label": "標籤",
|
||||
"publish_dialog_click_placeholder": "通訊被點擊時到訪的網址",
|
||||
"publish_dialog_click_reset": "移除點擊網址",
|
||||
"publish_dialog_email_reset": "移除電郵轉發",
|
||||
"publish_dialog_chip_attach_file_label": "上載檔案",
|
||||
"publish_dialog_chip_delay_label": "延遲傳送",
|
||||
"publish_dialog_chip_topic_label": "更變主題",
|
||||
"publish_dialog_details_examples_description": "可以在 <docsLink>documentation</docsLink> 找到詳細的功能說明及例子。",
|
||||
"publish_dialog_checkbox_publish_another": "公佈更多",
|
||||
"publish_dialog_attached_file_title": "附件:",
|
||||
"publish_dialog_attached_file_filename_placeholder": "附件名稱",
|
||||
"subscribe_dialog_subscribe_use_another_label": "使用另一個伺服器",
|
||||
"subscribe_dialog_subscribe_base_url_label": "服務網址",
|
||||
"publish_dialog_message_placeholder": "在此輸入訊息",
|
||||
"publish_dialog_tags_label": "標記",
|
||||
"publish_dialog_priority_label": "優先級",
|
||||
"publish_dialog_base_url_label": "服務鏈結地址",
|
||||
"publish_dialog_base_url_placeholder": "服務鏈結地址,例如 https://example.com",
|
||||
"publish_dialog_click_label": "點擊鏈結地址",
|
||||
"publish_dialog_click_placeholder": "點擊通知時打開鏈結地址",
|
||||
"publish_dialog_email_placeholder": "將通知轉發到的地址,例如 phil@example.com",
|
||||
"publish_dialog_email_reset": "移除電子郵件轉發",
|
||||
"publish_dialog_filename_label": "文件名",
|
||||
"publish_dialog_filename_placeholder": "附件文件名",
|
||||
"publish_dialog_delay_label": "延期",
|
||||
"publish_dialog_other_features": "其它功能:",
|
||||
"publish_dialog_attach_placeholder": "使用鏈結地址附加文件,例如 https://f-droid.org/F-Droid.apk",
|
||||
"publish_dialog_delay_reset": "刪除延期投遞",
|
||||
"publish_dialog_attach_reset": "移除附件鏈結地址",
|
||||
"publish_dialog_chip_click_label": "點擊鏈結地址",
|
||||
"publish_dialog_chip_email_label": "轉發郵件",
|
||||
"publish_dialog_chip_attach_file_label": "本地文件附件",
|
||||
"publish_dialog_chip_topic_label": "變更主題",
|
||||
"publish_dialog_button_cancel_sending": "取消發送",
|
||||
"publish_dialog_checkbox_publish_another": "發布另一個",
|
||||
"publish_dialog_attached_file_title": "附件文件:",
|
||||
"publish_dialog_attached_file_filename_placeholder": "附件文件名",
|
||||
"publish_dialog_attached_file_remove": "刪除附件文件",
|
||||
"publish_dialog_drop_file_here": "將文件拖拽至此",
|
||||
"emoji_picker_search_placeholder": "查找表情符號",
|
||||
"emoji_picker_search_clear": "清除搜索",
|
||||
"subscribe_dialog_subscribe_title": "訂閱主題",
|
||||
"publish_dialog_chip_delay_label": "延期投遞",
|
||||
"publish_dialog_chip_attach_url_label": "鏈結附件地址",
|
||||
"subscribe_dialog_subscribe_use_another_label": "使用其他伺服器",
|
||||
"subscribe_dialog_subscribe_button_subscribe": "訂閱",
|
||||
"subscribe_dialog_login_title": "請登錄",
|
||||
"subscribe_dialog_login_description": "本主題受密碼保護,請輸入用戶名和密碼以訂閱。",
|
||||
"subscribe_dialog_login_username_label": "用戶名,例如 phil",
|
||||
"subscribe_dialog_login_password_label": "密碼",
|
||||
"common_back": "返回",
|
||||
"subscribe_dialog_login_button_login": "登入",
|
||||
"subscribe_dialog_error_user_not_authorized": "未授權 {{username}} 使用者",
|
||||
"subscribe_dialog_error_user_anonymous": "匿名",
|
||||
"prefs_notifications_title": "通知",
|
||||
"prefs_notifications_sound_title": "通知提示音",
|
||||
"prefs_notifications_sound_description_none": "收到通知時不播放任何聲音",
|
||||
"prefs_notifications_sound_description_some": "收到通知時播放 {{sound}} 聲音",
|
||||
"prefs_notifications_sound_no_sound": "靜音",
|
||||
"prefs_notifications_sound_play": "播放選中聲音",
|
||||
"prefs_notifications_min_priority_title": "最低優先級",
|
||||
"prefs_notifications_min_priority_description_x_or_higher": "僅顯示優先級為{{number}}({{name}})或以上的通知",
|
||||
"prefs_notifications_min_priority_description_max": "僅顯示最高優先級的通知",
|
||||
"prefs_notifications_min_priority_any": "任意優先級",
|
||||
"prefs_notifications_min_priority_low_and_higher": "低優先級或更高",
|
||||
"prefs_notifications_min_priority_default_and_higher": "默認優先級或更高",
|
||||
"prefs_notifications_min_priority_high_and_higher": "高優先級或更高",
|
||||
"prefs_notifications_min_priority_max_only": "僅最高優先級",
|
||||
"prefs_notifications_delete_after_never": "從不",
|
||||
"prefs_notifications_delete_after_one_month": "一個月後",
|
||||
"prefs_notifications_delete_after_one_week": "一周後",
|
||||
"prefs_notifications_delete_after_never_description": "永不自動刪除通知",
|
||||
"prefs_notifications_delete_after_three_hours_description": "三小時後自動刪除通知",
|
||||
"prefs_notifications_delete_after_one_day_description": "一天後自動刪除通知",
|
||||
"prefs_notifications_delete_after_one_week_description": "一周後自動刪除通知",
|
||||
"prefs_notifications_delete_after_one_month_description": "一個月後自動刪除通知",
|
||||
"prefs_notifications_web_push_enabled_description": "即使網頁程式未有運街亦會收到通知 (via Web Push)",
|
||||
"prefs_notifications_web_push_disabled_description": "當網頁程式在運行時將會收到通知 (透過 WebSocket)",
|
||||
"prefs_notifications_web_push_enabled": "己為 {{server}} 啟用",
|
||||
"prefs_notifications_web_push_disabled": "己暫用",
|
||||
"prefs_notifications_web_push_title": "背景通知",
|
||||
"prefs_users_title": "管理使用者",
|
||||
"prefs_users_description": "在此處新增/刪除受保護主題的使用者。請注意,使用者名和密碼將存儲在瀏覽器的本地存儲中。",
|
||||
"prefs_users_add_button": "新增使用者",
|
||||
"prefs_users_dialog_title_add": "新增使用者",
|
||||
"prefs_users_dialog_title_edit": "編輯使用者",
|
||||
"prefs_users_dialog_username_label": "使用者名,例如 phil",
|
||||
"prefs_users_dialog_password_label": "密碼",
|
||||
"common_cancel": "取消",
|
||||
"common_save": "保存",
|
||||
"prefs_appearance_title": "外觀",
|
||||
"prefs_appearance_language_title": "語言",
|
||||
"prefs_appearance_theme_title": "主題",
|
||||
"prefs_appearance_theme_system": "系統 (預設)",
|
||||
"prefs_appearance_theme_dark": "黑暗模式",
|
||||
"prefs_appearance_theme_light": "光亮模式",
|
||||
"priority_min": "最低",
|
||||
"priority_low": "低",
|
||||
"priority_default": "預設",
|
||||
"priority_high": "高",
|
||||
"priority_max": "最高",
|
||||
"error_boundary_title": "天啊,ntfy 崩潰了",
|
||||
"prefs_users_table_base_url_header": "服務連結地址",
|
||||
"prefs_users_dialog_base_url_label": "服務連結地址,例如 https://ntfy.sh",
|
||||
"error_boundary_button_copy_stack_trace": "複製堆疊追踪",
|
||||
"error_boundary_button_reload_ntfy": "重新加載 ntfy",
|
||||
"error_boundary_stack_trace": "堆疊追踪",
|
||||
"error_boundary_gathering_info": "收集更多資訊……",
|
||||
"error_boundary_unsupported_indexeddb_title": "不支援隱私瀏覽",
|
||||
"error_boundary_unsupported_indexeddb_description": "Ntfy Web應用程式需要IndexedDB才能運行,且你的瀏覽器在隱私瀏覽模式下不支援IndexedDB。<br/><br/>儘管這很不幸,但在隱私瀏覽模式下使用ntfy Web應用程式也沒有多大意義,因為所有東西都存儲在瀏覽器存儲中。你可以在<githubLink>本GitHub問題</githubLink>中閱讀有關它的更多資訊,或者在<discordLink>Discord</discordLink>或<matrixLink>Matrix</matrixLink>上與我們交談。",
|
||||
"message_bar_error_publishing": "發佈通知時出錯",
|
||||
"nav_button_settings": "設定",
|
||||
"notifications_delete": "刪除",
|
||||
"notifications_attachment_copy_url_title": "將附件中連結地址複製到剪貼板",
|
||||
"notifications_attachment_copy_url_button": "複製連結地址",
|
||||
"notifications_attachment_open_title": "轉到 {{url}}",
|
||||
"notifications_actions_http_request_title": "發送 HTTP {{method}} 到 {{url}}",
|
||||
"notifications_actions_failed_notification": "通知失敗",
|
||||
"notifications_actions_open_url_title": "轉到 {{url}}",
|
||||
"notifications_none_for_topic_description": "要向此主題發送通知,只需使用 PUT 或 POST 到主題連結即可。",
|
||||
"subscribe_dialog_subscribe_topic_placeholder": "主題名,例如 phil_alerts",
|
||||
"notifications_no_subscriptions_description": "點擊 \"{{linktext}}\" 連結以建立或訂閱主題。之後,你可以使用 PUT 或 POST 發送訊息,你將在這裡收到通知。",
|
||||
"publish_dialog_attachment_limits_file_reached": "超過 {{fileSizeLimit}} 文件限制",
|
||||
"publish_dialog_title_placeholder": "主題標題,例如 磁碟空間警告",
|
||||
"publish_dialog_email_label": "電子郵件",
|
||||
"publish_dialog_button_send": "發送",
|
||||
"publish_dialog_checkbox_markdown": "格式化為 Markdown",
|
||||
"publish_dialog_attachment_limits_quota_reached": "超過配額,剩餘 {{remainingBytes}}",
|
||||
"publish_dialog_attach_label": "附件連結地址",
|
||||
"publish_dialog_click_reset": "移除點擊連結地址",
|
||||
"publish_dialog_button_cancel": "取消",
|
||||
"subscribe_dialog_subscribe_button_cancel": "取消",
|
||||
"subscribe_dialog_subscribe_base_url_label": "服務地址地址",
|
||||
"subscribe_dialog_subscribe_use_another_background_info": "當網頁程式未開啟, 將不會收到來自其他伺服器的通知",
|
||||
"prefs_notifications_min_priority_description_any": "顯示所有通知,無論優先級如何",
|
||||
"prefs_notifications_delete_after_title": "刪除通知",
|
||||
"prefs_notifications_delete_after_three_hours": "三小時後",
|
||||
"prefs_users_delete_button": "刪除用戶",
|
||||
"prefs_users_table_user_header": "用戶",
|
||||
"common_add": "新增",
|
||||
"prefs_notifications_delete_after_one_day": "一天後",
|
||||
"error_boundary_description": "這顯然不應該發生。對此非常抱歉。<br/>如果你有時間,請<githubLink>在GitHub</githubLink>上報告,或通過<discordLink>Discord</discordLink>或<matrixLink>Matrix</matrixLink>告訴我們。",
|
||||
"prefs_users_table": "用戶表",
|
||||
"prefs_users_edit_button": "編輯用戶",
|
||||
"publish_dialog_tags_placeholder": "英文逗號分隔標記列表,例如 warning, srv1-backup",
|
||||
"publish_dialog_details_examples_description": "有關所有發送功能的範例和詳細說明,請參閱<docsLink>文檔</docsLink>。",
|
||||
"subscribe_dialog_subscribe_description": "主題可能不受密碼保護,因此請選擇一個不容易被猜中的名字。訂閱後,你可以使用 PUT/POST 通知。",
|
||||
"publish_dialog_delay_placeholder": "延期投遞,例如 {{unixTimestamp}}、{{relativeTime}}或「{{naturalLanguage}}」(僅限英語)",
|
||||
"account_usage_basis_ip_description": "此帳戶的使用統計資訊和限制基於你的 IP 地址,因此可能會與其他用戶共享。上面顯示的限制是基於現有速率限制的近似值。",
|
||||
"account_usage_cannot_create_portal_session": "無法打開計費門戶",
|
||||
"account_delete_title": "刪除帳戶",
|
||||
"account_delete_description": "永久刪除你的帳戶",
|
||||
"signup_error_username_taken": "用戶名 {{username}} 已被取用",
|
||||
"signup_error_creation_limit_reached": "已達到帳戶創建限制",
|
||||
"login_title": "請登錄你的 ntfy 帳戶",
|
||||
"action_bar_change_display_name": "更改顯示名稱",
|
||||
"action_bar_reservation_add": "保留主題",
|
||||
"action_bar_reservation_delete": "移除保留",
|
||||
"action_bar_reservation_limit_reached": "達到限制",
|
||||
"action_bar_profile_title": "個人資料",
|
||||
"action_bar_profile_settings": "設定",
|
||||
"action_bar_profile_logout": "登出",
|
||||
"action_bar_mute_notifications": "靜音",
|
||||
"action_bar_sign_in": "登錄",
|
||||
"action_bar_sign_up": "註冊",
|
||||
"nav_button_account": "帳戶",
|
||||
"nav_upgrade_banner_label": "升級到 ntfy Pro",
|
||||
"nav_upgrade_banner_description": "保留主題,更多訊息和郵件,以及更大的附件",
|
||||
"alert_not_supported_context_description": "通知僅支援 HTTPS。這是 <mdnLink>Notifications API</mdnLink> 的限制。",
|
||||
"display_name_dialog_title": "更改顯示名稱",
|
||||
"display_name_dialog_description": "為訂閱列表中顯示的主題設置一個替代名稱。這有助於更輕鬆地識別名稱複雜的主題。",
|
||||
"display_name_dialog_placeholder": "顯示名稱",
|
||||
"reserve_dialog_checkbox_label": "保留主題並配置訪問",
|
||||
"subscribe_dialog_subscribe_button_generate_topic_name": "生成名稱",
|
||||
"subscribe_dialog_login_title": "需要登入",
|
||||
"subscribe_dialog_login_username_label": "用戶名稱,例如 phil",
|
||||
"subscribe_dialog_error_topic_already_reserved": "主題已被保留",
|
||||
"account_basics_username_description": "嘿,那是你 ❤",
|
||||
"account_basics_password_description": "更改你的帳戶密碼",
|
||||
"account_basics_password_dialog_title": "更改密碼",
|
||||
"account_basics_password_dialog_current_password_label": "當前密碼",
|
||||
"account_basics_password_dialog_new_password_label": "新密碼",
|
||||
"account_basics_password_dialog_confirm_password_label": "確認密碼",
|
||||
"account_basics_password_dialog_button_submit": "更改密碼",
|
||||
"account_basics_password_dialog_current_password_incorrect": "密碼錯誤",
|
||||
"account_usage_title": "使用量",
|
||||
"account_usage_of_limit": "{{limit}} 的",
|
||||
"account_usage_unlimited": "無限",
|
||||
"account_usage_limits_reset_daily": "使用限制每天午夜 (UTC) 重置",
|
||||
"account_basics_tier_title": "帳戶類型",
|
||||
"account_basics_tier_description": "你帳戶的權限級別",
|
||||
"account_basics_tier_admin": "管理員",
|
||||
"account_basics_tier_admin_suffix_with_tier": "(有 {{tier}} 等級)",
|
||||
"account_basics_tier_admin_suffix_no_tier": "(無等級)",
|
||||
"account_basics_tier_basic": "基礎版",
|
||||
"account_basics_tier_free": "免費",
|
||||
"account_basics_tier_upgrade_button": "升級到專業版",
|
||||
"account_basics_tier_change_button": "改變",
|
||||
"account_basics_tier_paid_until": "訂閱已支付至 {{date}},並將自動續訂",
|
||||
"account_basics_tier_manage_billing_button": "管理計費",
|
||||
"account_usage_messages_title": "已發布訊息",
|
||||
"account_usage_emails_title": "已發送電子郵件",
|
||||
"account_usage_reservations_title": "保留主題",
|
||||
"account_usage_reservations_none": "此帳戶沒有保留主題",
|
||||
"account_usage_attachment_storage_title": "附件存儲",
|
||||
"account_usage_attachment_storage_description": "每個文件 {{filesize}},在 {{expiry}} 後刪除",
|
||||
"account_upgrade_dialog_button_pay_now": "立即付款並訂閱",
|
||||
"account_upgrade_dialog_button_cancel_subscription": "取消訂閱",
|
||||
"account_upgrade_dialog_button_update_subscription": "更新訂閱",
|
||||
"account_tokens_dialog_title_create": "創建訪問令牌",
|
||||
"account_tokens_dialog_title_edit": "編輯訪問令牌",
|
||||
"account_tokens_dialog_title_delete": "刪除訪問令牌",
|
||||
"account_tokens_dialog_button_cancel": "取消",
|
||||
"account_tokens_dialog_expires_label": "訪問令牌過期於",
|
||||
"account_tokens_dialog_expires_unchanged": "保持過期日期不變",
|
||||
"account_tokens_dialog_expires_x_hours": "令牌在 {{hours}} 小時後過期",
|
||||
"account_tokens_dialog_expires_x_days": "令牌在 {{days}} 天後過期",
|
||||
"account_tokens_dialog_expires_never": "令牌永不過期",
|
||||
"account_tokens_delete_dialog_title": "刪除訪問令牌",
|
||||
"account_tokens_delete_dialog_description": "在刪除訪問令牌之前,請確保沒有應用程序或腳本正在活躍使用它。 <strong>此操作無法撤銷</strong>。",
|
||||
"account_tokens_delete_dialog_submit_button": "永久删除令牌",
|
||||
"prefs_users_description_no_sync": "用戶和密碼不會同步到你的賬戶。",
|
||||
"prefs_users_table_cannot_delete_or_edit": "無法刪除或編輯已登錄用戶",
|
||||
"prefs_reservations_title": "保留主題",
|
||||
"prefs_reservations_description": "你可以在此處保留主題名稱供個人使用。保留主題使你擁有該主題的所有權,並允許你為其他用戶定義對該主題的訪問權限。",
|
||||
"prefs_reservations_limit_reached": "你已達到保留主題限制。",
|
||||
"prefs_reservations_add_button": "新增保留主題",
|
||||
"prefs_reservations_edit_button": "編輯主題訪問",
|
||||
"prefs_reservations_delete_button": "重置主題訪問",
|
||||
"prefs_reservations_table": "保留主題表格",
|
||||
"prefs_reservations_table_topic_header": "主題",
|
||||
"prefs_reservations_table_access_header": "訪問",
|
||||
"prefs_reservations_table_everyone_deny_all": "只有我可以發佈和訂閱",
|
||||
"prefs_reservations_table_everyone_read_only": "我可以發佈和訂閱,每個人都可以訂閱",
|
||||
"prefs_reservations_table_everyone_write_only": "我可以發佈和訂閱,每個人都可以發佈",
|
||||
"prefs_reservations_table_everyone_read_write": "每個人都可以發佈和訂閱",
|
||||
"prefs_reservations_table_not_subscribed": "未訂閱",
|
||||
"prefs_reservations_table_click_to_subscribe": "點擊以訂閱",
|
||||
"prefs_reservations_dialog_title_add": "保留主題",
|
||||
"prefs_reservations_dialog_title_edit": "編輯保留主題",
|
||||
"prefs_reservations_dialog_title_delete": "刪除主題保留",
|
||||
"prefs_reservations_dialog_description": "保留主題使你擁有該主題的所有權,並允許你為其他用戶定義對該主題的訪問權限。",
|
||||
"prefs_reservations_dialog_topic_label": "主題",
|
||||
"prefs_reservations_dialog_access_label": "訪問",
|
||||
"reservation_delete_dialog_description": "刪除保留會放棄對該主題的所有權,並允許其他人保留它。你可以保留或刪除現有郵件和附件。",
|
||||
"reservation_delete_dialog_action_keep_title": "保留緩存的郵件和附件",
|
||||
"reservation_delete_dialog_action_keep_description": "緩存在伺服器上的訊息和附件將對知道主題名稱的人公開可見。",
|
||||
"reservation_delete_dialog_action_delete_title": "刪除緩存的郵件和附件",
|
||||
"reservation_delete_dialog_action_delete_description": "緩存的郵件和附件將被永久刪除。此操作無法撤銷。",
|
||||
"reservation_delete_dialog_submit_button": "刪除保留",
|
||||
"account_delete_dialog_description": "這將永久刪除你的帳戶,包括存儲在伺服器上的所有數據。刪除後,你的用戶名將在 7 天內不可用。如果你真的想繼續,請在下面的框中使用你的密碼作確認。",
|
||||
"account_delete_dialog_label": "密碼",
|
||||
"account_delete_dialog_button_cancel": "取消",
|
||||
"account_delete_dialog_button_submit": "永久刪除帳戶",
|
||||
"account_delete_dialog_billing_warning": "刪除你的帳戶也會立即取消你的計費訂閱。你將無法再訪問計費儀錶板。",
|
||||
"account_upgrade_dialog_title": "更改帳戶等級",
|
||||
"account_upgrade_dialog_cancel_warning": "這將<strong>取消你的訂閱</strong>,並在 {{date}} 降級你的帳戶。在那一天,主題保留以及緩存在伺服器上的訊息<strong>將被刪除</strong>。",
|
||||
"account_upgrade_dialog_proration_info": "<strong>按比例分配</strong>:在付費計劃之間升級時,差價將被<strong>立刻收取</strong>。在降級到較低級別時,餘額將被用於支付未來的賬單周期。",
|
||||
"account_upgrade_dialog_reservations_warning_one": "所選等級允許的保留主題少於當前等級。在更改你的等級之前,<strong>請至少刪除 1 項保留</strong>。你可以在<Link>設置</Link>中刪除保留。",
|
||||
"account_upgrade_dialog_reservations_warning_other": "所選等級允許的保留主題少於當前等級。在更改你的等級之前,<strong>請至少刪除 {{count}} 項保留</strong>。你可以在<Link>設置</Link>中刪除保留。",
|
||||
"account_upgrade_dialog_tier_features_reservations_other": "保留 {{reservations}} 條主題",
|
||||
"account_upgrade_dialog_tier_features_messages_other": "每日 {{messages}} 條訊息",
|
||||
"account_upgrade_dialog_tier_features_emails_other": "每日 {{emails}} 條郵件",
|
||||
"account_upgrade_dialog_tier_features_attachment_file_size": "每個文件 {{filesize}} ",
|
||||
"signup_form_confirm_password": "確認密碼",
|
||||
"signup_form_button_submit": "註冊",
|
||||
"signup_form_toggle_password_visibility": "切換密碼可見性",
|
||||
"signup_title": "創建一個 ntfy 帳戶",
|
||||
"signup_form_username": "用戶名",
|
||||
"signup_form_password": "密碼",
|
||||
"signup_already_have_account": "已有帳戶?登錄!",
|
||||
"signup_disabled": "註冊已禁用",
|
||||
"login_form_button_submit": "登錄",
|
||||
"login_link_signup": "註冊",
|
||||
"login_disabled": "登錄已禁用",
|
||||
"action_bar_account": "帳戶",
|
||||
"action_bar_reservation_edit": "更改保留",
|
||||
"subscribe_dialog_error_topic_already_reserved": "主題已保留",
|
||||
"account_basics_title": "帳戶",
|
||||
"account_basics_username_title": "用戶名稱",
|
||||
"account_basics_username_description": "這就是你了❤",
|
||||
"account_basics_username_title": "用戶名",
|
||||
"account_basics_username_admin_tooltip": "你是管理員",
|
||||
"account_basics_password_title": "密碼",
|
||||
"account_basics_password_description": "更變你的密碼",
|
||||
"account_basics_password_dialog_title": "更變密碼",
|
||||
"account_basics_password_dialog_new_password_label": "新的密碼",
|
||||
"account_basics_password_dialog_confirm_password_label": "確認密碼",
|
||||
"account_basics_password_dialog_button_submit": "更變密碼",
|
||||
"account_usage_unlimited": "無限制",
|
||||
"account_usage_title": "已經使用",
|
||||
"account_usage_limits_reset_daily": "使用限制每天午夜重置",
|
||||
"account_basics_tier_title": "帳戶類型",
|
||||
"account_basics_tier_description": "你的能量值",
|
||||
"account_basics_tier_admin": "管理員",
|
||||
"account_basics_tier_admin_suffix_with_tier": "(擁有 {{tier}})",
|
||||
"account_basics_tier_admin_suffix_no_tier": "(無層)",
|
||||
"account_basics_tier_basic": "基礎",
|
||||
"account_basics_tier_free": "免費",
|
||||
"account_basics_tier_upgrade_button": "升級至專業版",
|
||||
"publish_dialog_email_placeholder": "轉發到電郵,例如 phil@example.com",
|
||||
"subscribe_dialog_subscribe_topic_placeholder": "主題名稱,例如 phil_alerts",
|
||||
"publish_dialog_attached_file_remove": "移除附件",
|
||||
"subscribe_dialog_subscribe_description": "主題可能不受到密碼保護, 所以盡量選擇一個不會容易被猜中的主題名稱。 一旦已訂閱,你能夠 PUT/POST 通訊。",
|
||||
"subscribe_dialog_login_description": "這個主題受密碼保護,請輸入用戶名稱及密碼以訂閱主題。",
|
||||
"account_basics_password_dialog_current_password_label": "現在的密碼",
|
||||
"account_basics_password_dialog_current_password_incorrect": "密碼不正確",
|
||||
"account_basics_tier_change_button": "更變",
|
||||
"common_add": "新增",
|
||||
"signup_form_confirm_password": "確認密碼",
|
||||
"publish_dialog_drop_file_here": "拖曳檔案到此",
|
||||
"account_basics_tier_payment_overdue": "你的付款已逾期。請更新你的付款方式,否則你的帳戶將很快被降級。",
|
||||
"account_basics_tier_canceled_subscription": "你的訂閱已取消,並將在 {{date}} 降級為免費帳戶。",
|
||||
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} 總存儲空間",
|
||||
"account_upgrade_dialog_tier_selected_label": "已選",
|
||||
"account_upgrade_dialog_tier_current_label": "當前",
|
||||
"account_upgrade_dialog_button_cancel": "取消",
|
||||
"account_upgrade_dialog_button_redirect_signup": "立即註冊",
|
||||
"account_tokens_title": "訪問令牌",
|
||||
"account_tokens_description": "通過 ntfy API 發布和訂閱時使用訪問令牌,因此你不必發送你的帳戶憑證。查看<Link>文檔</Link>以了解更多資訊。",
|
||||
"account_tokens_table_token_header": "令牌",
|
||||
"account_tokens_table_label_header": "標籤",
|
||||
"account_tokens_table_last_access_header": "最後訪問",
|
||||
"account_tokens_table_expires_header": "過期",
|
||||
"account_tokens_table_never_expires": "永不過期",
|
||||
"account_tokens_table_current_session": "當前瀏覽器會話",
|
||||
"common_copy_to_clipboard": "複製到剪貼板",
|
||||
"account_tokens_table_copied_to_clipboard": "已複製訪問令牌",
|
||||
"account_tokens_table_cannot_delete_or_edit": "無法編輯或刪除當前會話令牌",
|
||||
"account_tokens_table_create_token_button": "創建訪問令牌",
|
||||
"account_tokens_table_last_origin_tooltip": "於IP地址 {{ip}},點擊查找",
|
||||
"account_tokens_dialog_label": "標籤,例如:Radarr 通知",
|
||||
"account_tokens_dialog_button_create": "創建令牌",
|
||||
"account_tokens_dialog_button_update": "更新令牌",
|
||||
"account_basics_tier_interval_monthly": "每月",
|
||||
"common_copy_to_clipboard": "複製到剪貼簿",
|
||||
"publish_dialog_call_label": "電話",
|
||||
"publish_dialog_call_reset": "移除電話",
|
||||
"publish_dialog_chip_call_label": "電話",
|
||||
"account_usage_reservations_none": "此帳戶沒有預留主題",
|
||||
"account_usage_attachment_storage_title": "附件容量",
|
||||
"account_basics_tier_canceled_subscription": "你的付費訂閱已取消,並於 {{date}} 下調為免費帳戶。",
|
||||
"account_usage_messages_title": "已發佈的信息",
|
||||
"publish_dialog_chip_call_no_verified_numbers_tooltip": "沒有已驗證的電話號碼",
|
||||
"account_basics_tier_interval_yearly": "每年",
|
||||
"account_usage_emails_title": "已發送電郵",
|
||||
"account_usage_attachment_storage_description": "每個檔案約 {{filesize}},將於 {{expiry}} 後刪除",
|
||||
"publish_dialog_attachment_limits_file_and_quota_reached": "已超過 {{fileSizeLimit}} 檔案上限,尚餘 {{remainingBytes}}",
|
||||
"account_basics_tier_paid_until": "已付費訂閱至 {{date}} 並自動續期",
|
||||
"account_basics_tier_payment_overdue": "你的費用已逾期。請更新付款方法,否則你的戶口等級將會下調。",
|
||||
"publish_dialog_call_item": "致電 {{number}}",
|
||||
"account_basics_tier_manage_billing_button": "管理付款方式"
|
||||
"account_upgrade_dialog_interval_monthly": "每月",
|
||||
"account_upgrade_dialog_interval_yearly": "每年",
|
||||
"account_upgrade_dialog_interval_yearly_discount_save": "節省 {{discount}}%",
|
||||
"account_upgrade_dialog_interval_yearly_discount_save_up_to": "節省高達 {{discount}}%",
|
||||
"account_upgrade_dialog_tier_features_no_reservations": "無保留主題",
|
||||
"account_upgrade_dialog_tier_price_per_month": "月",
|
||||
"account_upgrade_dialog_tier_price_billed_monthly": "{{price}} 每年。按月計費。",
|
||||
"account_upgrade_dialog_tier_price_billed_yearly": "{{價格}} 按年計費。節省 {{save}}。",
|
||||
"account_upgrade_dialog_billing_contact_email": "有關賬單問題,請直接<Link>聯繫我們 </Link>。",
|
||||
"account_upgrade_dialog_billing_contact_website": "有關賬單問題,請參考我們的<Link>網站 </Link>。",
|
||||
"publish_dialog_call_item": "撥打電話 {{number}}",
|
||||
"publish_dialog_call_label": "撥號",
|
||||
"publish_dialog_chip_call_label": "撥號",
|
||||
"publish_dialog_chip_call_no_verified_numbers_tooltip": "未驗證的電話號碼",
|
||||
"account_basics_phone_numbers_title": "電話號碼",
|
||||
"account_basics_phone_numbers_description": "電話通知",
|
||||
"account_basics_phone_numbers_dialog_description": "要使用來電通知功能,你需要新增並驗證至少一個電話號碼。可以通過短信或電話驗證。",
|
||||
"account_basics_phone_numbers_dialog_code_label": "驗證碼",
|
||||
"account_basics_phone_numbers_dialog_code_placeholder": "例如:123456",
|
||||
"account_basics_phone_numbers_dialog_check_verification_button": "確認碼",
|
||||
"account_basics_phone_numbers_dialog_channel_sms": "短信",
|
||||
"account_basics_phone_numbers_dialog_channel_call": "撥打",
|
||||
"publish_dialog_call_reset": "清空撥號",
|
||||
"account_basics_phone_numbers_no_phone_numbers_yet": "無可執行的電話號碼",
|
||||
"account_basics_phone_numbers_dialog_title": "新增電話號碼",
|
||||
"account_basics_phone_numbers_copied_to_clipboard": "電話號碼已複製到剪貼板",
|
||||
"account_basics_phone_numbers_dialog_number_label": "電話號碼",
|
||||
"account_basics_phone_numbers_dialog_number_placeholder": "例如:+1222333444",
|
||||
"account_usage_calls_title": "已撥打電話",
|
||||
"account_usage_calls_none": "此帳號無法撥打電話",
|
||||
"account_upgrade_dialog_tier_features_reservations_one": "保留一條主題",
|
||||
"account_upgrade_dialog_tier_features_emails_one": "每日一封郵件",
|
||||
"account_upgrade_dialog_tier_features_calls_one": "每日一通電話",
|
||||
"account_basics_phone_numbers_dialog_verify_button_sms": "發送資訊",
|
||||
"account_basics_phone_numbers_dialog_verify_button_call": "撥打電話",
|
||||
"account_upgrade_dialog_tier_features_messages_one": "每日一條訊息",
|
||||
"account_upgrade_dialog_tier_features_calls_other": "每日{{calls}} 通電話",
|
||||
"account_upgrade_dialog_tier_features_no_calls": "沒有電話",
|
||||
"web_push_subscription_expiring_title": "通知會被暫停",
|
||||
"web_push_subscription_expiring_body": "開啟ntfy以繼續接收通知",
|
||||
"web_push_unknown_notification_title": "接收到不明通知",
|
||||
"web_push_unknown_notification_body": "你可能需要開啟網頁來更新ntfy"
|
||||
}
|
||||
|
||||
@@ -7,8 +7,7 @@ import { clientsClaim } from "workbox-core";
|
||||
import { dbAsync } from "../src/app/db";
|
||||
|
||||
import { toNotificationParams, icon, badge } from "../src/app/notificationUtils";
|
||||
|
||||
import i18n from "../src/app/i18n";
|
||||
import initI18n from "../src/app/i18n";
|
||||
|
||||
/**
|
||||
* General docs for service workers and PWAs:
|
||||
@@ -67,8 +66,10 @@ const handlePushMessage = async (data) => {
|
||||
* Handle a received web push subscription expiring.
|
||||
*/
|
||||
const handlePushSubscriptionExpiring = async (data) => {
|
||||
await self.registration.showNotification(i18n.t("web_push_subscription_expiring_title"), {
|
||||
body: i18n.t("web_push_subscription_expiring_body"),
|
||||
const t = await initI18n();
|
||||
|
||||
await self.registration.showNotification(t("web_push_subscription_expiring_title"), {
|
||||
body: t("web_push_subscription_expiring_body"),
|
||||
icon,
|
||||
data,
|
||||
badge,
|
||||
@@ -80,8 +81,10 @@ const handlePushSubscriptionExpiring = async (data) => {
|
||||
* permission can be revoked by the browser.
|
||||
*/
|
||||
const handlePushUnknown = async (data) => {
|
||||
await self.registration.showNotification(i18n.t("web_push_unknown_notification_title"), {
|
||||
body: i18n.t("web_push_unknown_notification_body"),
|
||||
const t = await initI18n();
|
||||
|
||||
await self.registration.showNotification(t("web_push_unknown_notification_title"), {
|
||||
body: t("web_push_unknown_notification_body"),
|
||||
icon,
|
||||
data,
|
||||
badge,
|
||||
@@ -107,6 +110,8 @@ const handlePush = async (data) => {
|
||||
* This is also called when the user clicks on an action button.
|
||||
*/
|
||||
const handleClick = async (event) => {
|
||||
const t = await initI18n();
|
||||
|
||||
const clients = await self.clients.matchAll({ type: "window" });
|
||||
|
||||
const rootUrl = new URL(self.location.origin);
|
||||
@@ -147,7 +152,7 @@ const handleClick = async (event) => {
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[ServiceWorker] Error performing http action", e);
|
||||
self.registration.showNotification(`${i18n.t("notifications_actions_failed_notification")}: ${action.label} (${action.action})`, {
|
||||
self.registration.showNotification(`${t("notifications_actions_failed_notification")}: ${action.label} (${action.action})`, {
|
||||
body: e.message,
|
||||
icon,
|
||||
badge,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import i18n from "i18next";
|
||||
import i18next from "i18next";
|
||||
import Backend from "i18next-http-backend";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
@@ -11,19 +11,20 @@ import { initReactI18next } from "react-i18next";
|
||||
// See example project here:
|
||||
// https://github.com/i18next/react-i18next/tree/master/example/react
|
||||
|
||||
i18n
|
||||
.use(Backend)
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
fallbackLng: "en",
|
||||
debug: true,
|
||||
interpolation: {
|
||||
escapeValue: false, // not needed for react as it escapes by default
|
||||
},
|
||||
backend: {
|
||||
loadPath: "/static/langs/{{lng}}.json",
|
||||
},
|
||||
});
|
||||
const initI18n = () =>
|
||||
i18next
|
||||
.use(Backend)
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
fallbackLng: "en",
|
||||
debug: true,
|
||||
interpolation: {
|
||||
escapeValue: false, // not needed for react as it escapes by default
|
||||
},
|
||||
backend: {
|
||||
loadPath: "/static/langs/{{lng}}.json",
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
export default initI18n;
|
||||
|
||||
@@ -35,7 +35,7 @@ export const formatMessage = (m) => {
|
||||
};
|
||||
|
||||
const imageRegex = /\.(png|jpe?g|gif|webp)$/i;
|
||||
const isImage = (attachment) => {
|
||||
export const isImage = (attachment) => {
|
||||
if (!attachment) return false;
|
||||
|
||||
// if there's a type, only take that into account
|
||||
|
||||
@@ -20,10 +20,12 @@ import Messaging from "./Messaging";
|
||||
import Login from "./Login";
|
||||
import Signup from "./Signup";
|
||||
import Account from "./Account";
|
||||
import "../app/i18n"; // Translations!
|
||||
import initI18n from "../app/i18n"; // Translations!
|
||||
import prefs, { THEME } from "../app/Prefs";
|
||||
import RTLCacheProvider from "./RTLCacheProvider";
|
||||
|
||||
initI18n();
|
||||
|
||||
export const AccountContext = createContext(null);
|
||||
|
||||
const darkModeEnabled = (prefersDarkMode, themePreference) => {
|
||||
|
||||
@@ -27,7 +27,7 @@ import { useOutletContext } from "react-router-dom";
|
||||
import { useRemark } from "react-remark";
|
||||
import styled from "@emotion/styled";
|
||||
import { formatBytes, formatShortDateTime, maybeActionErrors, openUrl, shortUrl, topicShortUrl, unmatchedTags } from "../app/utils";
|
||||
import { formatMessage, formatTitle } from "../app/notificationUtils";
|
||||
import { formatMessage, formatTitle, isImage } from "../app/notificationUtils";
|
||||
import { LightboxBackdrop, Paragraph, VerticallyCenteredContainer } from "./styles";
|
||||
import subscriptionManager from "../app/SubscriptionManager";
|
||||
import priority1 from "../img/priority-1.svg";
|
||||
@@ -346,7 +346,7 @@ const Attachment = (props) => {
|
||||
const { attachment } = props;
|
||||
const expired = attachment.expires && attachment.expires < Date.now() / 1000;
|
||||
const expires = attachment.expires && attachment.expires > Date.now() / 1000;
|
||||
const displayableImage = !expired && attachment.type && attachment.type.startsWith("image/");
|
||||
const displayableImage = !expired && isImage(attachment);
|
||||
|
||||
// Unexpired image
|
||||
if (displayableImage) {
|
||||
|
||||
@@ -571,7 +571,8 @@ const Language = () => {
|
||||
<MenuItem value="id">Bahasa Indonesia</MenuItem>
|
||||
<MenuItem value="bg">Български</MenuItem>
|
||||
<MenuItem value="cs">Čeština</MenuItem>
|
||||
<MenuItem value="zh_Hans">中文</MenuItem>
|
||||
<MenuItem value="zh_Hant">繁體中文</MenuItem>
|
||||
<MenuItem value="zh_Hans">简体中文</MenuItem>
|
||||
<MenuItem value="da">Dansk</MenuItem>
|
||||
<MenuItem value="de">Deutsch</MenuItem>
|
||||
<MenuItem value="es">Español</MenuItem>
|
||||
|
||||
Reference in New Issue
Block a user