Compare commits

..

42 Commits

Author SHA1 Message Date
binwiederhier
9015b27803 Release notes 2023-08-18 22:47:36 +02:00
binwiederhier
a5f0670f7f ACLs and underscores, resolves #840 2023-08-18 22:44:52 +02:00
binwiederhier
d7db395016 Release note details 2023-08-17 23:06:52 +02:00
binwiederhier
99eef493d2 Thank you @spirossi for your sponsorship 2023-08-17 22:23:06 +02:00
binwiederhier
0d395249ff Thank you @eenturk for your donation 2023-08-17 22:21:06 +02:00
binwiederhier
5cf1da974a Thank you @skorokithakis for your donation 2023-08-17 22:20:29 +02:00
binwiederhier
2f0ec88f40 Release bump 2023-08-17 22:17:07 +02:00
binwiederhier
d9d3c4a724 Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web 2023-08-17 22:16:31 +02:00
binwiederhier
bc4d4f424a Pin go-smtp v0.17.0 2023-08-17 22:05:51 +02:00
binwiederhier
67459650d4 Release notes 2023-08-17 21:59:24 +02:00
binwiederhier
c31bce1e2d Merge branch 'main' of github.com:binwiederhier/ntfy 2023-08-17 21:43:05 +02:00
binwiederhier
3e3b556108 Fix excess token deletion bug 2023-08-17 21:42:40 +02:00
Philipp C. Heckel
723daf9497 Merge pull request #819 from nisbet-hubbard/patch-1
Tweak httpd config to use less resources
2023-08-17 21:37:00 +02:00
Erik S
f77958fc35 Translated using Weblate (Russian)
Currently translated at 95.5% (365 of 382 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ru/
2023-08-16 06:48:49 +02:00
CaptB
ea9f2c6e35 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (382 of 382 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/zh_Hans/
2023-08-11 12:51:15 +02:00
Philipp C. Heckel
7d20238423 Merge pull request #820 from nihalgonsalves/ng/display-external-images
fix: check extension to display external images
2023-08-08 20:47:43 -04:00
Philipp C. Heckel
31131db756 Merge pull request #834 from wunter8/829-empty-userpass-override
fixes #829
2023-08-08 20:44:32 -04:00
Shjosan
17e634c563 Translated using Weblate (Swedish)
Currently translated at 100.0% (382 of 382 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/sv/
2023-08-08 11:50:23 +02:00
wunter8
a7dc3d84e0 fix typo 2023-08-07 22:59:24 -06:00
Hunter Kehoe
b80aec90d0 fixes #829 2023-08-06 22:44:55 -06:00
Nguyen Loc
8544733048 Added translation using Weblate (Vietnamese) 2023-08-07 03:58:56 +02:00
binwiederhier
140fdcca81 Thank you @bmcgonag for your sponsorship 2023-08-05 21:13:48 -04:00
Christian Meis
2e08c48742 Translated using Weblate (German)
Currently translated at 100.0% (382 of 382 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2023-08-04 17:07:34 +02:00
binwiederhier
4800bb05d2 Thank you @darkmattercoder for your donation 2023-08-03 10:49:04 -04:00
Nihal Gonsalves
384cabede5 feat: check extension to display external images 2023-07-14 13:10:24 +02:00
nisbet-hubbard
4e9eeb1fa1 Add missing note on log file permissions 2023-07-12 20:24:57 +08:00
nisbet-hubbard
a534cc9eca Add server.yml ex. when using proxy
This would help inexperienced sysadmins who may not realise that since TLS terminates at proxy, ntfy is actually listening on a TCP socket that’s using http rather than https.
2023-07-12 20:00:48 +08:00
nisbet-hubbard
e52132c85b Use mod_alias for redirection
It’s a less resource-intensive alternative to mod_rewrite.
2023-07-12 19:48:51 +08:00
nisbet-hubbard
76667ffcf9 Use mod_proxy_http for websocket upgrade
mod_proxy_wstunnel is deprecated as of httpd 2.4.47. It also uses more resources since it relies on mod_rewrite.

See https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#protoupgrade.
2023-07-12 18:18:48 +08:00
binwiederhier
8ba4b72b37 Changelog 2023-07-11 19:46:10 -04:00
Philipp C. Heckel
81e1417ce5 Merge pull request #817 from nihalgonsalves/ng/fix-web-push-i18n
fix(web-push): re-init i18n on each sw message
2023-07-11 19:43:12 -04:00
binwiederhier
c1576b5b19 Blog posts 2023-07-11 09:34:47 -04:00
Nihal Gonsalves
86cc3b9607 chore(build): bump Dockerfile-build go version 2023-07-10 20:14:29 +02:00
Nihal Gonsalves
c7f85e6283 fix(web-push): re-init i18n on each sw message 2023-07-10 20:10:45 +02:00
binwiederhier
6a93dc9d54 Bump packages 2023-07-09 07:51:33 -04:00
binwiederhier
dfd08b337c Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web 2023-07-09 07:50:34 -04:00
binwiederhier
2d1f2f319f Changelog, CLI fix 2023-07-09 07:50:00 -04:00
binwiederhier
68f82b9182 Fix wording in tests 2023-07-09 07:36:36 -04:00
109247019824
8d9fa31f3d Translated using Weblate (Bulgarian)
Currently translated at 83.7% (320 of 382 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/bg/
2023-07-05 22:52:48 +02:00
waclaw66
911fe9e9f8 Translated using Weblate (Czech)
Currently translated at 100.0% (382 of 382 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/cs/
2023-07-04 09:52:38 +02:00
Nicola Rizzo
9f255aee25 Translated using Weblate (Italian)
Currently translated at 70.4% (269 of 382 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/it/
2023-07-02 16:52:40 +02:00
Nicola Rizzo
67603e58bf Translated using Weblate (Italian)
Currently translated at 70.1% (268 of 382 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/it/
2023-07-01 15:53:21 +02:00
34 changed files with 1498 additions and 736 deletions

View File

@@ -1,4 +1,4 @@
FROM golang:1.19-bullseye as builder
FROM golang:1.20-bullseye as builder
ARG VERSION=dev
ARG COMMIT=unknown

View File

@@ -141,8 +141,13 @@ 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>
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>

View File

@@ -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:

View File

@@ -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"`
}

View File

@@ -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)
}

View File

@@ -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
}
}

View File

@@ -96,7 +96,7 @@ func execPublish(c *cli.Context) error {
icon := c.String("icon")
actions := c.String("actions")
attach := c.String("attach")
markdown := c.Bool("attach")
markdown := c.Bool("markdown")
filename := c.String("filename")
file := c.String("file")
email := c.String("email")

View File

@@ -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

View File

@@ -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()))
}

View File

@@ -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,
@@ -649,8 +657,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 +666,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 +683,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 +692,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>
```

View File

@@ -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).

View File

@@ -56,6 +56,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
@@ -129,6 +130,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had
## Blog + forum posts
- [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 - 9/2021
- [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 +157,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

View File

@@ -653,8 +653,8 @@ As of today, **Markdown is only supported in the web app.** Here's an example of
=== "ntfy CLI"
```
ntfy publish \
mytopic \
--markdown \
mytopic \
"Look ma, **bold text**, *italics*, ..."
```

View File

@@ -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
@@ -1251,15 +1283,11 @@ 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 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)
### ntfy Android app v1.16.1 (UNRELEASED)

48
go.mod
View File

@@ -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.12.0 // indirect
cloud.google.com/go/storage v1.32.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.16.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.10.0
golang.org/x/oauth2 v0.9.0 // indirect
golang.org/x/crypto v0.12.0
golang.org/x/oauth2 v0.11.0 // indirect
golang.org/x/sync v0.3.0
golang.org/x/term v0.9.0
golang.org/x/term v0.11.0
golang.org/x/time v0.3.0
google.golang.org/api v0.129.0
google.golang.org/api v0.138.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.24.0
github.com/stripe/stripe-go/v74 v74.30.0
)
require (
cloud.google.com/go v0.110.3 // indirect
cloud.google.com/go/compute v1.20.1 // indirect
cloud.google.com/go v0.110.7 // 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/s2a-go v0.1.5 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect
github.com/googleapis/gax-go/v2 v2.11.0 // 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.11.1 // 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.11.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/net v0.14.0 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/text v0.12.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-20230629202037-9506855d4529 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230629202037-9506855d4529 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230629202037-9506855d4529 // indirect
google.golang.org/grpc v1.56.1 // indirect
google.golang.org/appengine/v2 v2.0.4 // indirect
google.golang.org/genproto v0.0.0-20230815205213-6bfd019c3878 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230815205213-6bfd019c3878 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230815205213-6bfd019c3878 // indirect
google.golang.org/grpc v1.57.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

93
go.sum
View File

@@ -1,21 +1,21 @@
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/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.7 h1:rJyC7nWRg2jWGZ4wSJ5nY65GTdYJkg0cd/uXb+ACI6o=
cloud.google.com/go v0.110.7/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI=
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.12.0 h1:aeEA/N7DW7+l2u5jtkO8I0qv0D95YwjggD8kUHrTHO4=
cloud.google.com/go/firestore v1.12.0/go.mod h1:b38dKhgzlmNNGTNZZwe7ZRFEuRab1Hay3/DBsIGKKy4=
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.32.0 h1:5w6DxEGOnktmJHarxAOUywxVW9lbNWIzlzzUltG/3+o=
cloud.google.com/go/storage v1.32.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=
@@ -48,8 +48,8 @@ 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=
@@ -95,15 +95,15 @@ 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.5 h1:8IYp3w9nysqv3JH+NJgXJzGbDHzLOTj43BmSkp+O7qg=
github.com/google/s2a-go v0.1.5/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
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/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=
@@ -127,8 +127,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/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
@@ -143,8 +143,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
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.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=
@@ -158,8 +158,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
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.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
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=
@@ -179,12 +179,12 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
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.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
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.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU=
golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk=
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=
@@ -202,20 +202,20 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
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.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.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.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
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.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.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=
@@ -230,24 +230,24 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
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.138.0 h1:K/tVp05MxNVbHShRw9m7e9VJGdagNeTdMzqPH7AUqr0=
google.golang.org/api v0.138.0/go.mod h1:4xyob8CxC+0GChNBvEUAk8VBKNvYOTWM9T3v3UfRxuY=
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/v2 v2.0.4 h1:aAAPYixP9EfTJjNO6F46afaxp+jfzb0VgwVjMeLBtF4=
google.golang.org/appengine/v2 v2.0.4/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/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/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 v0.0.0-20230815205213-6bfd019c3878 h1:Iveh6tGCJkHAjJgEqUQYGDGgbwmhjoAOz8kO/ajxefY=
google.golang.org/genproto v0.0.0-20230815205213-6bfd019c3878/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4=
google.golang.org/genproto/googleapis/api v0.0.0-20230815205213-6bfd019c3878 h1:WGq4lvB/mlicysM/dUT3SBvijH4D3sm/Ny1A4wmt2CI=
google.golang.org/genproto/googleapis/api v0.0.0-20230815205213-6bfd019c3878/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230815205213-6bfd019c3878 h1:lv6/DhyiFFGsmzxbsUUTOkN29II+zeWHxvT8Lpdxsv0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230815205213-6bfd019c3878/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
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=
@@ -256,8 +256,8 @@ google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTp
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.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
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=
@@ -269,6 +269,7 @@ 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=

View File

@@ -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

View File

@@ -1520,29 +1520,29 @@ func TestServer_PublishActions_AndPoll(t *testing.T) {
func TestServer_PublishMarkdown(t *testing.T) {
s := newTestServer(t, newTestConfig(t))
response := request(t, s, "PUT", "/mytopic", "_underline this_", map[string]string{
response := request(t, s, "PUT", "/mytopic", "**make this bold**", map[string]string{
"Content-Type": "text/markdown",
})
require.Equal(t, 200, response.Code)
m := toMessage(t, response.Body.String())
require.Equal(t, "_underline this_", m.Message)
require.Equal(t, "**make this bold**", m.Message)
require.Equal(t, "text/markdown", m.ContentType)
}
func TestServer_PublishMarkdown_QueryParam(t *testing.T) {
s := newTestServer(t, newTestConfig(t))
response := request(t, s, "PUT", "/mytopic?md=1", "_underline this_", nil)
response := request(t, s, "PUT", "/mytopic?md=1", "**make this bold**", nil)
require.Equal(t, 200, response.Code)
m := toMessage(t, response.Body.String())
require.Equal(t, "_underline this_", m.Message)
require.Equal(t, "**make this bold**", m.Message)
require.Equal(t, "text/markdown", m.ContentType)
}
func TestServer_PublishMarkdown_NotMarkdown(t *testing.T) {
s := newTestServer(t, newTestConfig(t))
response := request(t, s, "PUT", "/mytopic", "_underline this_", map[string]string{
response := request(t, s, "PUT", "/mytopic", "**make this bold**", map[string]string{
"Content-Type": "not-markdown",
})
require.Equal(t, 200, response.Code)

View File

@@ -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{}

View File

@@ -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)

1306
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
"alert_notification_permission_required_description": "Разрешете на мрежовия четец да показва известия.",
"notifications_attachment_copy_url_title": "Копиране на адреса на прикачения файл",
"notifications_example": "Пример",
"notifications_no_subscriptions_title": "Липсват абонаменти",
"notifications_no_subscriptions_title": "Липсват абонаменти.",
"nav_topics_title": "Абонаменти",
"action_bar_send_test_notification": "Пробно известие",
"action_bar_unsubscribe": "Отписване",
@@ -60,8 +60,8 @@
"notifications_click_copy_url_button": "Копиране на препратка",
"notifications_click_open_button": "Отваряне",
"notifications_click_copy_url_title": "Копиране на препратката в междинната памет",
"notifications_none_for_topic_title": "Липсват известия в темата",
"notifications_none_for_any_title": "Липсват известия",
"notifications_none_for_topic_title": "Липсват известия в темата.",
"notifications_none_for_any_title": "Липсват известия.",
"notifications_none_for_topic_description": "За да изпратите известия в тази тема направете заявка чрез методите PUT или POST към адреса й.",
"notifications_none_for_any_description": "За да изпратите известия в тема направете заявка чрез методите PUT или POST към адреса ѝ. Ето пример с една от вашите теми.",
"notifications_no_subscriptions_description": "Щракнете върху „{{linktext}}“, за да създадете тема или да се абонирате. След това като направите заявка чрез методите PUT или POST ще ги получите тук.",
@@ -287,5 +287,36 @@
"account_upgrade_dialog_cancel_warning": "Това действие ще <strong>прекрати абонамента</strong> и ще промени профила ви на неплатен на {{date}}. На тази дата резервираните теми, както и пазените на сървъра съобщения, <strong> ще бъдат премахнати</strong>.",
"account_upgrade_dialog_proration_info": "<strong>Преизчисляване на плащания</strong>: При надграждане между платени планове разликата в цената ще бъде <strong>начислена незабавно</strong>. При преминаване към по-евтин план надплатената сума ще бъде използвана за плащане за бъдещи периоди.",
"account_basics_tier_manage_billing_button": "Управление на плащанията",
"account_basics_tier_canceled_subscription": "Абонаментът е прекратен и профилът ще бъде променен на неплатен на {{date}}."
"account_basics_tier_canceled_subscription": "Абонаментът е прекратен и профилът ще бъде променен на неплатен на {{date}}.",
"account_basics_phone_numbers_dialog_verify_button_sms": "Изпращане на SMS",
"account_basics_phone_numbers_dialog_verify_button_call": "Обаждане до мен",
"account_upgrade_dialog_tier_features_calls_other": "{{calls}} телефонни обаждания на ден",
"common_copy_to_clipboard": "Копиране в междинната памет",
"publish_dialog_call_label": "Телефонно обаждане",
"publish_dialog_call_reset": "Премахване на телефонно обаждане",
"publish_dialog_chip_call_label": "Телефонно обаждане",
"account_basics_phone_numbers_dialog_description": "За да възползвате от услугата известяване чрез телефонно обаждане, трябва да добавите и потвърдите поне един телефонен номер. Проверката може да бъде извършена чрез SMS или телефонно обаждане.",
"account_basics_phone_numbers_title": "Телефонни номера",
"account_basics_phone_numbers_dialog_number_placeholder": "напр. +1222333444",
"account_basics_phone_numbers_dialog_number_label": "Телефонен номер",
"account_basics_phone_numbers_dialog_title": "Добавяне на телефонен номер",
"account_basics_phone_numbers_copied_to_clipboard": "Телефонният номер е копиран в междинната памет",
"account_basics_phone_numbers_no_phone_numbers_yet": "Все още няма телефонни номера",
"account_basics_phone_numbers_description": "За известяване чрез телефонно обаждане",
"publish_dialog_call_item": "Обаждане на телефонен номер {{number}}",
"publish_dialog_chip_call_no_verified_numbers_tooltip": "Няма потвърдени телефонни номера",
"account_basics_phone_numbers_dialog_channel_call": "Обаждане",
"account_basics_phone_numbers_dialog_channel_sms": "SMS",
"account_basics_phone_numbers_dialog_check_verification_button": "Код за потвърждаване",
"account_basics_phone_numbers_dialog_code_placeholder": "напр. 123456",
"account_basics_phone_numbers_dialog_code_label": "Код за потвърждение",
"account_usage_calls_none": "С този профил не могат да се извършват телефонни обаждания",
"account_usage_calls_title": "Извършени телефонни обаждания",
"account_upgrade_dialog_tier_features_no_calls": "Без телефонни обаждания",
"account_upgrade_dialog_tier_features_messages_one": "{{messages}} съобщение на ден",
"account_upgrade_dialog_tier_features_messages_other": "{{messages}} съобщения на ден",
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} ел. писмо на ден",
"account_upgrade_dialog_tier_features_emails_other": "{{emails}} ел. писма на ден",
"account_upgrade_dialog_tier_features_calls_one": "{{calls}} телефонни обаждания на ден",
"account_usage_attachment_storage_description": "{{filesize}} на файл, изтриване след {{expiry}}"
}

View File

@@ -365,5 +365,20 @@
"account_basics_phone_numbers_no_phone_numbers_yet": "Zatím žádná telefonní čísla",
"account_basics_phone_numbers_copied_to_clipboard": "Telefonní číslo zkopírováno do schránky",
"publish_dialog_chip_call_no_verified_numbers_tooltip": "Žádná ověřená telefonní čísla",
"publish_dialog_call_item": "Vytočit číslo {{number}}"
"publish_dialog_call_item": "Vytočit číslo {{number}}",
"account_basics_phone_numbers_dialog_channel_sms": "SMS",
"account_basics_phone_numbers_dialog_title": "Přidat telefonní číslo",
"account_basics_phone_numbers_dialog_number_label": "Telefonní číslo",
"account_basics_phone_numbers_dialog_code_placeholder": "např. 123456",
"account_basics_phone_numbers_dialog_code_label": "Ověřovací kód",
"account_usage_calls_none": "S tímto účtem nelze uskutečňovat žádné telefonní hovory",
"account_basics_phone_numbers_dialog_check_verification_button": "Potvrdit kód",
"account_basics_phone_numbers_dialog_number_placeholder": "např. +1222333444",
"account_basics_phone_numbers_dialog_verify_button_sms": "Odeslat SMS",
"account_basics_phone_numbers_dialog_verify_button_call": "Zavolat mi",
"account_basics_phone_numbers_dialog_channel_call": "Zavolat",
"account_usage_calls_title": "Uskutečněné telefonáty",
"account_upgrade_dialog_tier_features_no_calls": "Žádné telefonní hovory",
"account_upgrade_dialog_tier_features_calls_one": "{{calls}} denní telefonní hovor",
"account_upgrade_dialog_tier_features_calls_other": "{{calls}} denních telefonních hovorů"
}

View File

@@ -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",

View File

@@ -259,5 +259,13 @@
"account_usage_emails_title": "Email inviate",
"account_usage_cannot_create_portal_session": "Impossibile aprire il portale di pagamento",
"account_delete_title": "Elimina account",
"account_basics_username_description": "Hey, sei tu ❤"
"account_basics_username_description": "Hey, sei tu ❤",
"publish_dialog_call_item": "Chiama numero {{number}}",
"common_copy_to_clipboard": "Copia negli appunti",
"publish_dialog_call_label": "Chiamata telefonica",
"publish_dialog_call_reset": "Rimuovi chiamata telefonica",
"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."
}

View File

@@ -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": "Нет вызовов"
}

View File

@@ -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",

View File

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

View File

@@ -359,5 +359,26 @@
"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": "无电话呼叫"
}

View File

@@ -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,

View File

@@ -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;

View File

@@ -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

View File

@@ -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) => {

View File

@@ -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) {