1
0
Fork 0
mirror of https://github.com/binwiederhier/ntfy.git synced 2024-11-23 11:49:19 +01:00

Merge branch 'main' into e2e

This commit is contained in:
Philipp Heckel 2022-10-01 20:56:50 -04:00
commit a66731641c
60 changed files with 4572 additions and 3400 deletions

1
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1 @@
github: [binwiederhier]

View file

@ -13,7 +13,7 @@ jobs:
name: Install node
uses: actions/setup-node@v2
with:
node-version: '16'
node-version: '17'
-
name: Checkout code
uses: actions/checkout@v2

View file

@ -16,7 +16,7 @@ jobs:
name: Install node
uses: actions/setup-node@v2
with:
node-version: '16'
node-version: '17'
-
name: Checkout code
uses: actions/checkout@v2

View file

@ -13,7 +13,7 @@ jobs:
name: Install node
uses: actions/setup-node@v2
with:
node-version: '16'
node-version: '17'
-
name: Checkout code
uses: actions/checkout@v2

View file

@ -64,9 +64,7 @@ builds:
- "-X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
goos: [windows]
goarch: [amd64]
hooks:
post:
- upx "{{ .Path }}" # apt install upx
# No "upx" for Windows to hopefully avoid Virus warnings
-
id: ntfy_darwin_all
binary: ntfy

View file

@ -1,5 +1,6 @@
MAKEFLAGS := --jobs=1
VERSION := $(shell git describe --tag)
COMMIT := $(shell git rev-parse --short HEAD)
.PHONY:
@ -169,7 +170,7 @@ cli-linux-server: cli-deps-static-sites
-o dist/ntfy_linux_server/ntfy \
-tags sqlite_omit_load_extension,osusergo,netgo \
-ldflags \
"-linkmode=external -extldflags=-static -s -w -X main.version=$(VERSION) -X main.commit=$(shell git rev-parse --short HEAD) -X main.date=$(shell date +%s)"
"-linkmode=external -extldflags=-static -s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(shell date +%s)"
cli-darwin-server: cli-deps-static-sites
# This is a target to build the CLI (including the server) manually.
@ -179,7 +180,7 @@ cli-darwin-server: cli-deps-static-sites
-o dist/ntfy_darwin_server/ntfy \
-tags sqlite_omit_load_extension,osusergo,netgo \
-ldflags \
"-linkmode=external -s -w -X main.version=$(VERSION) -X main.commit=$(shell git rev-parse --short HEAD) -X main.date=$(shell date +%s)"
"-linkmode=external -s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(shell date +%s)"
cli-client: cli-deps-static-sites
# This is a target to build the CLI (excluding the server) manually. This should work on Linux/macOS/Windows.
@ -189,7 +190,7 @@ cli-client: cli-deps-static-sites
-o dist/ntfy_client/ntfy \
-tags noserver \
-ldflags \
"-X main.version=$(VERSION) -X main.commit=$(shell git rev-parse --short HEAD) -X main.date=$(shell date +%s)"
"-X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(shell date +%s)"
cli-deps: cli-deps-static-sites cli-deps-all cli-deps-gcc

View file

@ -1,5 +1,13 @@
![ntfy](web/public/static/img/ntfy.png)
---
## 👶 Baby break - My baby girl was born!
Hey folks, my daughter was born on 8/30/22, so I'll be taking some time off from working on ntfy. I'll likely return
to working on features and bugs in a few weeks. I hope you understand. I posted some pictures in [#387](https://github.com/binwiederhier/ntfy/issues/387) 🥰
---
# ntfy.sh | Send push notifications to your phone or desktop via PUT/POST
[![Release](https://img.shields.io/github/release/binwiederhier/ntfy.svg?color=success&style=flat-square)](https://github.com/binwiederhier/ntfy/releases/latest)
[![Go Reference](https://pkg.go.dev/badge/heckel.io/ntfy.svg)](https://pkg.go.dev/heckel.io/ntfy)
@ -53,12 +61,26 @@ Or, if you'd like to help translate 🇩🇪 🇺🇸 🇧🇬, you can start im
<img src="https://hosted.weblate.org/widgets/ntfy/-/multi-blue.svg" alt="Translation status" />
</a>
## Donations
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
appreciated. A big fat Thank You to the folks already sponsoring ntfy:
<a href="https://github.com/aspyct"><img src="https://github.com/aspyct.png" width="40px" /></a>
<a href="https://github.com/codinghipster"><img src="https://github.com/codinghipster.png" width="40px" /></a>
<a href="https://github.com/HinFort"><img src="https://github.com/HinFort.png" width="40px" /></a>
<a href="https://github.com/mckay115"><img src="https://github.com/mckay115.png" width="40px" /></a>
<a href="https://github.com/neutralinsomniac"><img src="https://github.com/neutralinsomniac.png" width="40px" /></a>
<a href="https://github.com/nickexyz"><img src="https://github.com/nickexyz.png" width="40px" /></a>
<a href="https://github.com/qcasey"><img src="https://github.com/qcasey.png" width="40px" /></a>
<a href="https://github.com/Salamafet"><img src="https://github.com/Salamafet.png" width="40px" /></a>
## License
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:
* [github.com/urfave/cli/v2](https://github.com/urfave/cli/v2) (MIT) is used to drive the CLI
* [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
* [Roboto Font](https://fonts.google.com/specimen/Roboto) (Apache 2.0) is used as a font in everything web

View file

@ -53,6 +53,7 @@ type Message struct { // TODO combine with server.message
Priority int
Tags []string
Click string
Icon string
Attachment *Attachment
// Additional fields
@ -222,11 +223,12 @@ func (c *Client) Poll(topic string, options ...SubscribeOption) ([]*Message, err
// The method returns a unique subscriptionID that can be used in Unsubscribe.
//
// Example:
// c := client.New(client.NewConfig())
// subscriptionID := c.Subscribe("mytopic")
// for m := range c.Messages {
// fmt.Printf("New message: %s", m.Message)
// }
//
// c := client.New(client.NewConfig())
// subscriptionID := c.Subscribe("mytopic")
// for m := range c.Messages {
// fmt.Printf("New message: %s", m.Message)
// }
func (c *Client) Subscribe(topic string, options ...SubscribeOption) string {
c.mu.Lock()
defer c.mu.Unlock()

View file

@ -5,6 +5,12 @@
#
# default-host: https://ntfy.sh
# Defaults below will be used when a topic does not have its own settings
#
# default-user:
# default-password:
# default-command:
# Subscriptions to topics and their actions. This option is primarily used by the systemd service,
# or if you cann "ntfy subscribe --from-config" directly.
#

View file

@ -12,8 +12,11 @@ const (
// Config is the config struct for a Client
type Config struct {
DefaultHost string `yaml:"default-host"`
Subscribe []struct {
DefaultHost string `yaml:"default-host"`
DefaultUser string `yaml:"default-user"`
DefaultPassword string `yaml:"default-password"`
DefaultCommand string `yaml:"default-command"`
Subscribe []struct {
Topic string `yaml:"topic"`
User string `yaml:"user"`
Password string `yaml:"password"`
@ -25,8 +28,11 @@ type Config struct {
// NewConfig creates a new Config struct for a Client
func NewConfig() *Config {
return &Config{
DefaultHost: DefaultBaseURL,
Subscribe: nil,
DefaultHost: DefaultBaseURL,
DefaultUser: "",
DefaultPassword: "",
DefaultCommand: "",
Subscribe: nil,
}
}

View file

@ -12,6 +12,9 @@ func TestConfig_Load(t *testing.T) {
filename := filepath.Join(t.TempDir(), "client.yml")
require.Nil(t, os.WriteFile(filename, []byte(`
default-host: http://localhost
default-user: phil
default-password: mypass
default-command: 'echo "Got the message: $message"'
subscribe:
- topic: no-command-with-auth
user: phil
@ -22,12 +25,16 @@ subscribe:
command: notify-send -i /usr/share/ntfy/logo.png "Important" "$m"
if:
priority: high,urgent
- topic: defaults
`), 0600))
conf, err := client.LoadConfig(filename)
require.Nil(t, err)
require.Equal(t, "http://localhost", conf.DefaultHost)
require.Equal(t, 3, len(conf.Subscribe))
require.Equal(t, "phil", conf.DefaultUser)
require.Equal(t, "mypass", conf.DefaultPassword)
require.Equal(t, `echo "Got the message: $message"`, conf.DefaultCommand)
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)
@ -37,4 +44,5 @@ subscribe:
require.Equal(t, "alerts", conf.Subscribe[2].Topic)
require.Equal(t, `notify-send -i /usr/share/ntfy/logo.png "Important" "$m"`, conf.Subscribe[2].Command)
require.Equal(t, "high,urgent", conf.Subscribe[2].If["priority"])
require.Equal(t, "defaults", conf.Subscribe[3].Topic)
}

View file

@ -56,6 +56,11 @@ func WithClick(url string) PublishOption {
return WithHeader("X-Click", url)
}
// WithIcon makes the notification use the given URL as its icon
func WithIcon(icon string) PublishOption {
return WithHeader("X-Icon", icon)
}
// WithActions adds custom user actions to the notification. The value can be either a JSON array or the
// simple format definition. See https://ntfy.sh/docs/publish/#action-buttons for details.
func WithActions(value string) PublishOption {

View file

@ -97,11 +97,11 @@ func execUserAccess(c *cli.Context) error {
}
func changeAccess(c *cli.Context, manager auth.Manager, username string, topic string, perms string) error {
if !util.InStringList([]string{"", "read-write", "rw", "read-only", "read", "ro", "write-only", "write", "wo", "none", "deny"}, perms) {
if !util.Contains([]string{"", "read-write", "rw", "read-only", "read", "ro", "write-only", "write", "wo", "none", "deny"}, perms) {
return errors.New("permission must be one of: read-write, read-only, write-only, or deny (or the aliases: read, ro, write, wo, none)")
}
read := util.InStringList([]string{"read-write", "rw", "read-only", "read", "ro"}, perms)
write := util.InStringList([]string{"read-write", "rw", "write-only", "write", "wo"}, perms)
read := util.Contains([]string{"read-write", "rw", "read-only", "read", "ro"}, perms)
write := util.Contains([]string{"read-write", "rw", "write-only", "write", "wo"}, perms)
user, err := manager.User(username)
if err == auth.ErrNotFound {
return fmt.Errorf("user %s does not exist", username)

View file

@ -40,7 +40,7 @@ func initConfigFileInputSourceFunc(configFlag string, flags []cli.Flag, next cli
// This function also maps aliases, so a .yml file can contain short options, or options with underscores
// instead of dashes. See https://github.com/binwiederhier/ntfy/issues/255.
func newYamlSourceFromFile(file string, flags []cli.Flag) (altsrc.InputSourceContext, error) {
var rawConfig map[interface{}]interface{}
var rawConfig map[any]any
b, err := os.ReadFile(file)
if err != nil {
return nil, err

View file

@ -29,6 +29,7 @@ var flagsPublish = append(
&cli.StringFlag{Name: "tags", Aliases: []string{"tag", "T"}, EnvVars: []string{"NTFY_TAGS"}, Usage: "comma separated list of tags and emojis"},
&cli.StringFlag{Name: "delay", Aliases: []string{"at", "in", "D"}, EnvVars: []string{"NTFY_DELAY"}, Usage: "delay/schedule message"},
&cli.StringFlag{Name: "click", Aliases: []string{"U"}, EnvVars: []string{"NTFY_CLICK"}, Usage: "URL to open when notification is clicked"},
&cli.StringFlag{Name: "icon", Aliases: []string{"i"}, EnvVars: []string{"NTFY_ICON"}, Usage: "URL to use as notification icon"},
&cli.StringFlag{Name: "actions", Aliases: []string{"A"}, EnvVars: []string{"NTFY_ACTIONS"}, Usage: "actions JSON array or simple definition"},
&cli.StringFlag{Name: "attach", Aliases: []string{"a"}, EnvVars: []string{"NTFY_ATTACH"}, Usage: "URL to send as an external attachment"},
&cli.StringFlag{Name: "filename", Aliases: []string{"name", "n"}, EnvVars: []string{"NTFY_FILENAME"}, Usage: "filename for the attachment"},
@ -65,6 +66,7 @@ Examples:
ntfy pub --at=8:30am delayed_topic Laterzz # Send message at 8:30am
ntfy pub -e phil@example.com alerts 'App is down!' # Also send email to phil@example.com
ntfy pub --click="https://reddit.com" redd 'New msg' # Opens Reddit when notification is clicked
ntfy pub --icon="http://some.tld/icon.png" 'Icon!' # Send notification with custom icon
ntfy pub --attach="http://some.tld/file.zip" files # Send ZIP archive from URL as attachment
ntfy pub --file=flower.jpg flowers 'Nice!' # Send image.jpg as attachment
ntfy pub -u phil:mypass secret Psst # Publish with username/password
@ -91,6 +93,7 @@ func execPublish(c *cli.Context) error {
tags := c.String("tags")
delay := c.String("delay")
click := c.String("click")
icon := c.String("icon")
actions := c.String("actions")
attach := c.String("attach")
filename := c.String("filename")
@ -124,6 +127,9 @@ func execPublish(c *cli.Context) error {
return err
}
pm.Priority = p
if icon != "" {
options = append(options, client.WithIcon(icon))
}
if actions != "" {
options = append(options, client.WithActions(strings.ReplaceAll(actions, "\n", " ")))
}
@ -207,10 +213,11 @@ func execPublish(c *cli.Context) error {
// parseTopicMessageCommand reads the topic and the remaining arguments from the context.
//
// There are a few cases to consider:
// ntfy publish <topic> [<message>]
// ntfy publish --wait-cmd <topic> <command>
// NTFY_TOPIC=.. ntfy publish [<message>]
// NTFY_TOPIC=.. ntfy publish --wait-cmd <command>
//
// ntfy publish <topic> [<message>]
// ntfy publish --wait-cmd <topic> <command>
// NTFY_TOPIC=.. ntfy publish [<message>]
// NTFY_TOPIC=.. ntfy publish --wait-cmd <command>
func parseTopicMessageCommand(c *cli.Context) (topic string, message string, command []string, err error) {
var args []string
topic, args, err = parseTopicAndArgs(c)

View file

@ -1,8 +0,0 @@
package cmd
import "syscall"
func processExists(pid int) bool {
err := syscall.Kill(pid, syscall.Signal(0))
return err == nil
}

View file

@ -52,6 +52,7 @@ func TestCLI_Publish_All_The_Things(t *testing.T) {
"--tags", "tag1,tag2",
// No --delay, --email
"--click", "https://ntfy.sh",
"--icon", "https://ntfy.sh/static/img/ntfy.png",
"--attach", "https://f-droid.org/F-Droid.apk",
"--filename", "fdroid.apk",
"--no-cache",
@ -73,6 +74,7 @@ func TestCLI_Publish_All_The_Things(t *testing.T) {
require.Equal(t, "", m.Attachment.Owner)
require.Equal(t, int64(0), m.Attachment.Expires)
require.Equal(t, "", m.Attachment.Type)
require.Equal(t, "https://ntfy.sh/static/img/ntfy.png", m.Icon)
}
func TestCLI_Publish_Wait_PID_And_Cmd(t *testing.T) {

View file

@ -1,3 +1,6 @@
//go:build darwin || linux || dragonfly || freebsd || netbsd || openbsd
// +build darwin linux dragonfly freebsd netbsd openbsd
package cmd
import "syscall"

View file

@ -157,14 +157,18 @@ func execServe(c *cli.Context) error {
return errors.New("if smtp-server-listen is set, smtp-server-domain must also be set")
} else if attachmentCacheDir != "" && baseURL == "" {
return errors.New("if attachment-cache-dir is set, base-url must also be set")
} else if baseURL != "" && !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") && strings.HasSuffix(baseURL, "/") {
return errors.New("if set, base-url must start with http:// or https://, and must not end with a slash (/)")
} else if !util.InStringList([]string{"read-write", "read-only", "write-only", "deny-all"}, authDefaultAccess) {
} else if baseURL != "" && !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") {
return errors.New("if set, base-url must start with http:// or https://")
} else if baseURL != "" && strings.HasSuffix(baseURL, "/") {
return errors.New("if set, base-url must not end with a slash (/)")
} else if !util.Contains([]string{"read-write", "read-only", "write-only", "deny-all"}, authDefaultAccess) {
return errors.New("if set, auth-default-access must start set to 'read-write', 'read-only', 'write-only' or 'deny-all'")
} else if !util.InStringList([]string{"app", "home", "disable"}, webRoot) {
} else if !util.Contains([]string{"app", "home", "disable"}, webRoot) {
return errors.New("if set, web-root must be 'home' or 'app'")
} else if upstreamBaseURL != "" && !strings.HasPrefix(upstreamBaseURL, "http://") && !strings.HasPrefix(upstreamBaseURL, "https://") {
return errors.New("if set, upstream-base-url must start with http:// or https://")
} else if upstreamBaseURL != "" && strings.HasSuffix(upstreamBaseURL, "/") {
return errors.New("if set, upstream-base-url must not end with a slash (/)")
} else if upstreamBaseURL != "" && baseURL == "" {
return errors.New("if upstream-base-url is set, base-url must also be set")
} else if upstreamBaseURL != "" && baseURL != "" && baseURL == upstreamBaseURL {

View file

@ -175,11 +175,28 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic,
for filter, value := range s.If {
topicOptions = append(topicOptions, client.WithFilter(filter, value))
}
if s.User != "" && s.Password != "" {
topicOptions = append(topicOptions, client.WithBasicAuth(s.User, s.Password))
var user, password string
if s.User != "" {
user = s.User
} else if conf.DefaultUser != "" {
user = conf.DefaultUser
}
if s.Password != "" {
password = s.Password
} else if conf.DefaultPassword != "" {
password = conf.DefaultPassword
}
if user != "" && password != "" {
topicOptions = append(topicOptions, client.WithBasicAuth(user, password))
}
subscriptionID := cl.Subscribe(s.Topic, topicOptions...)
cmds[subscriptionID] = s.Command
if s.Command != "" {
cmds[subscriptionID] = s.Command
} else if conf.DefaultCommand != "" {
cmds[subscriptionID] = conf.DefaultCommand
} else {
cmds[subscriptionID] = ""
}
}
if topic != "" {
subscriptionID := cl.Subscribe(topic, options...)

View file

@ -1,3 +1,6 @@
//go:build linux || dragonfly || freebsd || netbsd || openbsd
// +build linux dragonfly freebsd netbsd openbsd
package cmd
const (

View file

@ -273,7 +273,7 @@ func createAuthManager(c *cli.Context) (auth.Manager, error) {
return nil, errors.New("option auth-file not set; auth is unconfigured for this server")
} else if !util.FileExists(authFile) {
return nil, errors.New("auth-file does not exist; please start the server at least once to create it")
} else if !util.InStringList([]string{"read-write", "read-only", "write-only", "deny-all"}, authDefaultAccess) {
} else if !util.Contains([]string{"read-write", "read-only", "write-only", "deny-all"}, authDefaultAccess) {
return nil, errors.New("if set, auth-default-access must start set to 'read-write', 'read-only' or 'deny-all'")
}
authDefaultRead := authDefaultAccess == "read-write" || authDefaultAccess == "read-only"

View file

@ -805,9 +805,25 @@ and [here](https://easyengine.io/tutorials/nginx/block-wp-login-php-bruteforce-a
=== "/etc/nginx/nginx.conf"
```
# Rate limit all IP addresses
http {
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
}
# Alternatively, whitelist certain IP addresses
http {
geo $limited {
default 1;
116.203.112.46/32 0;
132.226.42.65/32 0;
...
}
map $limited $limitkey {
1 $binary_remote_addr;
0 "";
}
limit_req_zone $limitkey zone=one:10m rate=1r/s;
}
```
=== "/etc/nginx/sites-enabled/ntfy.sh"

View file

@ -58,8 +58,8 @@ These steps **assume Ubuntu**. Steps may vary on different Linux distributions.
First, install [Go](https://go.dev/) (see [official instructions](https://go.dev/doc/install)):
``` shell
wget https://go.dev/dl/go1.18.linux-amd64.tar.gz
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.18.linux-amd64.tar.gz
wget https://go.dev/dl/go1.19.1.linux-amd64.tar.gz
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.19.1.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin
go version # verifies that it worked
```
@ -72,7 +72,7 @@ goreleaser -v # verifies that it worked
Install [nodejs](https://nodejs.org/en/) (see [official instructions](https://nodejs.org/en/download/package-manager/)):
``` shell
curl -fsSL https://deb.nodesource.com/setup_17.x | sudo -E bash -
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs
npm -v # verifies that it worked
```

View file

@ -449,6 +449,43 @@ You can now test the notifications and apply them to monitors:
<a href="../static/img/uptimekuma-ios-up.jpg"><img src="../static/img/uptimekuma-ios-up.jpg"/></a>
</div>
## UptimeRobot
Go to your [UptimeRobot](https://github.com/uptimerobot) My Settings > Alert Contacts > Add Alert Contact
Select **Alert Contact Type** = Webhook. Then set your desired **Friendly Name** (e.g. "ntfy-sh-UP"), **URL to Notify**, **POST value** and select checkbox **Send as JSON (application/json)**. Make sure to send the JSON POST request to ntfy.domain.com without the topic name in the url and include the "topic" name in the JSON body.
<div id="uptimerobot-monitor-setup" class="screenshots">
<a href="../static/img/uptimerobot-setup.jpg"><img src="../static/img/uptimerobot-setup.jpg"/></a>
</div>
``` json
{
"topic":"myTopic",
"title": "*monitorFriendlyName* *alertTypeFriendlyName*",
"message": "*alertDetails*",
"tags": ["green_circle"],
"priority": 3,
"click": https://uptimerobot.com/dashboard#*monitorID*
}
```
You can create two Alert Contacts each with a different icon and priority, for example:
``` json
{
"topic":"myTopic",
"title": "*monitorFriendlyName* *alertTypeFriendlyName*",
"message": "*alertDetails*",
"tags": ["red_circle"],
"priority": 3,
"click": https://uptimerobot.com/dashboard#*monitorID*
}
```
You can now add the created Alerts Contact(s) to the monitor(s) and test the notifications:
<div id="uptimerobot-monitor-screenshots" class="screenshots">
<a href="../static/img/uptimerobot-test.jpg"><img src="../static/img/uptimerobot-test.jpg"/></a>
</div>
## Apprise
ntfy is integrated natively into [Apprise](https://github.com/caronc/apprise) (also check out the
[Apprise/ntfy wiki page](https://github.com/caronc/apprise/wiki/Notify_ntfy)).

View file

@ -44,8 +44,6 @@ server and listens for incoming notifications. This consumes additional battery
but delivers notifications instantly.
## Where can I donate?
Many people have asked (thanks for that!), but I am currently not accepting any donations. The cost is manageable
($25/month for hosting, and $99/year for the Apple cert) right now, and I don't want to have to feel obligated to
anyone by accepting their money.
I may ask for donations in the future, though. After all, $400 per year isn't nothing...
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
appreciated.

View file

@ -26,37 +26,37 @@ deb/rpm packages.
=== "x86_64/amd64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.27.2/ntfy_1.27.2_linux_x86_64.tar.gz
tar zxvf ntfy_1.27.2_linux_x86_64.tar.gz
sudo cp -a ntfy_1.27.2_linux_x86_64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.27.2_linux_x86_64/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_x86_64.tar.gz
tar zxvf ntfy_1.28.0_linux_x86_64.tar.gz
sudo cp -a ntfy_1.28.0_linux_x86_64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.28.0_linux_x86_64/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "armv6"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.27.2/ntfy_1.27.2_linux_armv6.tar.gz
tar zxvf ntfy_1.27.2_linux_armv6.tar.gz
sudo cp -a ntfy_1.27.2_linux_armv6/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.27.2_linux_armv6/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_armv6.tar.gz
tar zxvf ntfy_1.28.0_linux_armv6.tar.gz
sudo cp -a ntfy_1.28.0_linux_armv6/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.28.0_linux_armv6/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "armv7/armhf"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.27.2/ntfy_1.27.2_linux_armv7.tar.gz
tar zxvf ntfy_1.27.2_linux_armv7.tar.gz
sudo cp -a ntfy_1.27.2_linux_armv7/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.27.2_linux_armv7/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_armv7.tar.gz
tar zxvf ntfy_1.28.0_linux_armv7.tar.gz
sudo cp -a ntfy_1.28.0_linux_armv7/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.28.0_linux_armv7/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "arm64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.27.2/ntfy_1.27.2_linux_arm64.tar.gz
tar zxvf ntfy_1.27.2_linux_arm64.tar.gz
sudo cp -a ntfy_1.27.2_linux_arm64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.27.2_linux_arm64/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_arm64.tar.gz
tar zxvf ntfy_1.28.0_linux_arm64.tar.gz
sudo cp -a ntfy_1.28.0_linux_arm64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.28.0_linux_arm64/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
@ -103,7 +103,7 @@ Manually installing the .deb file:
=== "x86_64/amd64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.27.2/ntfy_1.27.2_linux_amd64.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_amd64.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@ -111,7 +111,7 @@ Manually installing the .deb file:
=== "armv6"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.27.2/ntfy_1.27.2_linux_armv6.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_armv6.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@ -119,7 +119,7 @@ Manually installing the .deb file:
=== "armv7/armhf"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.27.2/ntfy_1.27.2_linux_armv7.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_armv7.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@ -127,7 +127,7 @@ Manually installing the .deb file:
=== "arm64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.27.2/ntfy_1.27.2_linux_arm64.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_arm64.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@ -137,28 +137,28 @@ Manually installing the .deb file:
=== "x86_64/amd64"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.27.2/ntfy_1.27.2_linux_amd64.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_amd64.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "armv6"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.27.2/ntfy_1.27.2_linux_armv6.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.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/v1.27.2/ntfy_1.27.2_linux_armv7.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_armv7.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "arm64"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.27.2/ntfy_1.27.2_linux_arm64.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_arm64.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
@ -184,18 +184,18 @@ nix-env -iA ntfy-sh
## 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/v1.27.2/ntfy_1.27.2_macOS_all.tar.gz),
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_macOS_all.tar.gz),
extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`).
If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For all other users, it'll look for it at
`~/Library/Application Support/ntfy/client.yml` (sample included in the tarball).
```bash
curl -L https://github.com/binwiederhier/ntfy/releases/download/v1.27.2/ntfy_1.27.2_macOS_all.tar.gz > ntfy_1.27.2_macOS_all.tar.gz
tar zxvf ntfy_1.27.2_macOS_all.tar.gz
sudo cp -a ntfy_1.27.2_macOS_all/ntfy /usr/local/bin/ntfy
curl -L https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_macOS_all.tar.gz > ntfy_1.28.0_macOS_all.tar.gz
tar zxvf ntfy_1.28.0_macOS_all.tar.gz
sudo cp -a ntfy_1.28.0_macOS_all/ntfy /usr/local/bin/ntfy
mkdir ~/Library/Application\ Support/ntfy
cp ntfy_1.27.2_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
cp ntfy_1.28.0_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
ntfy --help
```
@ -207,7 +207,7 @@ ntfy --help
## 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/v1.27.2/ntfy_1.27.2_windows_x86_64.zip),
To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_windows_x86_64.zip),
extract it and place the `ntfy.exe` binary somewhere in your `%Path%`.
The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file).
@ -228,6 +228,11 @@ The server exposes its web UI and the API on port 80, so you need to expose that
[message cache](config.md#message-cache), you also need to map a volume to `/var/cache/ntfy`. To change other settings,
you should map `/etc/ntfy`, so you can edit `/etc/ntfy/server.yml`.
!!! info
Note that the Docker image **does not contain a `/etc/ntfy/server.yml` file**. If you'd like to use a config file,
please manually create one outside the image and map it as a volume, e.g. via `-v /etc/ntfy:/etc/ntfy`. You may
use the [`server.yml` file on GitHub](https://github.com/binwiederhier/ntfy/blob/main/server/server.yml) as a template.
Basic usage (no cache or additional config):
```
docker run -p 80:80 -it binwiederhier/ntfy serve

100
docs/integrations.md Normal file
View file

@ -0,0 +1,100 @@
# Integrations + community projects
There are quite a few projects that work with ntfy, integrate ntfy, or have been built around ntfy. It's super exciting to see what you guys have come up with. Feel free to [create a pull request on GitHub](https://github.com/binwiederhier/ntfy/issues) to add your own project here.
I've added a ⭐ to projects or posts that have a significant following, or had a lot of interaction by the community.
## Public ntfy servers
| URL | Country |
|-----------------------------------------------|:---------:|
| [ntfy.sh](https://ntfy.sh/) (*Official*) | 🇺🇸 |
| [ntfy.tedomum.net](https://ntfy.tedomum.net/) | 🇫🇷 🇪🇺 |
| [ntfy.jae.fi](https://ntfy.jae.fi/) | 🇫🇮 🇪🇺 |
Thanks to everyone running a public server. **You guys rock!** To the users: Be aware that server operators can log your
messages until I finally finish implementing end-to-end encryption.
## Official integrations
- [Apprise](https://github.com/caronc/apprise/wiki/Notify_ntfy) ⭐ - Push Notifications that work with just about every platform
- [Uptime Kuma](https://uptime.kuma.pet/) ⭐ - A self-hosted monitoring tool
- [Robusta](https://docs.robusta.dev/master/catalog/sinks/webhook.html) ⭐ - open source platform for Kubernetes troubleshooting
- [borgmatic](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#third-party-monitoring-services) ⭐ - configuration-driven backup software for servers and workstations
- [Radarr](https://radarr.video/) ⭐ - Movie collection manager for Usenet and BitTorrent users
- [FlexGet](https://flexget.com/Plugins/Notifiers/ntfysh) ⭐ - Multipurpose automation tool for all of your media
- [Platypush](https://docs.platypush.tech/platypush/plugins/ntfy.html) - Automation platform aimed to run on any device that can run Python
## [UnifiedPush](https://unifiedpush.org/users/apps/) integrations
- [Element](https://f-droid.org/packages/im.vector.app/) ⭐ - Matrix client
- [SchildiChat](https://schildi.chat/android/) ⭐ - Matrix client
- [Tusky](https://tusky.app/) ⭐ - Fediverse client
- [Fedilab](https://fedilab.app/) - Fediverse client
- [FindMyDevice](https://gitlab.com/Nulide/findmydevice/) - Find your Device with an SMS or online with the help of FMDServer
- [Tox Push Message App](https://github.com/zoff99/tox_push_msg_app) - Tox Push Message App
## Libraries
- [ntfy-php-library](https://github.com/VerifiedJoseph/ntfy-php-library) - PHP library for sending messages using a ntfy server (PHP)
- [ntfy-notifier](https://github.com/DAcodedBEAT/ntfy-notifier) - Symfony Notifier integration for ntfy (PHP)
- [ntfpy](https://github.com/Nevalicjus/ntfpy) - API Wrapper for ntfy.sh (Python)
- [pyntfy](https://github.com/DP44/pyntfy) - A module for interacting with ntfy notifications (Python)
- [vntfy](https://github.com/lmangani/vntfy) - Barebone V client for ntfy (V)
- [ntfy-middleman](https://github.com/nachotp/ntfy-middleman) - Wraps APIs and send notifications using ntfy.sh on schedule (Python)
## CLIs + GUIs
- [ntfy.sh.sh](https://github.com/mininmobile/ntfy.sh.sh) - Run scripts on ntfy.sh events
- [ntfy Desktop client](https://github.com/mininmobile/ntfy-desktop) - Cross-platform desktop application for ntfy
- [ntfy svelte front-end](https://github.com/novatorem/Ntfy) - Front-end built with svelte
- [wio-ntfy-ticker](https://github.com/nachotp/wio-ntfy-ticker) - Ticker display for a ntfy.sh topic
- [ntfysh-windows](https://github.com/lucas-bortoli/ntfysh-windows) - A ntfy client for Windows Desktop
- [ntfyr](https://github.com/haxwithaxe/ntfyr) - A simple commandline tool to send notifications to ntfy
- [ntfy.py](https://github.com/ioqy/ntfy-client-python) - ntfy.py is a simple nfty.sh client for sending notifications
## Projects + scripts
- [Grafana-to-ntfy](https://github.com/kittyandrew/grafana-to-ntfy) - Grafana-to-ntfy alerts channel (Rust)
- [ntfy-long-zsh-command](https://github.com/robfox92/ntfy-long-zsh-command) - Notifies you once a long-running command completes (zsh)
- [ntfy-shellscripts](https://github.com/nickexyz/ntfy-shellscripts) - A few scripts for the ntfy project (Shell)
- [QuickStatus](https://github.com/corneliusroot/QuickStatus) - A shell script to alert to any immediate problems upon login (Shell)
- [ntfy.el](https://github.com/shombando/ntfy) - Send notifications from Emacs (Emacs)
- [backup-projects](https://gist.github.com/anthonyaxenov/826ba65abbabd5b00196bc3e6af76002) - Stupidly simple backup script for own projects (Shell)
- [grav-plugin-whistleblower](https://github.com/Himmlisch-Studios/grav-plugin-whistleblower) - Grav CMS plugin to get notifications via ntfy (PHP)
- [ntfy-server-status](https://github.com/filip2cz/ntfy-server-status) - Checking if server is online and reporting through ntfy (C)
- [borg-based backup](https://github.com/davidhi7/backup) - Simple borg-based backup script with notifications based on ntfy.sh or Discord webhooks (Python/Shell)
- [ntfy.sh *arr script](https://github.com/agent-squirrel/nfty-arr-script) - Quick and hacky script to get sonarr/radarr to notify the ntfy.sh service (Shell)
- [siteeagle](https://github.com/tpanum/siteeagle) - A small Python script to monitor websites and notify changes (Python)
- [send_to_phone](https://github.com/whipped-cream/send_to_phone) - Scripts to upload a file to Transfer.sh and ping ntfy with the download link (Python)
- [ntfy Discord bot](https://github.com/R0dn3yS/ntfy-bot) - WIP ntfy discord bot (TypeScript)
- [ntfy Discord bot](https://github.com/binwiederhier/ntfy-bot) - ntfy Discord bot (Go)
- [Bettarr Notifications](https://github.com/NiNiyas/Bettarr-Notifications) - Better Notifications for Sonarr and Radarr (Python)
- [Notify me the intruders](https://github.com/nothingbutlucas/notify_me_the_intruders) - Notify you if they are intruders or new connections on your network (Shell)
- [Send GitHub Action to ntfy](https://github.com/NiNiyas/ntfy-action) - Send GitHub Action workflow notifications to ntfy (JS)
- [ntfy alertmanager bridge](https://github.com/aTable/ntfy_alertmanager_bridge) - Basic alertmanager bridge to ntfy (JS)
- [restreamchat2ntfy](https://github.com/kurohuku7/restreamchat2ntfy) - Send restream.io chat to ntfy to check on the Meta Quest (JS)
- [k8s-ntfy-deployment-service](https://github.com/Christian42/k8s-ntfy-deployment-service) - Automatic Kubernetes (k8s) ntfy deployment
## Blog + forum posts
- [Self hosted Mobile Push Notifications using NTFY | Thejesh GN](https://thejeshgn.com/2022/08/23/self-hosted-mobile-push-notifications-using-ntfy/) - 8/2022
- [Fedora Magazine | 4 cool new projects to try in Copr](https://fedoramagazine.org/4-cool-new-projects-to-try-in-copr-for-august-2022/) - 8/2022
- [Docker로 오픈소스 푸시알람 프로젝트 ntfy.sh 설치 및 사용하기.(Feat. Uptimekuma)](https://svrforum.com/svr/398979) - 8/2022
- [Easy notifications from R](https://sometimesir.com/posts/easy-notifications-from-r/) - 6/2022
- [ntfy is finally coming to iOS, and Matrix/UnifiedPush gateway support](https://www.reddit.com/r/selfhosted/comments/vdzvxi/ntfy_is_finally_coming_to_ios_with_full/) ⭐ - 6/2022
- [无需注册的通知服务ntfy](https://wbsu2003.4everland.app/2022/05/30/%E6%97%A0%E9%9C%80%E6%B3%A8%E5%86%8C%E7%9A%84%E9%80%9A%E7%9F%A5%E6%9C%8D%E5%8A%A1ntfy/) - 5/2022
- [Install guide (with Docker)](https://chowdera.com/2022/150/202205301257379077.html) - 5/2022
- [Updated review post (Jan-Lukas Else)](https://jlelse.blog/thoughts/2022/04/ntfy) - 4/2022
- [Reddit feature update post](https://www.reddit.com/r/selfhosted/comments/uetlso/ntfy_is_a_tool_to_send_push_notifications_to_your/) ⭐ - 4/2022
- [無料で簡単に通知の送受信ができつつオープンソースでセルフホストも可能な「ntfy」を使ってみた (Gigazine)](https://gigazine.net/news/20220404-ntfy-push-notification/) - 4/2022
- [Pocketmags ntfy review](https://pocketmags.com/us/linux-format-magazine/march-2022/articles/1104187/ntfy) - 3/2022
- [Reddit web app release post](https://www.reddit.com/r/selfhosted/comments/tc0p0u/say_hello_to_the_brand_new_ntfysh_web_app_push/) ⭐ - 3/2022
- [Lemmy post (Jakob)](https://lemmy.eus/post/15541) - 1/2022
- [Reddit UnifiedPush release post](https://www.reddit.com/r/selfhosted/comments/s5jylf/my_open_source_notification_android_app_and/) ⭐ - 1/2022
- [ntfy: send notifications from your computer to your phone](https://rs1.es/tutorials/2022/01/19/ntfy-send-notifications-phone.html) - 1/2022
- [Short ntfy review (Jan-Lukas Else)](https://jlelse.blog/links/2021/12/ntfy-sh) - 12/2021
- [Free MacroDroid webhook alternative (FrameXX)](https://www.macrodroidforum.com/index.php?threads/ntfy-sh-free-macrodroid-webhook-alternative.1505/) - 12/2021
- [ntfy otro sistema de notificaciones pub-sub simple basado en HTTP](https://ugeek.github.io/blog/post/2021-11-05-ntfy-sh-otro-sistema-de-notificaciones-pub-sub-simple-basado-en-http.html) - 11/2021
- [Show HN: A tool to send push notifications to your phone, written in Go](https://news.ycombinator.com/item?id=29715464) ⭐ - 12/2021
- [Reddit selfhostable post](https://www.reddit.com/r/selfhosted/comments/qxlsm9/my_open_source_notification_android_app_and/) ⭐ - 11/2021

View file

@ -1166,7 +1166,7 @@ Alternatively, the same actions can be defined as **JSON array**, if the notific
method: 'POST',
body: JSON.stringify({
topic: "myhome",
message": "You left the house. Turn down the A/C?",
message: "You left the house. Turn down the A/C?",
actions: [
{
action: "view",
@ -2221,7 +2221,7 @@ Here's an example showing how to upload an image:
Filename: flower.jpg
Content-Type: 52312
<binary JPEG data>
(binary JPEG data)
```
=== "JavaScript"
@ -2349,6 +2349,112 @@ Here's an example showing how to attach an APK file:
<figcaption>File attachment sent from an external URL</figcaption>
</figure>
## Icons
_Supported on:_ :material-android:
You can include an icon that will appear next to the text of the notification. Simply pass the `X-Icon` header or query
parameter (or its alias `Icon`) to specify the URL that the icon is located at. The client will automatically download
the icon (unless it is already cached locally, and less than 24 hours old), and show it in the notification. Icons are
cached locally in the client until the notification is deleted. **Only JPEG and PNG images are supported at this time**.
Here's an example showing how to include an icon:
=== "Command line (curl)"
```
curl \
-H "Icon: https://styles.redditmedia.com/t5_32uhe/styles/communityIcon_xnt6chtnr2j21.png" \
-H "Title: Kodi: Resuming Playback" \
-H "Tags: arrow_forward" \
-d "The Wire, S01E01" \
ntfy.sh/tvshows
```
=== "ntfy CLI"
```
ntfy publish \
--icon="https://styles.redditmedia.com/t5_32uhe/styles/communityIcon_xnt6chtnr2j21.png" \
--title="Kodi: Resuming Playback" \
--tags="arrow_forward" \
tvshows \
"The Wire, S01E01"
```
=== "HTTP"
``` http
POST /tvshows HTTP/1.1
Host: ntfy.sh
Icon: https://styles.redditmedia.com/t5_32uhe/styles/communityIcon_xnt6chtnr2j21.png
Tags: arrow_forward
Title: Kodi: Resuming Playback
The Wire, S01E01
```
=== "JavaScript"
``` javascript
fetch('https://ntfy.sh/tvshows', {
method: 'POST',
headers: {
'Icon': 'https://styles.redditmedia.com/t5_32uhe/styles/communityIcon_xnt6chtnr2j21.png',
'Title': 'Kodi: Resuming Playback',
'Tags': 'arrow_forward'
},
body: "The Wire, S01E01"
})
```
=== "Go"
``` go
req, _ := http.NewRequest("POST", "https://ntfy.sh/tvshows", strings.NewReader("The Wire, S01E01"))
req.Header.Set("Icon", "https://styles.redditmedia.com/t5_32uhe/styles/communityIcon_xnt6chtnr2j21.png")
req.Header.Set("Tags", "arrow_forward")
req.Header.Set("Title", "Kodi: Resuming Playback")
http.DefaultClient.Do(req)
```
=== "PowerShell"
``` powershell
$uri = "https://ntfy.sh/tvshows"
$headers = @{ Title"="Kodi: Resuming Playback"
Tags="arrow_forward"
Icon="https://styles.redditmedia.com/t5_32uhe/styles/communityIcon_xnt6chtnr2j21.png" }
$body = "The Wire, S01E01"
Invoke-RestMethod -Method 'Post' -Uri $uri -Headers $headers -Body $body -UseBasicParsing
```
=== "Python"
``` python
requests.post("https://ntfy.sh/tvshows",
data="The Wire, S01E01",
headers={
"Title": "Kodi: Resuming Playback",
"Tags": "arrow_forward",
"Icon": "https://styles.redditmedia.com/t5_32uhe/styles/communityIcon_xnt6chtnr2j21.png"
})
```
=== "PHP"
``` php-inline
file_get_contents('https://ntfy.sh/tvshows', false, stream_context_create([
'http' => [
'method' => 'PUT',
'header' =>
"Content-Type: text/plain\r\n" . // Does not matter
"Title: Kodi: Resuming Playback\r\n" .
"Tags: arrow_forward\r\n" .
"Icon: https://styles.redditmedia.com/t5_32uhe/styles/communityIcon_xnt6chtnr2j21.png",
],
'content' => "The Wire, S01E01"
]));
```
Here's an example of how it will look on Android:
<figure markdown>
![file attachment](static/img/android-screenshot-icon.png){ width=500 }
<figcaption>Custom icon from an external URL</figcaption>
</figure>
## E-mail notifications
_Supported on:_ :material-android: :material-apple: :material-firefox:
@ -2804,6 +2910,7 @@ and can be passed as **HTTP headers** or **query parameters in the URL**. They a
| `X-Actions` | `Actions`, `Action` | JSON array or short format of [user actions](#action-buttons) |
| `X-Click` | `Click` | URL to open when [notification is clicked](#click-action) |
| `X-Attach` | `Attach`, `a` | URL to send as an [attachment](#attachments), as an alternative to PUT/POST-ing an attachment |
| `X-Icon` | `Icon` | URL to use as notification [icon](#icons) |
| `X-Filename` | `Filename`, `file`, `f` | Optional [attachment](#attachments) filename, as it appears in the client |
| `X-Email` | `X-E-Mail`, `Email`, `E-Mail`, `mail`, `e` | E-mail address for [e-mail notifications](#e-mail-notifications) |
| `X-Cache` | `Cache` | Allows disabling [message caching](#message-caching) |

View file

@ -2,9 +2,45 @@
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 v1.29.0 (UNRELEASED)
## ntfy Android app v1.14.0 (UNRELEASED)
**Bug fixes + maintenance:**
* Subscriptions can now have a display name ([#370](https://github.com/binwiederhier/ntfy/issues/370), thanks to [@tfheen](https://github.com/tfheen) for reporting)
* Bump Go version to Go 18.x ([#422](https://github.com/binwiederhier/ntfy/issues/422))
**Documentation:**
* Updated developer docs, bump nodejs and go version ([#414](https://github.com/binwiederhier/ntfy/issues/414), thanks to [@YJSoft](https://github.com/YJSoft) for reporting)
**Additional translations:**
* Korean (thanks to [@YJSofta0f97461d82447ac](https://hosted.weblate.org/user/YJSofta0f97461d82447ac/))
**Sponsorships:**:
Thank you to the amazing folks who decided to [sponsor ntfy](https://github.com/sponsors/binwiederhier). Thank you for
helping carry the cost of the public server and developer licenses, and more importantly: Thank you for believing in ntfy!
You guys rock!
Sponsors (alphabetical order):
* [@aspyct](https://github.com/aspyct)
* [@codinghipster](https://github.com/codinghipster)
* [@HinFort](https://github.com/HinFort)
* [@mckay115](https://github.com/mckay115)
* [@neutralinsomniac](https://github.com/neutralinsomniac)
* [@nickexyz](https://github.com/nickexyz)
* [@qcasey](https://github.com/qcasey)
* [@Salamafet](https://github.com/Salamafet)
* +1 private sponsor
## ntfy Android app v1.14.0
Released September 27, 2022
This release adds the ability to set a custom icon to each notification, as well as a display name to subscriptions. We
also moved the action buttons in the detail view to a more logical place, fixed a bunch of bugs, and added four more
languages. Hurray!
**Features:**
@ -13,40 +49,61 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
* Polling is now done with `since=<id>` API, which makes deduping easier ([#165](https://github.com/binwiederhier/ntfy/issues/165))
* Turned JSON stream deprecation banner into "Use WebSockets" banner (no ticket)
* Move action buttons in notification cards ([#236](https://github.com/binwiederhier/ntfy/issues/236), thanks to [@wunter8](https://github.com/wunter8))
* Icons can be set for each individual notification ([#126](https://github.com/binwiederhier/ntfy/issues/126), thanks to [@wunter8](https://github.com/wunter8))
**Bugs:**
**Bug fixes:**
* Long-click selecting of notifications doesn't scroll to the top anymore ([#235](https://github.com/binwiederhier/ntfy/issues/235), thanks to [@wunter8](https://github.com/wunter8))
* Add attachment and click URL extras to MESSAGE_RECEIVED broadcast ([#329](https://github.com/binwiederhier/ntfy/issues/329), thanks to [@wunter8](https://github.com/wunter8))
* Accessibility: Clear/choose service URL button in base URL dropdown now has a label ([#292](https://github.com/binwiederhier/ntfy/issues/292), thanks to [@mhameed](https://github.com/mhameed) for reporting)
* Web: Switched "Pop" and "Pop Swoosh" sounds ([#352](https://github.com/binwiederhier/ntfy/issues/352), thanks to [@coma-toast](https://github.com/coma-toast) for reporting)
**Additional translations:**
* Italian (thanks to [@Genio2003](https://hosted.weblate.org/user/Genio2003/))
* Dutch (thanks to [@SchoNie](https://hosted.weblate.org/user/SchoNie/))
* Ukranian (thanks to [@v.kopitsa](https://hosted.weblate.org/user/v.kopitsa/))
* Polish (thanks to [@Namax0r](https://hosted.weblate.org/user/Namax0r/))
Thank you to [@wunter8](https://github.com/wunter8) for proactively picking up some Android tickets, and fixing them! You rock!
## ntfy server v1.28.0
Released September 27, 2022
## ntfy server v1.28.0 (UNRELEASED)
This release primarily adds icon support for the Android app, and adds a display name to subscriptions in the web app.
Aside from that, we fixed a few random bugs, most importantly the `Priority` header bug that allows the use behind
Cloudflare. We also added a ton of documentation. Most prominently, an [integrations + projects page](https://ntfy.sh/docs/integrations/).
As of now, I also have started accepting **[donations and sponsorships](https://github.com/sponsors/binwiederhier)** 💸.
I would be very humbled if you consider donating.
**Features:**
* Subscription display name for the web app ([#348](https://github.com/binwiederhier/ntfy/pull/348))
* Allow setting socket permissions via `--listen-unix-mode` ([#356](https://github.com/binwiederhier/ntfy/pull/356), thanks to [@koro666](https://github.com/koro666))
* Icons can be set for each individual notification ([#126](https://github.com/binwiederhier/ntfy/issues/126), thanks to [@wunter8](https://github.com/wunter8))
* CLI: Allow default username/password in `client.yml` ([#372](https://github.com/binwiederhier/ntfy/pull/372), thanks to [@wunter8](https://github.com/wunter8))
* Build support for other Unix systems ([#393](https://github.com/binwiederhier/ntfy/pull/393), thanks to [@la-ninpre](https://github.com/la-ninpre))
**Bugs:**
**Bug fixes:**
* `ntfy user` commands don't work with `auth_file` but works with `auth-file` ([#344](https://github.com/binwiederhier/ntfy/issues/344), thanks to [@Histalek](https://github.com/Histalek) for reporting)
* Ignore new draft HTTP `Priority` header ([#351](https://github.com/binwiederhier/ntfy/issues/351), thanks to [@ksurl](https://github.com/ksurl) for reporting)
* Delete expired attachments based on mod time instead of DB entry to avoid races (no ticket)
* Better logging for Matrix push key errors ([#384](https://github.com/binwiederhier/ntfy/pull/384), thanks to [@christophehenry](https://github.com/christophehenry))
* Web: Switched "Pop" and "Pop Swoosh" sounds ([#352](https://github.com/binwiederhier/ntfy/issues/352), thanks to [@coma-toast](https://github.com/coma-toast) for reporting)
**Documentation:**
* Added [integrations + projects page](https://ntfy.sh/docs/integrations/) (**so many integrations, whoa!**)
* Added example for [UptimeRobot](https://ntfy.sh/docs/examples/#uptimerobot)
* Fix some PowerShell publish docs ([#345](https://github.com/binwiederhier/ntfy/pull/345), thanks to [@noahpeltier](https://github.com/noahpeltier))
* Clarified Docker install instructions ([#361](https://github.com/binwiederhier/ntfy/issues/361), thanks to [@barart](https://github.com/barart) for reporting)
* Mismatched quotation marks ([#392](https://github.com/binwiederhier/ntfy/pull/392)], thanks to [@connorlanigan](https://github.com/connorlanigan))
-->
**Additional translations:**
* Ukranian (thanks to [@v.kopitsa](https://hosted.weblate.org/user/v.kopitsa/))
* Polish (thanks to [@Namax0r](https://hosted.weblate.org/user/Namax0r/))
## ntfy server v1.27.2
Released June 23, 2022
@ -62,7 +119,7 @@ minute or so, due to competing stats gathering (personal installations will like
* Trace: Log entire HTTP request to simplify debugging (no ticket)
* Allow setting user password via `NTFY_PASSWORD` env variable ([#327](https://github.com/binwiederhier/ntfy/pull/327), thanks to [@Kenix3](https://github.com/Kenix3))
**Bugs:**
**Bug fixes:**
* Fix slow requests due to excessive locking ([#338](https://github.com/binwiederhier/ntfy/issues/338))
* Return HTTP 500 for `GET /_matrix/push/v1/notify` when `base-url` is not configured (no ticket)
@ -87,7 +144,7 @@ CLI is now available via Scoop, and ntfy is now natively supported in Uptime Kum
* [Uptime Kuma](https://github.com/louislam/uptime-kuma) now allows publishing to ntfy ([uptime-kuma#1674](https://github.com/louislam/uptime-kuma/pull/1674), thanks to [@philippdormann](https://github.com/philippdormann))
* Display ntfy version in `ntfy serve` command ([#314](https://github.com/binwiederhier/ntfy/issues/314), thanks to [@poblabs](https://github.com/poblabs))
**Bugs:**
**Bug fixes:**
* Web app: Show "notifications not supported" alert on HTTP ([#323](https://github.com/binwiederhier/ntfy/issues/323), thanks to [@milksteakjellybeans](https://github.com/milksteakjellybeans) for reporting)
* Use last address in `X-Forwarded-For` header as visitor address ([#328](https://github.com/binwiederhier/ntfy/issues/328))
@ -110,7 +167,7 @@ set your server as the default server for new topics.
* Support for auth and user management ([#277](https://github.com/binwiederhier/ntfy/issues/277))
* Ability to add default server ([#295](https://github.com/binwiederhier/ntfy/issues/295))
**Bugs:**
**Bug fixes:**
* Add validation for selfhosted server URL ([#290](https://github.com/binwiederhier/ntfy/issues/290))
@ -173,7 +230,7 @@ for details).
* Cancel notifications when navigating to topic (no ticket)
* iOS 14.0 support (no ticket, [PR#1](https://github.com/binwiederhier/ntfy-ios/pull/1), thanks to [@callum-99](https://github.com/callum-99))
**Bugs:**
**Bug fixes:**
* iOS UI not always updating properly ([#267](https://github.com/binwiederhier/ntfy/issues/267))
@ -190,7 +247,7 @@ Apple development environment.
* Add subscribe filter to query exact messages by ID (no ticket)
* Support for `poll_request` messages to support [iOS push notifications](https://ntfy.sh/docs/config/#ios-instant-notifications) for self-hosted servers (no ticket)
**Bugs:**
**Bug fixes:**
* Support emails without `Content-Type` ([#265](https://github.com/binwiederhier/ntfy/issues/265), thanks to [@dmbonsall](https://github.com/dmbonsall))
@ -228,7 +285,7 @@ it adds support for APNs, the iOS messaging service. This is needed for the (soo
* Ability to disable the web app entirely ([#238](https://github.com/binwiederhier/ntfy/issues/238)/[#249](https://github.com/binwiederhier/ntfy/pull/249), thanks to [@Curid](https://github.com/Curid))
* Add APNs config to Firebase messages to support [iOS app](https://github.com/binwiederhier/ntfy/issues/4) ([#247](https://github.com/binwiederhier/ntfy/pull/247), thanks to [@Copephobia](https://github.com/Copephobia))
**Bugs:**
**Bug fixes:**
* Support underscores in server.yml config options ([#255](https://github.com/binwiederhier/ntfy/issues/255), thanks to [@ajdelgado](https://github.com/ajdelgado))
* Force MAKEFLAGS to --jobs=1 in `Makefile` ([#257](https://github.com/binwiederhier/ntfy/pull/257), thanks to [@oddlama](https://github.com/oddlama))
@ -257,7 +314,7 @@ and custom icons. Aside from that, we've got tons of bug fixes as usual.
* Per-subscription settings, custom subscription icons ([#155](https://github.com/binwiederhier/ntfy/issues/155), thanks to [@mztiq](https://github.com/mztiq) for reporting)
* Cards in notification detail view ([#175](https://github.com/binwiederhier/ntfy/issues/175), thanks to [@cmeis](https://github.com/cmeis) for reporting)
**Bugs:**
**Bug fixes:**
* Accurate naming of "mute notifications" from "pause notifications" ([#224](https://github.com/binwiederhier/ntfy/issues/224), thanks to [@shadow00](https://github.com/shadow00) for reporting)
* Make messages with links selectable ([#226](https://github.com/binwiederhier/ntfy/issues/226), thanks to [@StoyanDimitrov](https://github.com/StoyanDimitrov) for reporting)
@ -290,7 +347,7 @@ We've also improved the documentation a little and added translations for three
* Better parsing of the user actions, allowing quotes (no ticket)
* Add "mark as read" icon button to notification ([#243](https://github.com/binwiederhier/ntfy/pull/243), thanks to [@wunter8](https://github.com/wunter8))
**Bugs:**
**Bug fixes:**
* `Upgrade` header check is now case in-sensitive ([#228](https://github.com/binwiederhier/ntfy/issues/228), thanks to [@wunter8](https://github.com/wunter8) for finding it)
* Made web app sounds quieter ([#222](https://github.com/binwiederhier/ntfy/issues/222))
@ -332,7 +389,7 @@ languages and fixed a ton of bugs.
thanks to [@StoyanDimitrov](https://github.com/StoyanDimitrov) for reporting)
* Channel settings option to configure DND override, sounds, etc. ([#91](https://github.com/binwiederhier/ntfy/issues/91))
**Bugs:**
**Bug fixes:**
* Validate URLs when changing default server and server in user management ([#193](https://github.com/binwiederhier/ntfy/issues/193),
thanks to [@StoyanDimitrov](https://github.com/StoyanDimitrov) for reporting)
@ -373,7 +430,7 @@ Limited support is available in the web app.
* Added ARMv6 build ([#200](https://github.com/binwiederhier/ntfy/issues/200), thanks to [@jcrubioa](https://github.com/jcrubioa) for reporting)
* Web app internationalization support 🇧🇬 🇩🇪 🇺🇸 🌎 ([#189](https://github.com/binwiederhier/ntfy/issues/189))
**Bugs:**
**Bug fixes:**
* Web app: English language strings fixes, additional descriptions for settings ([#203](https://github.com/binwiederhier/ntfy/issues/203), thanks to [@StoyanDimitrov](https://github.com/StoyanDimitrov))
* Web app: Show error message snackbar when sending test notification fails ([#205](https://github.com/binwiederhier/ntfy/issues/205), thanks to [@cmeis](https://github.com/cmeis))
@ -413,7 +470,7 @@ Released Apr 7, 2022
* Translations to different languages ([#188](https://github.com/binwiederhier/ntfy/issues/188), thanks to
[@StoyanDimitrov](https://github.com/StoyanDimitrov) for initiating things)
**Bugs:**
**Bug fixes:**
* IllegalStateException: Failed to build unique file ([#177](https://github.com/binwiederhier/ntfy/issues/177), thanks to [@Fallenbagel](https://github.com/Fallenbagel) for reporting)
* SQLiteConstraintException: Crash during UP registration ([#185](https://github.com/binwiederhier/ntfy/issues/185))
@ -447,7 +504,7 @@ Released Apr 6, 2022
* Added message bar and publish dialog ([#196](https://github.com/binwiederhier/ntfy/issues/196))
**Bugs:**
**Bug fixes:**
* Added `EXPOSE 80/tcp` to Dockerfile to support auto-discovery in [Traefik](https://traefik.io/) ([#195](https://github.com/binwiederhier/ntfy/issues/195), thanks to [@s-h-a-r-d](https://github.com/s-h-a-r-d))
@ -463,7 +520,7 @@ Released Apr 6, 2022
## ntfy server v1.19.0
Released Mar 30, 2022
**Bugs:**
**Bug fixes:**
* Do not pack binary with `upx` for armv7/arm64 due to `illegal instruction` errors ([#191](https://github.com/binwiederhier/ntfy/issues/191), thanks to [@iexos](https://github.com/iexos))
* Do not allow comma in topic name in publish via GET endpoint (no ticket)

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
docs/static/img/uptimerobot-setup.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
docs/static/img/uptimerobot-test.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View file

@ -254,6 +254,14 @@ I hope this shows how powerful this command is. Here's a short video that demons
<figcaption>Execute all the things</figcaption>
</figure>
If most (or all) of your subscription usernames, passwords, and commands are the same, you can specify a `default-user`, `default-password`, and `default-command` at the top of the
`client.yml`. If a subscription does not specify a username/password to use or does not have a command, the defaults will be used, otherwise, the subscription settings will
override the defaults.
!!! warning
Because the `default-user` and `default-password` will be sent for each topic that does not have its own username/password (even if the topic does not require authentication),
be sure that the servers/topics you subscribe to use HTTPS to prevent leaking the username and password.
### Using the systemd service
You can use the `ntfy-client` systemd service (see [ntfy-client.service](https://github.com/binwiederhier/ntfy/blob/main/client/ntfy-client.service))
to subscribe to multiple topics just like in the example above. The service is automatically installed (but not started)

53
go.mod
View file

@ -1,25 +1,25 @@
module heckel.io/ntfy
go 1.17
go 1.18
require (
cloud.google.com/go/firestore v1.6.1 // indirect
cloud.google.com/go/storage v1.23.0 // indirect
github.com/BurntSushi/toml v1.1.0 // indirect
cloud.google.com/go/storage v1.27.0 // indirect
github.com/BurntSushi/toml v1.2.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/emersion/go-smtp v0.15.0
github.com/gabriel-vasile/mimetype v1.4.0
github.com/gabriel-vasile/mimetype v1.4.1
github.com/gorilla/websocket v1.5.0
github.com/mattn/go-sqlite3 v1.14.13
github.com/mattn/go-sqlite3 v1.14.15
github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6
github.com/stretchr/testify v1.7.0
github.com/urfave/cli/v2 v2.10.2
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2 // indirect
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467
golang.org/x/time v0.0.0-20220609170525-579cf78fd858
google.golang.org/api v0.85.0
github.com/urfave/cli/v2 v2.17.1
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 // indirect
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0
golang.org/x/term v0.0.0-20220919170432-7a66f970e087
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af
google.golang.org/api v0.98.0
gopkg.in/yaml.v2 v2.4.0
)
@ -31,31 +31,30 @@ require (
)
require (
cloud.google.com/go v0.102.1 // indirect
cloud.google.com/go/compute v1.7.0 // indirect
cloud.google.com/go/iam v0.3.0 // indirect
cloud.google.com/go v0.104.0 // indirect
cloud.google.com/go/compute v1.10.0 // indirect
cloud.google.com/go/iam v0.5.0 // indirect
github.com/AlekSi/pointer v1.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac // indirect
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.8 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.1.0 // indirect
github.com/googleapis/gax-go/v2 v2.4.0 // indirect
github.com/googleapis/go-type-adapters v1.0.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect
github.com/googleapis/gax-go/v2 v2.5.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
go.opencensus.io v0.23.0 // indirect
golang.org/x/net v0.0.0-20220622184535-263ec571b305 // indirect
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 // indirect
golang.org/x/net v0.0.0-20220930213112-107f3e3c3b0b // indirect
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // 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.1 // indirect
google.golang.org/genproto v0.0.0-20220623142657-077d458a5694 // indirect
google.golang.org/grpc v1.47.0 // indirect
google.golang.org/protobuf v1.28.0 // indirect
google.golang.org/appengine/v2 v2.0.2 // indirect
google.golang.org/genproto v0.0.0-20220930163606-c98284e70a91 // indirect
google.golang.org/grpc v1.49.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

187
go.sum
View file

@ -29,33 +29,91 @@ cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2Z
cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U=
cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc=
cloud.google.com/go v0.102.1 h1:vpK6iQWv/2uUeFJth4/cBHsQAGjn1iIE6AAlxipRaA0=
cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU=
cloud.google.com/go v0.104.0 h1:gSmWO7DY1vOm0MVU6DNXM11BWHHsTUmsC5cv1fuW5X8=
cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA=
cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw=
cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI=
cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4=
cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ=
cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o=
cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY=
cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA=
cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY=
cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM=
cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY=
cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=
cloud.google.com/go/compute v1.2.0/go.mod h1:xlogom/6gr8RJGBe7nT2eGsQYAFUbbv8dbC29qE3Xmw=
cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=
cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=
cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s=
cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=
cloud.google.com/go/compute v1.7.0 h1:v/k9Eueb8aAJ0vZuxKMrgm6kPhCLZU9HxFU+AFDs9Uk=
cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U=
cloud.google.com/go/compute v1.10.0 h1:aoLIYaA1fX3ywihqpBk2APQKOo20nXsp1GEZQbx5Jk4=
cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU=
cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I=
cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0=
cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs=
cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM=
cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo=
cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I=
cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo=
cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4=
cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU=
cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y=
cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk=
cloud.google.com/go/firestore v1.6.1 h1:8rBq3zRjnHx8UtBvaOWqBB1xq9jH6/wltfQLlTMh2Fw=
cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk=
cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM=
cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o=
cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0=
cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc=
cloud.google.com/go/iam v0.1.1/go.mod h1:CKqrcnI/suGpybEHxZ7BMehL0oA4LpdyJdUlTl9jVMw=
cloud.google.com/go/iam v0.3.0 h1:exkAomrVUuzx9kWFI1wm3KI0uoDeUFPB4kKGzx6x+Gc=
cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=
cloud.google.com/go/iam v0.4.0 h1:YBYU00SCDzZJdHqVc4I5d6lsklcYIjQZa1YmEz4jlSE=
cloud.google.com/go/iam v0.4.0/go.mod h1:cbaZxyScUhxl7ZAkNWiALgihfP75wS/fUsVNaa1r3vA=
cloud.google.com/go/iam v0.5.0 h1:fz9X5zyTWBmamZsqvqZqD7khbifcZF/q+Z1J8pfhIUg=
cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc=
cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic=
cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8=
cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4=
cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE=
cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY=
cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA=
cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ=
cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY=
cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs=
cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E=
cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0=
cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4=
cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o=
cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg=
cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg=
cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y=
cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4=
cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s=
cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA=
cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4=
cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0=
cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU=
cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs=
cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
@ -63,8 +121,15 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.21.0/go.mod h1:XmRlxkgPjlBONznT2dDUU/5XlpU2OjMnKuqnZI01LAA=
cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=
cloud.google.com/go/storage v1.23.0 h1:wWRIaDURQA8xxHguFCshYepGlrWIrbBnAmc7wfg07qY=
cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc=
cloud.google.com/go/storage v1.27.0 h1:YOO045NZI9RKfCj1c5A/ZtuuENUc8OAW+gHdGnDgyMQ=
cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s=
cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw=
cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU=
cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0=
cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo=
cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE=
cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
firebase.google.com/go/v4 v4.8.0 h1:ooJqjFEh1G6DQ5+wyb/RAXAgku0E2RzJeH6WauSpWSo=
firebase.google.com/go/v4 v4.8.0/go.mod h1:y+j6xX7BgBco/XaN+YExIBVm6pzvYutheDV3nprvbWc=
@ -72,8 +137,9 @@ github.com/AlekSi/pointer v1.0.0/go.mod h1:1kjywbfcPFCmncIxtk6fIEub6LKrfMz3gc5QK
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=
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0=
github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
@ -99,8 +165,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac h1:tn/OQ2PmwQ0XFVgAHfjlLyqMewry25Rz7jWnVoh4Ggs=
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac/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.15.0 h1:3+hMGMGrqP/lqd7qoxZc1hTU8LY8gHV9RFGWlqSDmP8=
github.com/emersion/go-smtp v0.15.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@ -113,8 +179,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/gabriel-vasile/mimetype v1.4.0 h1:Cn9dkdYsMIu56tGho+fqzh7XmvY2YyGU0FnbhiOsEro=
github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8=
github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q=
github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@ -168,8 +234,9 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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 v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@ -197,15 +264,17 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
github.com/googleapis/enterprise-certificate-proxy v0.1.0 h1:zO8WHNx/MYiAKJ3d5spxZXZE6KHmIQGQcAzwUzV7qQw=
github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
github.com/googleapis/enterprise-certificate-proxy v0.2.0 h1:y8Yozv7SZtlU//QXbezB6QkpuE6jMD2/gfzk4AftXjs=
github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
github.com/googleapis/gax-go/v2 v2.4.0 h1:dS9eYAjhrE2RjmzYw2XAPvcXfmcQLtFEQWn0CR82awk=
github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA=
github.com/googleapis/gax-go/v2 v2.5.1 h1:kBRZU0PSuI7PspsSb/ChWoVResUcwNVIdpB049pKTiw=
github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo=
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
@ -222,8 +291,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I=
github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6 h1:oDSPaYiL2dbjcArLrFS8ANtwgJMyOLzvQCZon+XmFsk=
github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6/go.mod h1:DPucAeQGDPUzYUt+NaWw6qsF5SFapWWToxEiVDh2aV0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -244,8 +313,10 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/urfave/cli/v2 v2.10.2 h1:x3p8awjp/2arX+Nl/G2040AZpOCHS/eMJJ1/a+mye4Y=
github.com/urfave/cli/v2 v2.10.2/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo=
github.com/urfave/cli/v2 v2.16.3 h1:gHoFIwpPjoyIMbJp/VFd+/vuD0dAgFK4B6DpEMFJfQk=
github.com/urfave/cli/v2 v2.16.3/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI=
github.com/urfave/cli/v2 v2.17.1 h1:UzjDEw2dJQUE3iRaiNQ1VrVFbyAtKGH3VdkMoHA58V0=
github.com/urfave/cli/v2 v2.17.1/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -267,8 +338,8 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A=
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -339,7 +410,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
@ -348,8 +418,13 @@ golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220622184535-263ec571b305 h1:dAgbJ2SP4jD6XYfMNLVj0BF21jo2PjChrtGaAvF5M3I=
golang.org/x/net v0.0.0-20220622184535-263ec571b305/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.0.0-20220927155233-aa73b2587036 h1:GDWXwjBkdo4XMin5T4iul98eH4BfGOR7TucJ057FxjY=
golang.org/x/net v0.0.0-20220927155233-aa73b2587036/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.0.0-20220930213112-107f3e3c3b0b h1:uKO3Js8lXGjpjdc4J3rqs0/Ex5yDKUGfk43tTYWVLas=
golang.org/x/net v0.0.0-20220930213112-107f3e3c3b0b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -371,8 +446,10 @@ golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2 h1:+jnHzr9VPj32ykQVai5DNahi9+NSp7yYuCsl5eAQtL0=
golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 h1:lxqLZaMad/dJHMFZH0NiNpiEZI/nhgWhe4wgzpE+MuA=
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
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=
@ -384,8 +461,11 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7 h1:ZrnxWX62AgTKOSagEqxvb3ffipvEDX2pl7E1TdqLqIc=
golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0 h1:cu5kTvlzcw1Q5S9f5ip1/cpiB4nXvw1XYzFPGgzLUOY=
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -448,12 +528,16 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 h1:wEZYwx+kK+KlZ0hpvP2Ls1Xr4+RWnlzGFwPP0aiDjIU=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220926163933-8cfa568d3c25 h1:nwzwVf0l2Y/lkov/+IYgMMbFyI+QypZDds9RxlSmsFQ=
golang.org/x/sys v0.0.0-20220926163933-8cfa568d3c25/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/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.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM=
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220919170432-7a66f970e087 h1:tPwmk4vmvVCMdr98VgL4JH+qZxPL8fqlUOHnyOM8N3w=
golang.org/x/term v0.0.0-20220919170432-7a66f970e087/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -467,8 +551,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U=
golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af h1:Yx9k8YCG3dvF87UAn2tu2HQLf2dt/eR1bXxpLMWeH+Y=
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@ -526,8 +610,9 @@ 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-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f h1:uF6paiQQebLeSXkrTqHqz0MXhXXS1KgF41eUdBNvxK0=
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
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.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
@ -569,11 +654,19 @@ google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc
google.golang.org/api v0.73.0/go.mod h1:lbd/q6BRFJbdpV6OUCXstVeiI5mL/d3/WifG7iNKnjI=
google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=
google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg=
google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o=
google.golang.org/api v0.85.0 h1:8rJoHuRxx+vCmZtAO/3k1dRLvYNVyTJtZ5oaFZvhgvc=
google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g=
google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI=
google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
google.golang.org/api v0.97.0 h1:x/vEL1XDF/2V4xzdNgFPaKHluRESo2aTsL7QzHnBtGQ=
google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
google.golang.org/api v0.98.0 h1:yxZrcxXESimy6r6mdL5Q6EnZwmewDJK2dVg3g75s5Dg=
google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
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.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@ -582,8 +675,9 @@ google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
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.1 h1:jTGfiRmR5qoInpT3CXJ72GJEB4owDGEKN+xRDA6ekBY=
google.golang.org/appengine/v2 v2.0.1/go.mod h1:XgltgQxPOF3ShivrVrZyfvYCx8Dunh73bKjUuXUZb8Q=
google.golang.org/appengine/v2 v2.0.2 h1:MSqyWy2shDLwG7chbwBJ5uMyw6SNqJzhJHNDwYB0Akk=
google.golang.org/appengine/v2 v2.0.2/go.mod h1:PkgRUWz4o1XOvbqtWTkBtCitEJ5Tp4HoVEdMMYQR/8E=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@ -665,14 +759,32 @@ google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX
google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220623142657-077d458a5694 h1:itnFmgk4Ls5nT+mYO2ZK6F0DpKsGZLhB5BB9y5ZL2HA=
google.golang.org/genproto v0.0.0-20220623142657-077d458a5694/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE=
google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc=
google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw=
google.golang.org/genproto v0.0.0-20220927151529-dcaddaf36704 h1:H1AcWFV69NFCMeBJ8nVLtv8uHZZ5Ozcgoq012hHEFuU=
google.golang.org/genproto v0.0.0-20220927151529-dcaddaf36704/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI=
google.golang.org/genproto v0.0.0-20220930163606-c98284e70a91 h1:Ezh2cpcnP5Rq60sLensUsFnxh7P6513NLvNtCm9iyJ4=
google.golang.org/genproto v0.0.0-20220930163606-c98284e70a91/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@ -703,8 +815,10 @@ google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ5
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8=
google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw=
google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
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=
@ -719,8 +833,9 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
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.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/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-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View file

@ -40,32 +40,32 @@ var (
)
// Trace prints the given message, if the current log level is TRACE
func Trace(message string, v ...interface{}) {
func Trace(message string, v ...any) {
logIf(TraceLevel, message, v...)
}
// Debug prints the given message, if the current log level is DEBUG or lower
func Debug(message string, v ...interface{}) {
func Debug(message string, v ...any) {
logIf(DebugLevel, message, v...)
}
// Info prints the given message, if the current log level is INFO or lower
func Info(message string, v ...interface{}) {
func Info(message string, v ...any) {
logIf(InfoLevel, message, v...)
}
// Warn prints the given message, if the current log level is WARN or lower
func Warn(message string, v ...interface{}) {
func Warn(message string, v ...any) {
logIf(WarnLevel, message, v...)
}
// Error prints the given message, if the current log level is ERROR or lower
func Error(message string, v ...interface{}) {
func Error(message string, v ...any) {
logIf(ErrorLevel, message, v...)
}
// Fatal prints the given message, and exits the program
func Fatal(v ...interface{}) {
func Fatal(v ...any) {
log.Fatalln(v...)
}
@ -122,7 +122,7 @@ func IsDebug() bool {
return Loggable(DebugLevel)
}
func logIf(l Level, message string, v ...interface{}) {
func logIf(l Level, message string, v ...any) {
if CurrentLevel() <= l {
log.Printf(l.String()+" "+message, v...)
}

View file

@ -85,6 +85,7 @@ nav:
- "Other things":
- "FAQs": faq.md
- "Examples": examples.md
- "Integrations + projects": integrations.md
- "Release notes": releases.md
- "Deprecation notices": deprecations.md
- "Emojis 🥳 🎉": emojis.md

View file

@ -60,13 +60,13 @@ func parseActions(s string) (actions []*action, err error) {
return nil, fmt.Errorf("only %d actions allowed", actionsMax)
}
for _, action := range actions {
if !util.InStringList(actionsAll, action.Action) {
if !util.Contains(actionsAll, action.Action) {
return nil, fmt.Errorf("parameter 'action' cannot be '%s', valid values are 'view', 'broadcast' and 'http'", action.Action)
} else if action.Label == "" {
return nil, fmt.Errorf("parameter 'label' is required")
} else if util.InStringList(actionsWithURL, action.Action) && action.URL == "" {
} else if util.Contains(actionsWithURL, action.Action) && action.URL == "" {
return nil, fmt.Errorf("parameter 'url' is required for action '%s'", action.Action)
} else if action.Action == actionHTTP && util.InStringList([]string{"GET", "HEAD"}, action.Method) && action.Body != "" {
} else if action.Action == actionHTTP && util.Contains([]string{"GET", "HEAD"}, action.Method) && action.Body != "" {
return nil, fmt.Errorf("parameter 'body' cannot be set if method is %s", action.Method)
}
}
@ -87,7 +87,8 @@ func parseActionsFromJSON(s string) ([]*action, error) {
// https://ntfy.sh/docs/publish/#action-buttons), into an array of actions.
//
// It can parse an actions string like this:
// view, "Look ma, commas and \"quotes\" too", url=https://..; action=broadcast, ...
//
// view, "Look ma, commas and \"quotes\" too", url=https://..; action=broadcast, ...
//
// It works by advancing the position ("pos") through the input string ("input").
//
@ -96,10 +97,11 @@ func parseActionsFromJSON(s string) ([]*action, error) {
// though it does not use state functions at all.
//
// Other resources:
// https://adampresley.github.io/2015/04/12/writing-a-lexer-and-parser-in-go-part-1.html
// https://github.com/adampresley/sample-ini-parser/blob/master/services/lexer/lexer/Lexer.go
// https://github.com/benbjohnson/sql-parser/blob/master/scanner.go
// https://blog.gopheracademy.com/advent-2014/parsers-lexers/
//
// https://adampresley.github.io/2015/04/12/writing-a-lexer-and-parser-in-go-part-1.html
// https://github.com/adampresley/sample-ini-parser/blob/master/services/lexer/lexer/Lexer.go
// https://github.com/benbjohnson/sql-parser/blob/master/scanner.go
// https://blog.gopheracademy.com/advent-2014/parsers-lexers/
func parseActionsFromSimple(s string) ([]*action, error) {
if !utf8.ValidString(s) {
return nil, errors.New("invalid utf-8 string")
@ -154,7 +156,7 @@ func populateAction(newAction *action, section int, key, value string) error {
key = "action"
} else if key == "" && section == 1 {
key = "label"
} else if key == "" && section == 2 && util.InStringList(actionsWithURL, newAction.Action) {
} else if key == "" && section == 2 && util.Contains(actionsWithURL, newAction.Action) {
key = "url"
}
@ -176,7 +178,7 @@ func populateAction(newAction *action, section int, key, value string) error {
newAction.Label = value
case "clear":
lvalue := strings.ToLower(value)
if !util.InStringList([]string{"true", "yes", "1", "false", "no", "0"}, lvalue) {
if !util.Contains([]string{"true", "yes", "1", "false", "no", "0"}, lvalue) {
return fmt.Errorf("parameter 'clear' cannot be '%s', only boolean values are allowed (true/yes/1/false/no/0)", value)
}
newAction.Clear = lvalue == "true" || lvalue == "yes" || lvalue == "1"

View file

@ -23,7 +23,7 @@ func (e errHTTP) JSON() string {
return string(b)
}
func wrapErrHTTP(err *errHTTP, message string, args ...interface{}) *errHTTP {
func wrapErrHTTP(err *errHTTP, message string, args ...any) *errHTTP {
return &errHTTP{
Code: err.Code,
HTTPCode: err.HTTPCode,
@ -53,6 +53,7 @@ var (
errHTTPBadRequestMatrixMessageInvalid = &errHTTP{40019, http.StatusBadRequest, "invalid request: Matrix JSON invalid", "https://ntfy.sh/docs/publish/#matrix-gateway"}
errHTTPBadRequestMatrixPushkeyBaseURLMismatch = &errHTTP{40020, http.StatusBadRequest, "invalid request: push key must be prefixed with base URL", "https://ntfy.sh/docs/publish/#matrix-gateway"}
errHTTPBadRequestUnexpectedMultipartField = &errHTTP{40021, http.StatusBadRequest, "invalid request: unexpected multipart field", "https://ntfy.sh/docs/publish/#end-to-end-encryption"}
errHTTPBadRequestIconURLInvalid = &errHTTP{40021, http.StatusBadRequest, "invalid request: icon URL is invalid", "https://ntfy.sh/docs/publish/#icons"}
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", ""}
errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication"}
errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication"}

View file

@ -30,6 +30,7 @@ const (
priority INT NOT NULL,
tags TEXT NOT NULL,
click TEXT NOT NULL,
icon TEXT NOT NULL,
actions TEXT NOT NULL,
attachment_name TEXT NOT NULL,
attachment_type TEXT NOT NULL,
@ -45,37 +46,37 @@ const (
COMMIT;
`
insertMessageQuery = `
INSERT INTO messages (mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding, published)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
INSERT INTO messages (mid, time, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding, published)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`
pruneMessagesQuery = `DELETE FROM messages WHERE time < ? AND published = 1`
selectRowIDFromMessageID = `SELECT id FROM messages WHERE mid = ?` // Do not include topic, see #336 and TestServer_PollSinceID_MultipleTopics
selectMessagesSinceTimeQuery = `
SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding
SELECT mid, time, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding
FROM messages
WHERE topic = ? AND time >= ? AND published = 1
ORDER BY time, id
`
selectMessagesSinceTimeIncludeScheduledQuery = `
SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding
SELECT mid, time, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding
FROM messages
WHERE topic = ? AND time >= ?
ORDER BY time, id
`
selectMessagesSinceIDQuery = `
SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding
SELECT mid, time, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding
FROM messages
WHERE topic = ? AND id > ? AND published = 1
ORDER BY time, id
`
selectMessagesSinceIDIncludeScheduledQuery = `
SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding
SELECT mid, time, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding
FROM messages
WHERE topic = ? AND (id > ? OR published = 0)
ORDER BY time, id
`
selectMessagesDueQuery = `
SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding
SELECT mid, time, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding
FROM messages
WHERE time <= ? AND published = 0
ORDER BY time, id
@ -89,7 +90,7 @@ const (
// Schema management queries
const (
currentSchemaVersion = 7
currentSchemaVersion = 8
createSchemaVersionTableQuery = `
CREATE TABLE IF NOT EXISTS schemaVersion (
id INT PRIMARY KEY,
@ -177,6 +178,11 @@ const (
migrate6To7AlterMessagesTableQuery = `
ALTER TABLE messages RENAME COLUMN attachment_owner TO sender;
`
// 7 -> 8
migrate7To8AlterMessagesTableQuery = `
ALTER TABLE messages ADD COLUMN icon TEXT NOT NULL DEFAULT('');
`
)
type messageCache struct {
@ -266,6 +272,7 @@ func (c *messageCache) addMessages(ms []*message) error {
m.Priority,
tags,
m.Click,
m.Icon,
actionsStr,
attachmentName,
attachmentType,
@ -414,7 +421,7 @@ func readMessages(rows *sql.Rows) ([]*message, error) {
for rows.Next() {
var timestamp, attachmentSize, attachmentExpires int64
var priority int
var id, topic, msg, title, tagsStr, click, actionsStr, attachmentName, attachmentType, attachmentURL, sender, encoding string
var id, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, encoding string
err := rows.Scan(
&id,
&timestamp,
@ -424,6 +431,7 @@ func readMessages(rows *sql.Rows) ([]*message, error) {
&priority,
&tagsStr,
&click,
&icon,
&actionsStr,
&attachmentName,
&attachmentType,
@ -466,6 +474,7 @@ func readMessages(rows *sql.Rows) ([]*message, error) {
Priority: priority,
Tags: tags,
Click: click,
Icon: icon,
Actions: actions,
Attachment: att,
Sender: sender,
@ -524,6 +533,8 @@ func setupCacheDB(db *sql.DB, startupQueries string) error {
return migrateFrom5(db)
} else if schemaVersion == 6 {
return migrateFrom6(db)
} else if schemaVersion == 7 {
return migrateFrom7(db)
}
return fmt.Errorf("unexpected schema version found: %d", schemaVersion)
}
@ -618,5 +629,16 @@ func migrateFrom6(db *sql.DB) error {
if _, err := db.Exec(updateSchemaVersion, 7); err != nil {
return err
}
return migrateFrom7(db)
}
func migrateFrom7(db *sql.DB) error {
log.Info("Migrating cache database schema: from 7 to 8")
if _, err := db.Exec(migrate7To8AlterMessagesTableQuery); err != nil {
return err
}
if _, err := db.Exec(updateSchemaVersion, 8); err != nil {
return err
}
return nil // Update this when a new version is added
}

View file

@ -75,7 +75,7 @@ var (
docsRegex = regexp.MustCompile(`^/docs(|/.*)$`)
fileRegex = regexp.MustCompile(`^/file/([-_A-Za-z0-9]{1,64})(?:\.[A-Za-z0-9]{1,16})?$`)
disallowedTopics = []string{"docs", "static", "file", "app", "settings"} // If updated, also update in Android app
attachURLRegex = regexp.MustCompile(`^https?://`)
urlRegex = regexp.MustCompile(`^https?://`)
//go:embed site
webFs embed.FS
@ -671,7 +671,7 @@ func (s *Server) checkAndConvertPublishMessage(v *visitor, im *inputMessage) (m
m.Attachment.Name = im.Filename
}
if im.Attach != "" {
if !attachURLRegex.MatchString(im.Attach) {
if !urlRegex.MatchString(im.Attach) {
return nil, errHTTPBadRequestAttachmentURLInvalid
}
if im.AttachmentBody != nil {
@ -691,6 +691,12 @@ func (s *Server) checkAndConvertPublishMessage(v *visitor, im *inputMessage) (m
m.Attachment.Name = "attachment"
}
}
if im.Icon != "" {
if !urlRegex.MatchString(im.Icon) {
return nil, errHTTPBadRequestIconURLInvalid
}
m.Icon = im.Icon
}
if im.Email != "" {
if err := v.EmailAllowed(); err != nil {
return nil, errHTTPTooManyRequestsLimitEmails
@ -791,6 +797,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *inputMessage) error {
m.Message = strings.ReplaceAll(readParam(r, "x-message", "message", "m"), "\\n", "\n")
m.Title = readParam(r, "x-title", "title", "t")
m.Click = readParam(r, "x-click", "click")
m.Icon = readParam(r, "x-icon", "icon")
m.Filename = readParam(r, "x-filename", "filename", "file", "f")
m.Attach = readParam(r, "x-attach", "attach", "a")
m.Email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e")
@ -824,18 +831,18 @@ func (s *Server) parsePublishParams(r *http.Request, m *inputMessage) error {
// handlePublishBody consumes the PUT/POST body and decides whether the body is an attachment or the message.
//
// 1. curl -X POST -H "Poll: 1234" ntfy.sh/...
// If a message is flagged as poll request, the body does not matter and is discarded
// 2. curl -T somebinarydata.bin "ntfy.sh/mytopic?up=1"
// If body is binary, encode as base64, if not do not encode
// 3. curl -H "Attach: http://example.com/file.jpg" ntfy.sh/mytopic
// Body must be a message, because we attached an external URL
// 4. curl -T short.txt -H "Filename: short.txt" ntfy.sh/mytopic
// Body must be attachment, because we passed a filename
// 5. curl -T file.txt ntfy.sh/mytopic
// If file.txt is <= 4096 (message limit) and valid UTF-8, treat it as a message
// 6. curl -T file.txt ntfy.sh/mytopic
// If file.txt is > message limit, treat it as an attachment
// 1. curl -X POST -H "Poll: 1234" ntfy.sh/...
// If a message is flagged as poll request, the body does not matter and is discarded
// 2. curl -T somebinarydata.bin "ntfy.sh/mytopic?up=1"
// If body is binary, encode as base64, if not do not encode
// 3. curl -H "Attach: http://example.com/file.jpg" ntfy.sh/mytopic
// Body must be a message, because we attached an external URL
// 4. curl -T short.txt -H "Filename: short.txt" ntfy.sh/mytopic
// Body must be attachment, because we passed a filename
// 5. curl -T file.txt ntfy.sh/mytopic
// If file.txt is <= 4096 (message limit) and valid UTF-8, treat it as a message
// 6. curl -T file.txt ntfy.sh/mytopic
// If file.txt is > message limit, treat it as an attachment
func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser, unifiedpush bool) error {
if m.Event == pollRequestEvent { // Case 1
return s.handleBodyDiscard(body)
@ -1255,7 +1262,7 @@ func (s *Server) topicsFromIDs(ids ...string) ([]*topic, error) {
defer s.mu.Unlock()
topics := make([]*topic, 0)
for _, id := range ids {
if id == "" || util.InStringList(disallowedTopics, id) {
if util.Contains(disallowedTopics, id) {
return nil, errHTTPBadRequestTopicDisallowed
}
if _, ok := s.topics[id]; !ok {
@ -1453,7 +1460,7 @@ func (s *Server) sendDelayedMessage(v *visitor, m *message) error {
func (s *Server) limitRequests(next handleFunc) handleFunc {
return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
if util.InStringList(s.config.VisitorRequestExemptIPAddrs, v.ip) {
if util.Contains(s.config.VisitorRequestExemptIPAddrs, v.ip) {
return next(w, r, v)
} else if err := v.RequestAllowed(); err != nil {
return errHTTPTooManyRequestsLimitRequests

View file

@ -148,6 +148,7 @@ func toFirebaseMessage(m *message, auther auth.Auther) (*messaging.Message, erro
"priority": fmt.Sprintf("%d", m.Priority),
"tags": strings.Join(m.Tags, ","),
"click": m.Click,
"icon": m.Icon,
"title": m.Title,
"message": m.Message,
"encoding": m.Encoding,
@ -216,7 +217,7 @@ func maybeTruncateFCMMessage(m *messaging.Message) *messaging.Message {
// We must set the Alert struct ("alert"), and we need to set MutableContent ("mutable-content"), so the Notification Service
// Extension in iOS can modify the message.
func createAPNSAlertConfig(m *message, data map[string]string) *messaging.APNSConfig {
apnsData := make(map[string]interface{})
apnsData := make(map[string]any)
for k, v := range data {
apnsData[k] = v
}
@ -240,7 +241,7 @@ func createAPNSAlertConfig(m *message, data map[string]string) *messaging.APNSCo
//
// See https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/pushing_background_updates_to_your_app
func createAPNSBackgroundConfig(data map[string]string) *messaging.APNSConfig {
apnsData := make(map[string]interface{})
apnsData := make(map[string]any)
for k, v := range data {
apnsData[k] = v
}

View file

@ -71,7 +71,7 @@ func TestToFirebaseMessage_Keepalive(t *testing.T) {
Aps: &messaging.Aps{
ContentAvailable: true,
},
CustomData: map[string]interface{}{
CustomData: map[string]any{
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
"event": m.Event,
@ -102,7 +102,7 @@ func TestToFirebaseMessage_Open(t *testing.T) {
Aps: &messaging.Aps{
ContentAvailable: true,
},
CustomData: map[string]interface{}{
CustomData: map[string]any{
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
"event": m.Event,
@ -123,6 +123,7 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) {
m.Priority = 4
m.Tags = []string{"tag 1", "tag2"}
m.Click = "https://google.com"
m.Icon = "https://ntfy.sh/static/img/ntfy.png"
m.Title = "some title"
m.Actions = []*action{
{
@ -165,7 +166,7 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) {
Body: "this is a message",
},
},
CustomData: map[string]interface{}{
CustomData: map[string]any{
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
"event": "message",
@ -173,6 +174,7 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) {
"priority": "4",
"tags": strings.Join(m.Tags, ","),
"click": "https://google.com",
"icon": "https://ntfy.sh/static/img/ntfy.png",
"title": "some title",
"message": "this is a message",
"actions": `[{"id":"123","action":"view","label":"Open page","clear":true,"url":"https://ntfy.sh"},{"id":"456","action":"http","label":"Close door","clear":false,"url":"https://door.com/close","method":"PUT","headers":{"really":"yes"}}]`,
@ -193,6 +195,7 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) {
"priority": "4",
"tags": strings.Join(m.Tags, ","),
"click": "https://google.com",
"icon": "https://ntfy.sh/static/img/ntfy.png",
"title": "some title",
"message": "this is a message",
"actions": `[{"id":"123","action":"view","label":"Open page","clear":true,"url":"https://ntfy.sh"},{"id":"456","action":"http","label":"Close door","clear":false,"url":"https://door.com/close","method":"PUT","headers":{"really":"yes"}}]`,
@ -239,7 +242,7 @@ func TestToFirebaseMessage_PollRequest(t *testing.T) {
Body: "New message",
},
},
CustomData: map[string]interface{}{
CustomData: map[string]any{
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
"event": "poll_request",

View file

@ -47,17 +47,17 @@ import (
//
// From the message, we only require the "pushkey", as it represents our target topic URL.
// A message may look like this (excerpt):
// {
// "notification": {
// "devices": [
// {
// "pushkey": "https://ntfy.sh/upDAHJKFFDFD?up=1",
// ...
// }
// ]
// }
// }
//
// {
// "notification": {
// "devices": [
// {
// "pushkey": "https://ntfy.sh/upDAHJKFFDFD?up=1",
// ...
// }
// ]
// }
// }
type matrixRequest struct {
Notification *struct {
Devices []*struct {
@ -96,14 +96,13 @@ const (
//
// It basically converts a Matrix push gatewqy request:
//
// POST /_matrix/push/v1/notify HTTP/1.1
// { "notification": { "devices": [ { "pushkey": "https://ntfy.sh/upDAHJKFFDFD?up=1", ... } ] } }
// POST /_matrix/push/v1/notify HTTP/1.1
// { "notification": { "devices": [ { "pushkey": "https://ntfy.sh/upDAHJKFFDFD?up=1", ... } ] } }
//
// to a ntfy request, looking like this:
//
// POST /upDAHJKFFDFD?up=1 HTTP/1.1
// { "notification": { "devices": [ { "pushkey": "https://ntfy.sh/upDAHJKFFDFD?up=1", ... } ] } }
//
// POST /upDAHJKFFDFD?up=1 HTTP/1.1
// { "notification": { "devices": [ { "pushkey": "https://ntfy.sh/upDAHJKFFDFD?up=1", ... } ] } }
func newRequestFromMatrixJSON(r *http.Request, baseURL string, messageLimit int) (*http.Request, error) {
if baseURL == "" {
return nil, errHTTPInternalErrorMissingBaseURL
@ -124,7 +123,7 @@ func newRequestFromMatrixJSON(r *http.Request, baseURL string, messageLimit int)
}
pushKey := m.Notification.Devices[0].PushKey // We ignore other devices for now, see discussion in #316
if !strings.HasPrefix(pushKey, baseURL+"/") {
return nil, &errMatrix{pushKey: pushKey, err: errHTTPBadRequestMatrixPushkeyBaseURLMismatch}
return nil, &errMatrix{pushKey: pushKey, err: wrapErrHTTP(errHTTPBadRequestMatrixPushkeyBaseURLMismatch, "received push key: %s, configured base URL: %s", pushKey, baseURL)}
}
newRequest, err := http.NewRequest(http.MethodPost, pushKey, io.NopCloser(bytes.NewReader(body.PeekedBytes)))
if err != nil {

View file

@ -56,7 +56,7 @@ func TestMatrix_NewRequestFromMatrixJSON_MismatchingPushKey(t *testing.T) {
_, err := newRequestFromMatrixJSON(r, baseURL, maxLength)
matrixErr, ok := err.(*errMatrix)
require.True(t, ok)
require.Equal(t, errHTTPBadRequestMatrixPushkeyBaseURLMismatch, matrixErr.err)
require.Equal(t, "invalid request: push key must be prefixed with base URL, received push key: https://ntfy.example.com/upABCDEFGHI?up=1, configured base URL: https://ntfy.sh", matrixErr.err.Error())
require.Equal(t, "https://ntfy.example.com/upABCDEFGHI?up=1", matrixErr.pushKey)
}

View file

@ -1048,7 +1048,7 @@ func TestServer_PublishAsJSON(t *testing.T) {
s := newTestServer(t, newTestConfig(t))
body := `{"topic":"mytopic","message":"A message","title":"a title\nwith lines","tags":["tag1","tag 2"],` +
`"not-a-thing":"ok", "attach":"http://google.com","filename":"google.pdf", "click":"http://ntfy.sh","priority":4,` +
`"delay":"30min"}`
`"icon":"https://ntfy.sh/static/img/ntfy.png", "delay":"30min"}`
response := request(t, s, "PUT", "/", body, nil)
require.Equal(t, 200, response.Code)
@ -1060,6 +1060,8 @@ func TestServer_PublishAsJSON(t *testing.T) {
require.Equal(t, "http://google.com", m.Attachment.URL)
require.Equal(t, "google.pdf", m.Attachment.Name)
require.Equal(t, "http://ntfy.sh", m.Click)
require.Equal(t, "https://ntfy.sh/static/img/ntfy.png", m.Icon)
require.Equal(t, 4, m.Priority)
require.True(t, m.Time > time.Now().Unix()+29*60)
require.True(t, m.Time < time.Now().Unix()+31*60)

View file

@ -137,7 +137,7 @@ func toEmojis(tags []string) (emojisOut []string, tagsOut []string, err error) {
nextTag:
for _, t := range tags { // TODO Super inefficient; we should just create a .json file with a map
for _, e := range emojis {
if util.InStringList(e.Aliases, t) {
if util.Contains(e.Aliases, t) {
emojisOut = append(emojisOut, e.Emoji)
continue nextTag
}

View file

@ -29,6 +29,7 @@ type message struct {
Priority int `json:"priority,omitempty"`
Tags []string `json:"tags,omitempty"`
Click string `json:"click,omitempty"`
Icon string `json:"icon,omitempty"`
Actions []*action `json:"actions,omitempty"`
Attachment *attachment `json:"attachment,omitempty"`
PollID string `json:"poll_id,omitempty"`
@ -66,17 +67,18 @@ func newAction() *action {
// PublishMessage is used as input when publishing as JSON
type PublishMessage struct {
Topic string `json:"topic"`
Title string `json:"title"`
Message string `json:"message"`
Priority int `json:"priority"`
Tags []string `json:"tags"`
Click string `json:"click"`
Actions []*action `json:"actions"`
Attach string `json:"attach"`
Filename string `json:"filename"`
Email string `json:"email"`
Delay string `json:"delay"`
Topic string `json:"topic"`
Title string `json:"title"`
Message string `json:"message"`
Priority int `json:"priority"`
Tags []string `json:"tags"`
Click string `json:"click"`
Icon string `json:"icon"`
Actions []action `json:"actions"`
Attach string `json:"attach"`
Filename string `json:"filename"`
Email string `json:"email"`
Delay string `json:"delay"`
}
// messageEncoder is a function that knows how to encode a message
@ -207,10 +209,10 @@ func (q *queryFilter) Pass(msg *message) bool {
if messagePriority == 0 {
messagePriority = 3 // For query filters, default priority (3) is the same as "not set" (0)
}
if len(q.Priority) > 0 && !util.InIntList(q.Priority, messagePriority) {
if len(q.Priority) > 0 && !util.Contains(q.Priority, messagePriority) {
return false
}
if len(q.Tags) > 0 && !util.InStringListAll(msg.Tags, q.Tags) {
if len(q.Tags) > 0 && !util.ContainsAll(msg.Tags, q.Tags) {
return false
}
return true

View file

@ -11,14 +11,13 @@ import (
// CachingEmbedFS is a wrapper around embed.FS that allows setting a ModTime, so that the
// default static file server can send 304s back. It can be used like this:
//
// var (
// //go:embed docs
// docsStaticFs embed.FS
// docsStaticCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: docsStaticFs}
// )
//
// http.FileServer(http.FS(docsStaticCached)).ServeHTTP(w, r)
// var (
// //go:embed docs
// docsStaticFs embed.FS
// docsStaticCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: docsStaticFs}
// )
//
// http.FileServer(http.FS(docsStaticCached)).ServeHTTP(w, r)
type CachingEmbedFS struct {
ModTime time.Time
FS embed.FS

View file

@ -3,7 +3,6 @@ package util
import (
"compress/gzip"
"io"
"io/ioutil"
"net/http"
"strings"
"sync"
@ -31,8 +30,8 @@ func Gzip(next http.Handler) http.Handler {
}
var gzPool = sync.Pool{
New: func() interface{} {
w := gzip.NewWriter(ioutil.Discard)
New: func() any {
w := gzip.NewWriter(io.Discard)
return w
},
}

View file

@ -35,8 +35,8 @@ func FileExists(filename string) bool {
return stat != nil
}
// InStringList returns true if needle is contained in haystack
func InStringList(haystack []string, needle string) bool {
// Contains returns true if needle is contained in haystack
func Contains[T comparable](haystack []T, needle T) bool {
for _, s := range haystack {
if s == needle {
return true
@ -45,8 +45,8 @@ func InStringList(haystack []string, needle string) bool {
return false
}
// InStringListAll returns true if all needles are contained in haystack
func InStringListAll(haystack []string, needles []string) bool {
// ContainsAll returns true if all needles are contained in haystack
func ContainsAll[T comparable](haystack []T, needles []T) bool {
matches := 0
for _, s := range haystack {
for _, needle := range needles {
@ -58,16 +58,6 @@ func InStringListAll(haystack []string, needles []string) bool {
return matches == len(needles)
}
// InIntList returns true if needle is contained in haystack
func InIntList(haystack []int, needle int) bool {
for _, s := range haystack {
if s == needle {
return true
}
}
return false
}
// SplitNoEmpty splits a string using strings.Split, but filters out empty strings
func SplitNoEmpty(s string, sep string) []string {
res := make([]string, 0)
@ -263,7 +253,7 @@ func BasicAuth(user, pass string) string {
// MaybeMarshalJSON returns a JSON string of the given object, or "<cannot serialize>" if serialization failed.
// This is useful for logging purposes where a failure doesn't matter that much.
func MaybeMarshalJSON(v interface{}) string {
func MaybeMarshalJSON(v any) string {
jsonBytes, err := json.MarshalIndent(v, "", " ")
if err != nil {
return "<cannot serialize>"
@ -280,7 +270,8 @@ func MaybeMarshalJSON(v interface{}) string {
// Warning: Never use this function with the intent to run the resulting command.
//
// Example:
// []string{"ls", "-al", "Document Folder"} -> ls -al "Document Folder"
//
// []string{"ls", "-al", "Document Folder"} -> ls -al "Document Folder"
func QuoteCommand(command []string) string {
var quoted []string
for _, c := range command {

View file

@ -2,7 +2,7 @@ package util
import (
"github.com/stretchr/testify/require"
"io/ioutil"
"os"
"path/filepath"
"testing"
)
@ -19,27 +19,27 @@ func TestRandomString(t *testing.T) {
func TestFileExists(t *testing.T) {
filename := filepath.Join(t.TempDir(), "somefile.txt")
require.Nil(t, ioutil.WriteFile(filename, []byte{0x25, 0x86}, 0600))
require.Nil(t, os.WriteFile(filename, []byte{0x25, 0x86}, 0600))
require.True(t, FileExists(filename))
require.False(t, FileExists(filename+".doesnotexist"))
}
func TestInStringList(t *testing.T) {
s := []string{"one", "two"}
require.True(t, InStringList(s, "two"))
require.False(t, InStringList(s, "three"))
require.True(t, Contains(s, "two"))
require.False(t, Contains(s, "three"))
}
func TestInStringListAll(t *testing.T) {
s := []string{"one", "two", "three", "four"}
require.True(t, InStringListAll(s, []string{"two", "four"}))
require.False(t, InStringListAll(s, []string{"three", "five"}))
require.True(t, ContainsAll(s, []string{"two", "four"}))
require.False(t, ContainsAll(s, []string{"three", "five"}))
}
func TestInIntList(t *testing.T) {
func TestContains(t *testing.T) {
s := []int{1, 2}
require.True(t, InIntList(s, 2))
require.False(t, InIntList(s, 3))
require.True(t, Contains(s, 2))
require.False(t, Contains(s, 3))
}
func TestSplitNoEmpty(t *testing.T) {

6266
web/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -82,7 +82,7 @@
"publish_dialog_attach_placeholder": "Datei von URL anhängen, z.B. https://f-droid.org/F-Droid.apk",
"publish_dialog_filename_placeholder": "Dateiname des Anhangs",
"publish_dialog_delay_label": "Verzögerung",
"publish_dialog_email_placeholder": "Adresse, an die die Benachrichtigung gesendet werden soll, z.B. phil@beispiel.com",
"publish_dialog_email_placeholder": "E-Mail-Adresse, an die die Benachrichtigung gesendet werden soll, z.B. phil@example.com",
"publish_dialog_chip_click_label": "Klick-URL",
"publish_dialog_button_cancel_sending": "Senden abbrechen",
"publish_dialog_drop_file_here": "Datei hierher ziehen",

View file

@ -0,0 +1,191 @@
{
"action_bar_show_menu": "메뉴 표시",
"action_bar_logo_alt": "ntfy 로고",
"action_bar_settings": "설정",
"action_bar_send_test_notification": "시험용 알림 발송",
"action_bar_clear_notifications": "모든 알림 초기화",
"action_bar_unsubscribe": "구독 해제",
"action_bar_toggle_mute": "알림 음소거/해제",
"action_bar_toggle_action_menu": "액션 메뉴 열기/닫기",
"message_bar_type_message": "여기에 메세지를 입력하세요",
"message_bar_error_publishing": "메세지 발송 오류",
"message_bar_show_dialog": "발송 창 표시",
"message_bar_publish": "메세지 발송",
"nav_topics_title": "구독한 주제",
"nav_button_all_notifications": "모든 알림",
"nav_button_publish_message": "알림 보내기",
"nav_button_subscribe": "주제 구독하기",
"nav_button_muted": "알림 음소거됨",
"nav_button_connecting": "연결중",
"alert_grant_title": "알림이 비활성화되어 있습니다",
"alert_grant_description": "데스크톱 알림을 받기 위해서는 브라우저에서 권한을 부여해야 합니다.",
"alert_grant_button": "권한 부여하기",
"alert_not_supported_title": "알림이 지원되지 않습니다",
"notifications_list_item": "알림",
"notifications_mark_read": "읽음으로 표시",
"notifications_delete": "삭제",
"notifications_copied_to_clipboard": "클립보드에 복사됨",
"notifications_tags": "태그",
"notifications_priority_x": "우선순위 {{priority}}",
"notifications_new_indicator": "새 알림",
"notifications_attachment_image": "첨부 이미지",
"notifications_attachment_copy_url_title": "첨부 주소를 클립보드에 복사",
"notifications_attachment_copy_url_button": "URL 복사",
"notifications_attachment_open_title": "{{url}}로 가기",
"publish_dialog_attachment_limits_file_and_quota_reached": "첨부파일 크기 제한({{fileSizeLimit}}) 초과 및 할당량 초과({{remainingBytes}} 남음)",
"publish_dialog_attachment_limits_file_reached": "첨부파일 크기 제한({{fileSizeLimit}}) 초과",
"publish_dialog_attachment_limits_quota_reached": "할당량 초과({{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_base_url_label": "서비스 URL",
"publish_dialog_base_url_placeholder": "서비스 URL, 예를 들면 https://example.com",
"publish_dialog_topic_label": "주제 이름",
"publish_dialog_topic_placeholder": "주제 이름, 예를 들면 phil_alerts",
"publish_dialog_topic_reset": "주제 초기화",
"publish_dialog_title_label": "제목",
"publish_dialog_title_placeholder": "알림 제목, 예를 들면 디스크 공간 경고",
"publish_dialog_message_label": "메세지",
"publish_dialog_message_placeholder": "메세지를 여기에 입력하세요",
"publish_dialog_tags_label": "태그",
"publish_dialog_tags_placeholder": "반점으로 구분된 태그 목록, 예를 들면 warning, srv1-backup",
"publish_dialog_priority_label": "우선순위",
"publish_dialog_click_label": "클릭 URL",
"publish_dialog_click_placeholder": "알림이 클릭되었을때 이동할 URL",
"publish_dialog_click_reset": "클릭 URL 제거",
"publish_dialog_email_label": "이메일",
"publish_dialog_email_placeholder": "알림을 전달할 이메일 주소, 예를 들면 phil@example.com",
"publish_dialog_email_reset": "이메일 전달 삭제",
"publish_dialog_attach_label": "첨부 파일 URL",
"publish_dialog_attach_placeholder": "파일을 URL로 첨부하기, 예를 들면 https://f-droid.org/F-Droid.apk",
"publish_dialog_attach_reset": "첨부 파일 URL 삭제",
"publish_dialog_filename_label": "파일 이름",
"publish_dialog_filename_placeholder": "첨부 파일 이름",
"publish_dialog_delay_label": "지연",
"publish_dialog_chip_email_label": "이메일로 전달",
"publish_dialog_chip_attach_url_label": "URL로 파일 첨부",
"publish_dialog_chip_attach_file_label": "로컬 파일 첨부",
"publish_dialog_chip_delay_label": "발송 지연",
"publish_dialog_chip_topic_label": "주제 변경",
"publish_dialog_details_examples_description": "예제와 모든 전송 기능의 자세한 설명은 <docsLink>문서</docsLink>를 참고해주세요.",
"publish_dialog_button_cancel": "취소",
"publish_dialog_button_send": "보내기",
"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": "주제 구독하기",
"subscribe_dialog_subscribe_description": "주제는 비밀번호로 보호되지 않을 수 있으니 추측하기 어려운 이름을 사용하십시오. 구독한 뒤 PUT/POST 알림을 보낼 수 있습니다.",
"subscribe_dialog_subscribe_topic_placeholder": "주제 이름, 예를 들면 phil_alerts",
"subscribe_dialog_subscribe_use_another_label": "다른 서버 사용",
"subscribe_dialog_subscribe_base_url_label": "서비스 URL",
"subscribe_dialog_subscribe_button_cancel": "취소",
"subscribe_dialog_subscribe_button_subscribe": "구독하기",
"subscribe_dialog_login_title": "로그인 필요함",
"subscribe_dialog_error_user_anonymous": "익명",
"subscribe_dialog_error_user_not_authorized": "사용자 {{username}} 은(는) 인증되지 않았습니다",
"subscribe_dialog_login_username_label": "사용자 이름, 예를 들면 phil",
"subscribe_dialog_login_password_label": "비밀번호",
"subscribe_dialog_login_button_back": "뒤로가기",
"subscribe_dialog_login_button_login": "로그인",
"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": "우선순위가 5 (최상)인 알림만 보기",
"prefs_notifications_min_priority_any": "아무 우선순위",
"prefs_notifications_min_priority_default_and_higher": "우선순위 기본 이상",
"prefs_notifications_min_priority_low_and_higher": "우선순위 낮음 이상",
"prefs_notifications_delete_after_three_hours": "3시간 뒤",
"prefs_notifications_delete_after_one_day": "1일 뒤",
"prefs_notifications_delete_after_one_week": "1주 뒤",
"prefs_notifications_delete_after_one_month": "1달 뒤",
"prefs_notifications_delete_after_never_description": "알림이 자동으로 삭제되지 않습니다",
"prefs_notifications_delete_after_three_hours_description": "알림이 3시간 뒤 자동으로 삭제됩니다",
"prefs_notifications_delete_after_one_day_description": "알림이 1일 뒤 자동으로 삭제됩니다",
"prefs_notifications_delete_after_one_week_description": "알림이 1주 뒤 자동으로 삭제됩니다",
"prefs_notifications_delete_after_one_month_description": "알림이 1달 뒤 자동으로 삭제됩니다",
"prefs_users_title": "사용자 관리",
"prefs_users_description": "이곳에서 보호된 주제를 위한 사용자를 추가하거나 삭제할 수 있습니다. 사용자 이름과 비밀번호는 브라우저의 로컬 저장소에 보관됩니다.",
"prefs_users_add_button": "사용자 추가",
"prefs_users_edit_button": "사용자 편집",
"prefs_users_delete_button": "사용자 삭제",
"prefs_users_table_user_header": "사용자",
"prefs_users_table_base_url_header": "서비스 URL",
"prefs_users_dialog_title_add": "사용자 추가",
"prefs_users_dialog_title_edit": "사용자 편집",
"prefs_users_dialog_base_url_label": "서비스 URL, 예를 들면 https://ntfy.sh",
"prefs_users_dialog_button_cancel": "취소",
"prefs_users_dialog_button_save": "저장",
"prefs_appearance_title": "표시 설정",
"prefs_users_dialog_button_add": "추가",
"prefs_appearance_language_title": "언어",
"priority_min": "최하",
"priority_low": "낮음",
"priority_default": "기본",
"priority_high": "높음",
"error_boundary_title": "이런, ntfy가 충돌했습니다",
"error_boundary_button_copy_stack_trace": "스택 트레이스 복사",
"error_boundary_stack_trace": "스택 트레이스",
"error_boundary_gathering_info": "더 많은 정보 모으기 …",
"error_boundary_unsupported_indexeddb_title": "시크릿 모드는 지원되지 않습니다",
"notifications_click_copy_url_button": "링크 복사",
"notifications_click_copy_url_title": "링크 URL을 클립보드에 복사",
"notifications_attachment_file_video": "동영상 파일",
"notifications_attachment_file_app": "안드로이드 앱 파일",
"notifications_attachment_file_document": "다른 문서",
"notifications_click_open_button": "링크 열기",
"notifications_actions_not_supported": "웹앱에서 지원되지 않는 동작입니다",
"publish_dialog_title_topic": "{{topic}}에 발송",
"alert_not_supported_description": "사용중인 브라우저에서 알림 기능을 지원하지 않습니다.",
"notifications_example": "예제",
"notifications_more_details": "더 많은 정보가 필요하시다면 <websiteLink>웹사이트</websiteLink>나 <docsLink>문서</docsLink>를 참고하세요.",
"notifications_list": "알림 목록",
"notifications_attachment_open_button": "첨부 파일 열기",
"notifications_no_subscriptions_title": "아직 아무런 구독을 추가하지 않으신 것 같습니다.",
"nav_button_settings": "설정",
"nav_button_documentation": "문서",
"notifications_attachment_link_expires": "링크가 {{date}}에 만료됨",
"notifications_attachment_link_expired": "다운로드 링크 만료됨",
"notifications_attachment_file_audio": "음성 파일",
"notifications_attachment_file_image": "사진 파일",
"notifications_actions_open_url_title": "{{url}]로 가기",
"notifications_actions_http_request_title": "HTTP {{method}}를 {{url}}에 보내기",
"notifications_none_for_topic_title": "아직 이 주제 관련 알림을 받지 않았습니다.",
"notifications_none_for_any_title": "아직 어떤 알림도 받지 않았습니다.",
"notifications_none_for_any_description": "알림을 받으려면 아래 주소로 PUT이나 POST 요청을 보내세요. 구독중이신 주제 중 하나로 예를 들자면 다음과 같습니다.",
"notifications_loading": "알림 불러오는중 …",
"publish_dialog_message_published": "알림 발송됨",
"notifications_none_for_topic_description": "알림을 받으려면 아래 주소로 PUT이나 POST 요청을 보내세요.",
"notifications_no_subscriptions_description": "\"{{linktext}}\" 링크를 눌러서 주제를 생성하거나 구독하세요. 그 다음, 메세지를 PUT이나 POST로 보내면 여기에서 알림을 받으실 수 있습니다.",
"publish_dialog_progress_uploading": "업로드중 …",
"publish_dialog_title_no_topic": "알림 발송",
"publish_dialog_progress_uploading_detail": "업로드중 {{loaded}}/{{total}} ({{percent}}%) …",
"publish_dialog_delay_placeholder": "알림 발송 지연, 예를 들면 {{unixTimestamp}}, {{relativeTime}} 또는 \"{{naturalLanguage}}\" (영어로 입력)",
"publish_dialog_delay_reset": "발송 지연 삭제",
"publish_dialog_chip_click_label": "클릭 URL",
"subscribe_dialog_login_description": "이 주제는 비밀번호로 보호되어 있습니다. 구독하시려면 사용자 이름과 비밀번호를 입력해주세요.",
"prefs_notifications_min_priority_max_only": "우선순위 최상만",
"publish_dialog_other_features": "다른 기능:",
"prefs_notifications_min_priority_description_any": "우선순위 무관 모든 알림 보기",
"prefs_notifications_min_priority_high_and_higher": "우선순위 높음 이상",
"error_boundary_unsupported_indexeddb_description": "ntfy 웹 앱은 동작하기 위해서 IndexedDB가 필요하지만 사용중이신 브라우저는 IndexedDB를 시크릿 모드에서 지원하지 않습니다.<br/><br/>안타깝지만 모든 정보는 브라우저에만 저장되므로 ntfy 웹앱을 시크릿 모드에서 사용할 이유는 존재하지 않습니다. <githubLink>이 깃허브 이슈</githubLink>를 참고해 보시거나, <discordLink>디스코드 서버</discordLink>나 <matrixLink>Matrix</matrixLink>에서 저희와 이야기를 나눌 수 있습니다.",
"prefs_notifications_delete_after_title": "알림 삭제",
"prefs_notifications_delete_after_never": "삭제하지 않음",
"prefs_users_table": "사용자 테이블",
"prefs_users_dialog_username_label": "사용자 이름, 예를 들면 phil",
"prefs_users_dialog_password_label": "비밀번호",
"priority_max": "최상",
"error_boundary_description": "이것은 당연히 발생되어서는 안됩니다. 굉장히 죄송합니다.<br/>가능하시다면 <githubLink>이 문제를 깃허브에 제보</githubLink>해 주시거나, <discordLink>디스코드 서버</discordLink>나 <matrixLink>Matrix</matrixLink>를 통해 알려주세요."
}

View file

@ -0,0 +1,191 @@
{
"action_bar_send_test_notification": "Wyślij powiadomienie testowe",
"action_bar_clear_notifications": "Wyczyść powiadomienia",
"action_bar_toggle_mute": "Włączanie/wyłączanie wyciszania powiadomień",
"action_bar_toggle_action_menu": "Otwórz/zamknij menu działań",
"message_bar_type_message": "Wpisz wiadomość tutaj",
"message_bar_error_publishing": "Błąd przy wysyłaniu powiadomienia",
"message_bar_show_dialog": "Pokaż okno dialogowe publikacji",
"nav_button_all_notifications": "Wszystkie powiadomienia",
"nav_button_documentation": "Dokumentacja",
"nav_button_muted": "Powiadomienia wyciszone",
"alert_grant_title": "Powiadomienia są wyłączone",
"alert_grant_description": "Udziel przeglądarce pozwolenia na wyświetlanie powiadomień na pulpicie.",
"alert_grant_button": "Pozwól teraz",
"alert_not_supported_title": "Powiadomienia nie są obsługiwane",
"alert_not_supported_description": "Powiadomienia nie są obsługiwane przez Twoją przeglądarkę.",
"notifications_list": "Lista powiadomień",
"notifications_list_item": "Powiadomienie",
"notifications_mark_read": "Oznacz jako przeczytane",
"notifications_delete": "Usuń",
"notifications_copied_to_clipboard": "Skopiowano do schowka",
"notifications_tags": "Tagi",
"message_bar_publish": "Opublikuj powiadomienie",
"nav_topics_title": "Subskrybowane tematy",
"nav_button_settings": "Ustawienia",
"nav_button_publish_message": "Opublikuj powiadomienie",
"nav_button_subscribe": "Zasubskrybuj temat",
"nav_button_connecting": "łączenie",
"notifications_attachment_image": "Obraz załącznika",
"notifications_attachment_copy_url_button": "Kopiuj Adres URL",
"notifications_attachment_link_expires": "Łącze wygasa w dniu {{date}}",
"notifications_attachment_link_expired": "Łącze do pobrania wygasło",
"notifications_attachment_file_image": "plik graficzny",
"notifications_attachment_file_video": "plik wideo",
"notifications_attachment_file_audio": "plik audio",
"notifications_attachment_file_app": "plik aplikacji Android",
"notifications_attachment_file_document": "inny dokument",
"notifications_click_copy_url_title": "Skopiuj adres URL do schowka",
"notifications_click_open_button": "Otwórz łącze",
"notifications_actions_open_url_title": "Przejdź do {{url}}",
"notifications_actions_not_supported": "Ta akcja nie jest obsługiwana w aplikacji internetowej",
"notifications_actions_http_request_title": "Wyślij HTTP {{method}} do {{url}}",
"notifications_none_for_topic_title": "Nie otrzymałeś jeszcze żadnych powiadomień dla tego tematu.",
"notifications_none_for_any_description": "Aby wysłać powiadomienia do tematu, wyślij PUT/POST do adresu URL tematu. Oto przykład z jednym z twoich tematów.",
"notifications_no_subscriptions_title": "Wygląda na to, że nie masz jeszcze żadnych subskrypcji.",
"notifications_no_subscriptions_description": "Kliknij łącze \"{{linktext}}\", aby stworzyć lub zasubskrybować temat. Następnie możesz wysyłać wiadomości za pomocą PUT lub POST i otrzymywać powiadomienia tutaj.",
"notifications_example": "Przykład",
"notifications_loading": "Ładowanie powiadomień …",
"publish_dialog_title_topic": "Opublikuj do {{topic}}",
"publish_dialog_title_no_topic": "Opublikuj powiadomienie",
"publish_dialog_progress_uploading": "Przesyłanie …",
"publish_dialog_progress_uploading_detail": "Przesyłanie {{loaded}}/{{total}} ({{percent}}%) …",
"publish_dialog_message_published": "Powiadomienie wysłane",
"publish_dialog_attachment_limits_file_and_quota_reached": "przekracza limit rozmiaru pliku {{fileSizeLimit}}, pozostaje {{remainingBytes}}",
"publish_dialog_attachment_limits_file_reached": "przekracza limit rozmiaru pliku {{filesizeLimit}}",
"publish_dialog_attachment_limits_quota_reached": "przekracza limit, {{remainingBytes}} pozostało",
"publish_dialog_emoji_picker_show": "Wybierz emotkę",
"publish_dialog_priority_min": "Min. priorytet",
"publish_dialog_priority_low": "Niski priorytet",
"publish_dialog_base_url_label": "Adres URL usługi",
"publish_dialog_base_url_placeholder": "Adres URL usługi, np. https://example.com",
"publish_dialog_topic_label": "Nazwa tematu",
"publish_dialog_topic_placeholder": "Nazwa tematu, np. moje_alerty",
"publish_dialog_topic_reset": "Resetuj temat",
"publish_dialog_title_label": "Tytuł",
"publish_dialog_title_placeholder": "Tytuł notyfikacji, np. Niski poziom baterrii",
"publish_dialog_message_label": "Wiadomość",
"publish_dialog_message_placeholder": "Wpisz wiadomość tutaj",
"publish_dialog_tags_label": "Tagi",
"publish_dialog_tags_placeholder": "Lista tagów oddzielona przecinkami, np. ostrzeżenie, srv1-backup",
"publish_dialog_priority_label": "Priorytet",
"publish_dialog_click_label": "Kliknij Adres URL",
"publish_dialog_click_placeholder": "Adres URL, który ma być otwarty po kliknięciu na powiadomienie",
"publish_dialog_click_reset": "Usuń adres URL kliknięcia",
"publish_dialog_email_label": "Email",
"publish_dialog_email_placeholder": "Adres, na który ma być wysłane powiadomienie, np. phil@example.com",
"publish_dialog_email_reset": "Usuń przekazywanie wiadomości email",
"publish_dialog_attach_label": "Adres URL załącznika",
"publish_dialog_attach_placeholder": "Dołączenie pliku z adresu URL, np. https://f-droid.org/F-Droid.apk",
"publish_dialog_attach_reset": "Usuń adres URL załącznika",
"publish_dialog_filename_label": "Nazwa pliku",
"publish_dialog_filename_placeholder": "Nazwa pliku załącznika",
"publish_dialog_delay_label": "Opóźnienie",
"publish_dialog_delay_reset": "Usuń opóźnione dostarczenie",
"publish_dialog_other_features": "Inne funkcje:",
"publish_dialog_chip_click_label": "Adres URL kliknięcia",
"publish_dialog_chip_email_label": "Przekaż na email",
"publish_dialog_chip_attach_url_label": "Dołącz plik z adresu URL",
"publish_dialog_chip_attach_file_label": "Dołącz plik lokalny",
"publish_dialog_chip_delay_label": "Opóźnienie dostawy",
"publish_dialog_chip_topic_label": "Zmień temat",
"publish_dialog_details_examples_description": "Przykłady i szczegółowe informacje na temat wszystkich opcji można znaleźć w <docsLink>dokumentacji</docsLink>.",
"publish_dialog_button_cancel_sending": "Anuluj wysyłanie",
"publish_dialog_button_send": "Wyślij",
"publish_dialog_checkbox_publish_another": "Wyślij kolejną wiadomość",
"publish_dialog_attached_file_title": "Załączony plik:",
"publish_dialog_attached_file_filename_placeholder": "Nazwa pliku załącznika",
"publish_dialog_drop_file_here": "Upuść plik tutaj",
"emoji_picker_search_placeholder": "Szukaj emotki",
"emoji_picker_search_clear": "Wyczyść wyszukiwanie",
"subscribe_dialog_subscribe_title": "Zasubskrybuj temat",
"subscribe_dialog_subscribe_topic_placeholder": "Nazwa tematu, np. moje_alerty",
"subscribe_dialog_subscribe_use_another_label": "Użyj innego serwera",
"subscribe_dialog_subscribe_base_url_label": "Adres URL usługi",
"subscribe_dialog_subscribe_button_cancel": "Anuluj",
"subscribe_dialog_login_description": "Ten temat jest chroniony hasłem. Proszę podać nazwę użytkownika i hasło, aby zasubskrybować.",
"subscribe_dialog_login_username_label": "Nazwa użytkownika, np. phil",
"subscribe_dialog_login_password_label": "Hasło",
"publish_dialog_button_cancel": "Anuluj",
"subscribe_dialog_login_button_back": "Powrót",
"subscribe_dialog_login_button_login": "Zaloguj się",
"subscribe_dialog_error_user_not_authorized": "Użytkownik {{username}} nie ma uprawnień",
"subscribe_dialog_error_user_anonymous": "anonim",
"prefs_notifications_title": "Powiadomienia",
"prefs_notifications_sound_title": "Dźwięk powiadomienia",
"prefs_notifications_sound_description_none": "Brak dźwięku po otrzymaniu powiadomienia",
"prefs_notifications_sound_description_some": "Odtwarzaj dźwięk {{sound}}, gdy nadejdzie powiadomienie",
"prefs_notifications_sound_play": "Odtwórz wybrany dźwięk",
"prefs_notifications_min_priority_title": "Minimalny priorytet",
"prefs_notifications_min_priority_description_any": "Pokaż wszystkie powiadomienia, niezależnie od priorytetu",
"prefs_notifications_min_priority_description_x_or_higher": "Pokazuj powiadomienia, gdy ich priorytet to {{number}} ({{name}}) lub wyższy",
"prefs_notifications_min_priority_description_max": "Pokaż powiadomienia, jeśli priorytet wynosi 5 (max)",
"prefs_notifications_min_priority_any": "Dowolny priorytet",
"prefs_notifications_min_priority_low_and_higher": "Niski priorytet i wyższy",
"prefs_notifications_min_priority_default_and_higher": "Priorytet standardowy i wyższy",
"prefs_notifications_min_priority_high_and_higher": "Wysoki priorytet i wyższy",
"prefs_notifications_delete_after_one_day": "Po jednym dniu",
"prefs_notifications_delete_after_one_week": "Po tygodniu",
"prefs_notifications_delete_after_one_month": "Po miesiącu",
"prefs_notifications_delete_after_never_description": "Powiadomienia nigdy nie są automatycznie usuwane",
"prefs_notifications_delete_after_three_hours_description": "Powiadomienia są automatycznie usuwane po trzech godzinach",
"prefs_notifications_delete_after_one_day_description": "Powiadomienia są automatycznie usuwane po jednym dniu",
"prefs_notifications_delete_after_one_month_description": "Powiadomienia są automatycznie usuwane po upływie jednego miesiąca",
"prefs_notifications_delete_after_one_week_description": "Powiadomienia są automatycznie usuwane po upływie jedego tygodnia",
"prefs_users_title": "Zarządzaj użytkownikami",
"prefs_users_description": "Dodaj/usuń użytkowników dla tematów chronionych hasłem. Uwaga: Nazwa użytkownika i hasło są przechowywane w lokalnej pamięci przeglądarki.",
"prefs_users_table": "Tabela użytkowników",
"prefs_users_add_button": "Dodaj użytkownika",
"notifications_attachment_open_button": "Otwórz załącznik",
"prefs_users_edit_button": "Edytuj użytkownika",
"prefs_users_delete_button": "Usuń użytkownika",
"prefs_users_table_base_url_header": "Adres URL usługi",
"prefs_users_dialog_title_add": "Dodaj użytkownika",
"prefs_users_dialog_button_cancel": "Anuluj",
"prefs_users_dialog_button_add": "Dodaj",
"prefs_users_dialog_button_save": "Zapisz",
"prefs_appearance_title": "Wygląd",
"prefs_appearance_language_title": "Język",
"error_boundary_title": "Oh nie, ntfy przestało działać",
"error_boundary_description": "Oczywiście, to nie miało się wydarzyć. Bardzo przepraszam za to.<br/>Jeśli masz minutę, proszę <githubLink>zgłoś to na GitHubie</githubLink>, albo daj nam znać przez <discordLink>Discord</discordLink> lub <matrixLink>Matrix</matrixLink>.",
"error_boundary_button_copy_stack_trace": "Kopiuj stack trace",
"error_boundary_stack_trace": "Stack trace",
"error_boundary_gathering_info": "Zbierz więcej informacji …",
"error_boundary_unsupported_indexeddb_title": "Prywatne karty przeglądarki nie są obsługiwane",
"action_bar_show_menu": "Pokaż menu",
"action_bar_logo_alt": "ntfy logo",
"action_bar_unsubscribe": "Zrezygnuj z subskrypcji",
"notifications_attachment_copy_url_title": "Kopiuj adres URL załącznika do schowka",
"action_bar_settings": "Ustawienia",
"notifications_priority_x": "Priorytet {{priority}}",
"notifications_new_indicator": "Nowe powiadomienie",
"notifications_attachment_open_title": "Przejdź do {{url}}",
"notifications_click_copy_url_button": "Skopiuj łącze",
"notifications_none_for_topic_description": "Aby wysłać powiadomienia do tego tematu, wyślij PUT lub POST-Request na adres URL tematu.",
"notifications_none_for_any_title": "Nie otrzymałeś żadnych powiadomień.",
"notifications_more_details": "Bardziej szczegółowe informacje można znaleźć na <websiteLink>stronie internetowej</websiteLink> oraz w <docsLink>dokumentacji</docsLink>.",
"publish_dialog_priority_default": "Domyślny priorytet",
"publish_dialog_priority_max": "Max. priorytet",
"publish_dialog_priority_high": "Wysoki priorytet",
"publish_dialog_delay_placeholder": "Opóźnienie dostarczenie, np.{{unixTimestamp}}, {{relativeTime}}, lub \"{{naturalLanguage}}\" (tylko w języku angielskim)",
"subscribe_dialog_subscribe_button_subscribe": "Subskrybuj",
"prefs_users_table_user_header": "Użytkownik",
"publish_dialog_attached_file_remove": "Usuń załączony plik",
"subscribe_dialog_subscribe_description": "Tematy nie mogą być chronione hasłem, więc wybierz trudną do odgadnięcia nazwę. Po zasubskrybowaniu możesz wysyłać powiadomienia poprzez POST/PUT.",
"subscribe_dialog_login_title": "Wymagane jest zalogowanie się",
"prefs_notifications_delete_after_title": "Usuń powiadomienia",
"prefs_users_dialog_password_label": "Hasło",
"priority_low": "niski",
"priority_default": "podstawowy",
"priority_max": "maksymalny",
"prefs_notifications_delete_after_three_hours": "Po trzech godzinach",
"prefs_users_dialog_base_url_label": "Adres URL usługi, np. https://ntfy.sh",
"prefs_notifications_sound_no_sound": "Bez dzwięku",
"prefs_users_dialog_username_label": "Nazwa użytkownika, np. phil",
"priority_high": "wysoki",
"prefs_notifications_min_priority_max_only": "Tylko maksymalny priorytet",
"prefs_notifications_delete_after_never": "Nigdy",
"prefs_users_dialog_title_edit": "Edytuj użytkownika",
"priority_min": "minimum",
"error_boundary_unsupported_indexeddb_description": "Aplikacja ntfy potrzebuje IndexedDB, aby działać poprawnie, a Twoja przeglądarka nie obsługuje IndexedDB w prywatnych zakładkach.<br/><br/>To denerwujące, ale używanie ntfy w prywatnej zakładce nie ma sensu, ponieważ wszystkie dane są przechowywane w przeglądarce. Więcej informacji można uzyskać <githubLink>w tym wydaniu GitHub</githubLink>, lub na czacie w <discordLink>Discord</discordLink> lub <matrixLink>Matrix</matrixLink>."
}

View file

@ -0,0 +1,191 @@
{
"action_bar_logo_alt": "логотип ntfy",
"action_bar_settings": "Налаштування",
"message_bar_type_message": "Введіть повідомлення тут",
"message_bar_error_publishing": "Помилка публікації сповіщення",
"message_bar_show_dialog": "Показати діалогове вікно публікації",
"nav_topics_title": "Підписки на теми",
"nav_button_settings": "Налаштування",
"nav_button_documentation": "Документація",
"nav_button_subscribe": "Підписатися на тему",
"nav_button_muted": "Сповіщення вимкнено",
"nav_button_connecting": "підключення",
"alert_grant_title": "Сповіщення вимкнено",
"alert_grant_description": "Дозвольте браузеру показувати сповіщення.",
"alert_grant_button": "Дозволити",
"alert_not_supported_title": "Сповіщення не підтримуються",
"notifications_list_item": "Сповіщення",
"notifications_attachment_image": "Прикріплене зображення",
"notifications_attachment_open_title": "Перейти на {{url}}",
"notifications_attachment_open_button": "Відкрити вкладення",
"notifications_attachment_link_expires": "термін дії посилання закінчується {{date}}",
"notifications_actions_http_request_title": "Надіслати HTTP {{method}} на {{url}}",
"notifications_none_for_any_title": "Ви не отримали жодних сповіщень.",
"notifications_no_subscriptions_description": "Натисніть \"{{linktext}}\" посилання, щоб створити або підписатися на тему. Після цього ви зможете надсилати повідомлення за допомогою PUT або POST, і ви отримуватимете тут повідомлення.",
"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_message_published": "Сповіщення опубліковано",
"publish_dialog_attachment_limits_quota_reached": "перевищує квоту, залишилося {{remainingBytes}}",
"publish_dialog_priority_low": "Низький пріоритет",
"publish_dialog_topic_label": "Назва теми",
"publish_dialog_topic_placeholder": "Назва теми, наприклад phil_alerts",
"publish_dialog_topic_reset": "Скинути тему",
"publish_dialog_title_label": "Заголовок",
"publish_dialog_title_placeholder": "Заголовок сповіщення, наприклад Сповіщення про дисковий простір",
"publish_dialog_message_label": "Повідомлення",
"publish_dialog_message_placeholder": "Введіть повідомлення",
"publish_dialog_tags_label": "Теги",
"publish_dialog_tags_placeholder": "Список тегів розділений комою, наприклад warning, srv1-backup",
"publish_dialog_click_placeholder": "URL-адреса, яка відкривається після натискання сповіщення",
"publish_dialog_email_label": "Електронна пошта",
"publish_dialog_attach_placeholder": "Прикріпіть файл за URL-адресою, наприклад https://f-droid.org/F-Droid.apk",
"publish_dialog_attach_reset": "Видалити URL вкладення",
"publish_dialog_filename_placeholder": "Ім'я файлу вкладення",
"publish_dialog_delay_reset": "Видалити затримку доставлення",
"publish_dialog_chip_click_label": "Адреса",
"publish_dialog_chip_email_label": "Переслати на електронну пошту",
"publish_dialog_chip_topic_label": "Змінити тему",
"publish_dialog_attached_file_remove": "Видалити прикріплений файл",
"subscribe_dialog_subscribe_topic_placeholder": "Назва теми, наприклад phil_alerts",
"subscribe_dialog_subscribe_use_another_label": "Використовувати інший сервер",
"subscribe_dialog_subscribe_base_url_label": "URL служби",
"subscribe_dialog_login_password_label": "Пароль",
"subscribe_dialog_login_button_back": "Назад",
"subscribe_dialog_error_user_not_authorized": "{{username}} користувач не авторизований",
"prefs_notifications_sound_description_none": "Сповіщення не відтворюють жодного звуку при надходженні",
"prefs_notifications_sound_description_some": "Сповіщення відтворюють звук {{sound}}",
"prefs_notifications_min_priority_description_any": "Показати всі сповіщень, незалежно від пріоритету",
"prefs_notifications_min_priority_any": "Будь-який пріоритет",
"prefs_notifications_min_priority_default_and_higher": "Пріоритет за замовчуванням та високий",
"prefs_notifications_delete_after_title": "Видалити сповіщення",
"prefs_notifications_delete_after_never": "Ніколи",
"prefs_notifications_delete_after_one_day": "Через день",
"prefs_notifications_delete_after_one_week": "Через тиждень",
"prefs_notifications_delete_after_one_month": "Через місяць",
"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_users_title": "Керувати користувачами",
"prefs_users_table": "Таблиця користувачів",
"prefs_users_edit_button": "Редагувати користувача",
"prefs_users_dialog_button_save": "Зберегти",
"prefs_appearance_title": "Зовнішній вигляд",
"priority_default": "за замовчуванням",
"priority_high": "високий",
"priority_max": "макс",
"error_boundary_title": "Ой, ntfy впав",
"error_boundary_button_copy_stack_trace": "Копіювати трасування стека",
"action_bar_show_menu": "Показати меню",
"action_bar_toggle_action_menu": "Відкрити/закрити меню",
"action_bar_send_test_notification": "Надіслати тестове сповіщення",
"action_bar_clear_notifications": "Очистити всі сповіщення",
"action_bar_toggle_mute": "Вимкнути/увімкнути сповіщення",
"action_bar_unsubscribe": "Відписатися",
"message_bar_publish": "Опублікувати повідомлення",
"nav_button_all_notifications": "Усі сповіщення",
"alert_not_supported_description": "Ваш браузер не підтримує сповіщення.",
"notifications_list": "Список сповіщень",
"notifications_mark_read": "Позначити як прочитане",
"notifications_delete": "Видалити",
"notifications_tags": "Теги",
"nav_button_publish_message": "Опублікувати сповіщення",
"notifications_attachment_copy_url_title": "Копіювати URL-адресу вкладення",
"notifications_attachment_link_expired": "термін дії посилання для завантаження закінчився",
"publish_dialog_progress_uploading_detail": "Завантажується {{loaded}}/{{total}} ({{percent}}%) …",
"notifications_priority_x": "Пріоритет {{priority}}",
"notifications_attachment_copy_url_button": "Копіювати URL-адресу",
"notifications_copied_to_clipboard": "Скопійовано в буфер обміну",
"notifications_attachment_file_video": "відео файл",
"notifications_attachment_file_audio": "звуковий файл",
"publish_dialog_emoji_picker_show": "Виберіть емодзі",
"notifications_new_indicator": "Нове сповіщення",
"notifications_attachment_file_image": "файл зображення",
"notifications_attachment_file_document": "інший документ",
"notifications_click_copy_url_title": "Копіювати URL-адресу посилання",
"notifications_click_copy_url_button": "Копіювати посилання",
"notifications_actions_not_supported": "Дія не підтримується у браузері",
"notifications_attachment_file_app": "Файл програми Android",
"notifications_click_open_button": "Відкрити посилання",
"notifications_actions_open_url_title": "Перейти на {{url}}",
"notifications_none_for_topic_description": "Щоб надіслати сповіщення до цієї теми, просто надішліть PUT або POST на URL-адресу цієї теми.",
"notifications_no_subscriptions_title": "Схоже, у вас ще немає жодної підписки.",
"publish_dialog_drop_file_here": "Перетягніть файл сюди",
"notifications_none_for_topic_title": "Ви ще не отримували сповіщення на цю тему.",
"notifications_example": "Приклад",
"notifications_none_for_any_description": "Щоб надіслати сповіщення до теми, просто надішліть PUT або POST на URL-адресу теми. Ось приклад, використовуючи одну з ваших тем.",
"publish_dialog_attachment_limits_file_and_quota_reached": "перевищує {{fileSizeLimit}} розмір файлу, {{remainingBytes}} залишилося",
"publish_dialog_priority_default": "Пріоритет за замовчуванням",
"publish_dialog_attachment_limits_file_reached": "перевищує {{fileSizeLimit}} розмір файлу",
"publish_dialog_priority_min": "Мін. пріоритет",
"publish_dialog_priority_high": "Високий пріоритет",
"publish_dialog_priority_max": "Макс. пріоритет",
"publish_dialog_base_url_placeholder": "URL-адреса сервісу, наприклад https://example.com",
"publish_dialog_base_url_label": "URL служби",
"publish_dialog_other_features": "Інші можливості:",
"publish_dialog_chip_attach_file_label": "Прикріпити локальний файл",
"publish_dialog_priority_label": "Пріоритет",
"publish_dialog_click_label": "Натисніть URL",
"publish_dialog_click_reset": "Видалити URL-адресу для натискання",
"publish_dialog_email_placeholder": "Адреса для пересилання сповіщення, наприклад phil@example.com",
"publish_dialog_attach_label": "URL-адреса вкладення",
"publish_dialog_filename_label": "Ім'я файлу",
"publish_dialog_delay_label": "Затримка",
"publish_dialog_email_reset": "Видалити пересилання електронної пошти",
"publish_dialog_chip_attach_url_label": "Прикріпити файл за URL",
"publish_dialog_details_examples_description": "Приклади та докладний опис усіх функцій, зверніться до <docsLink>документації</docsLink>.",
"publish_dialog_button_cancel_sending": "Скасувати відправку",
"publish_dialog_attached_file_filename_placeholder": "Ім'я прикріпленого файлу",
"publish_dialog_delay_placeholder": "Затримка доставлення, наприклад {{unixTimestamp}}, {{relativeTime}} або \"{{naturalLanguage}}\" (лише англійською)",
"publish_dialog_button_send": "Надіслати",
"publish_dialog_checkbox_publish_another": "Опублікувати ще",
"publish_dialog_chip_delay_label": "Затримка доставлення",
"publish_dialog_button_cancel": "Скасувати",
"publish_dialog_attached_file_title": "Прикріплений файл:",
"subscribe_dialog_subscribe_description": "Теми можуть не бути захищені паролем, тому виберіть назву, яку нелегко вгадати. Після підписки ви можете PUT/POST сповіщення.",
"emoji_picker_search_placeholder": "Пошук емодзі",
"emoji_picker_search_clear": "Очистити пошук",
"subscribe_dialog_subscribe_title": "Підпишіться на тему",
"subscribe_dialog_login_username_label": "Ім'я користувача, наприклад phil",
"prefs_notifications_title": "Сповіщення",
"subscribe_dialog_subscribe_button_cancel": "Скасувати",
"subscribe_dialog_subscribe_button_subscribe": "Підписатися",
"subscribe_dialog_error_user_anonymous": "анонімний",
"subscribe_dialog_login_title": "Потрібна авторизація",
"subscribe_dialog_login_description": "Ця тема захищена паролем. Будь ласка, введіть ім'я користувача та пароль, щоб підписатися.",
"prefs_notifications_sound_title": "Звук сповіщення",
"subscribe_dialog_login_button_login": "Логін",
"prefs_notifications_sound_no_sound": "Без звука",
"prefs_notifications_sound_play": "Відтворення вибраного звуку",
"prefs_users_description": "Додайте/видаляйте користувачів для захищених тем. Зверніть увагу, що ім'я користувача та пароль зберігаються у локальному сховищі браузера.",
"prefs_notifications_min_priority_title": "Мінімальний пріоритет",
"prefs_notifications_min_priority_high_and_higher": "Високий пріоритет і вище",
"prefs_notifications_min_priority_description_x_or_higher": "Показувати сповіщення, якщо пріоритет {{number}} ({{name}}) або вище",
"prefs_notifications_min_priority_description_max": "Показувати сповіщення, якщо пріоритет 5 (макс.)",
"prefs_notifications_min_priority_low_and_higher": "Низький та високий пріоритет",
"prefs_notifications_min_priority_max_only": "Тільки максимальний пріоритет",
"prefs_users_table_base_url_header": "URL служби",
"prefs_users_dialog_password_label": "Пароль",
"prefs_notifications_delete_after_three_hours": "Через три години",
"prefs_users_add_button": "Додати користувача",
"prefs_users_dialog_title_edit": "Редагувати користувача",
"prefs_users_dialog_base_url_label": "URL-адреса служби, наприклад https://ntfy.sh",
"prefs_users_delete_button": "Видалити користувача",
"prefs_users_table_user_header": "Користувач",
"prefs_users_dialog_title_add": "Додати користувача",
"prefs_users_dialog_username_label": "Ім'я користувача, наприклад phil",
"prefs_users_dialog_button_cancel": "Скасувати",
"prefs_users_dialog_button_add": "Додати",
"prefs_appearance_language_title": "Мова",
"error_boundary_gathering_info": "Зберіть більше інформації…",
"priority_min": "мін",
"error_boundary_description": "Очевидно, цього не повинно статися. Дуже шкода.<br/>Якщо у вас є хвилина, <githubLink>повідомте про це на GitHub</githubLink> або повідомте нам через <discordLink>Discord</discordLink> або <matrixLink>Matrix</matrixLink> .",
"priority_low": "низький",
"error_boundary_stack_trace": "Трасування стека",
"error_boundary_unsupported_indexeddb_title": "Приватний перегляд не підтримується",
"error_boundary_unsupported_indexeddb_description": "Веб-програма ntfy потребує IndexedDB для роботи, а ваш браузер не підтримує IndexedDB у режимі приватного перегляду.<br/><br/>На жаль, використання ntfy web не має сенсу у режимі приватного перегляду, оскільки все зберігається в пам’яті браузера. Ви можете прочитати більше про це <githubLink>у цьому випуску GitHub</githubLink> або поспілкуватися з нами на <discordLink>Discord</discordLink> або <matrixLink>Matrix</matrixLink>."
}

View file

@ -118,8 +118,8 @@
"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_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": "一月后",
@ -186,6 +186,6 @@
"prefs_users_edit_button": "编辑用户",
"publish_dialog_tags_placeholder": "英文逗号分隔标记列表,例如 warning, srv1-backup",
"publish_dialog_details_examples_description": "有关所有发送功能的示例和详细说明,请参阅<docsLink>文档</docsLink>。",
"subscribe_dialog_subscribe_description": "主题可能不受密码保护,因此请选择一个不容易猜测的名字。订阅后,您可以使用 PUT/POST 通知。",
"subscribe_dialog_subscribe_description": "主题可能不受密码保护,因此请选择一个不容易被猜中的名字。订阅后,您可以使用 PUT/POST 通知。",
"publish_dialog_delay_placeholder": "延期投递,例如 {{unixTimestamp}}、{{relativeTime}}或「{{naturalLanguage}}」(仅限英语)"
}

View file

@ -436,7 +436,7 @@ const Appearance = () => {
const Language = () => {
const { t, i18n } = useTranslation();
const labelId = "prefLanguage";
const randomFlags = shuffle(["🇬🇧", "🇺🇸", "🇪🇸", "🇫🇷", "🇧🇬", "🇨🇿", "🇩🇪", "🇨🇳", "🇮🇹", "🇭🇺", "🇧🇷", "🇳🇱", "🇮🇩", "🇯🇵", "🇷🇺", "🇹🇷"]).slice(0, 3);
const randomFlags = shuffle(["🇬🇧", "🇺🇸", "🇪🇸", "🇫🇷", "🇧🇬", "🇨🇿", "🇩🇪", "🇵🇱", "🇺🇦", "🇨🇳", "🇮🇹", "🇭🇺", "🇧🇷", "🇳🇱", "🇮🇩", "🇯🇵", "🇷🇺", "🇹🇷"]).slice(0, 3);
const title = t("prefs_appearance_language_title") + " " + randomFlags.join(" ");
const lang = i18n.language ?? "en";
@ -458,10 +458,13 @@ const Language = () => {
<MenuItem value="fr">Français</MenuItem>
<MenuItem value="it">Italiano</MenuItem>
<MenuItem value="hu">Magyar</MenuItem>
<MenuItem value="ko">한국어</MenuItem>
<MenuItem value="ja">日本語</MenuItem>
<MenuItem value="nl">Nederlands</MenuItem>
<MenuItem value="nb_NO">Norsk bokmål</MenuItem>
<MenuItem value="uk">Українська</MenuItem>
<MenuItem value="pt_BR">Português (Brasil)</MenuItem>
<MenuItem value="pl">Polski</MenuItem>
<MenuItem value="ru">Русский</MenuItem>
<MenuItem value="tr">Türkçe</MenuItem>
</Select>