diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 00000000..45a5ae2a --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,39 @@ +name: build +on: [push, pull_request] +jobs: + build: + runs-on: ubuntu-latest + steps: + - + name: Install Go + uses: actions/setup-go@v2 + with: + go-version: '1.18.x' + - + name: Install node + uses: actions/setup-node@v2 + with: + node-version: '16' + - + name: Checkout code + uses: actions/checkout@v2 + - + name: Cache Go and npm modules + uses: actions/cache@v3 + with: + path: | + ~/go/pkg/mod + ~/go/bin + ~/.npm + web/node_modules + key: ${{ runner.os }}-ntfy-${{ hashFiles('**/go.sum', '**/package.lock') }} + restore-keys: ${{ runner.os }}-ntfy- + - + name: Install dependencies + run: make build-deps-ubuntu + - + name: Build all the things + run: make build + - + name: Print build results and checksums + run: make cli-build-results diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 31fdfe20..00000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,72 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ main ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ main ] - schedule: - - cron: '21 10 * * 5' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'go', 'javascript' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 00000000..a2a6e335 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,50 @@ +name: release +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' +jobs: + release: + runs-on: ubuntu-latest + steps: + - + name: Install Go + uses: actions/setup-go@v2 + with: + go-version: '1.18.x' + - + name: Install node + uses: actions/setup-node@v2 + with: + node-version: '16' + - + name: Checkout code + uses: actions/checkout@v2 + - + name: Cache Go and npm modules + uses: actions/cache@v3 + with: + path: | + ~/go/pkg/mod + ~/go/bin + ~/.npm + web/node_modules + key: ${{ runner.os }}-ntfy-${{ hashFiles('**/go.sum', '**/package.lock') }} + restore-keys: ${{ runner.os }}-ntfy- + - + name: Docker login + uses: docker/login-action@v2 + with: + username: ${{ github.repository_owner }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + - + name: Install dependencies + run: make build-deps-ubuntu + - + name: Build and publish + run: make release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - + name: Print build results and checksums + run: make cli-build-results diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 0b63387f..96c43d85 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -3,26 +3,46 @@ on: [push, pull_request] jobs: test: runs-on: ubuntu-latest - steps: - - name: Install Go + steps: + - + name: Install Go uses: actions/setup-go@v2 with: - go-version: '1.17.x' - - name: Install node + go-version: '1.18.x' + - + name: Install node uses: actions/setup-node@v2 with: node-version: '16' - - name: Checkout code + - + name: Checkout code uses: actions/checkout@v2 - - name: Install dependencies - run: sudo apt update && sudo apt install -y python3-pip curl - - name: Build docs (required for tests) + - + name: Cache Go and npm modules + uses: actions/cache@v3 + with: + path: | + ~/go/pkg/mod + ~/go/bin + ~/.npm + web/node_modules + key: ${{ runner.os }}-ntfy-${{ hashFiles('**/go.sum', '**/package.lock') }} + restore-keys: ${{ runner.os }}-ntfy- + - + name: Install dependencies + run: make build-deps-ubuntu + - + name: Build docs (required for tests) run: make docs - - name: Build web app (required for tests) + - + name: Build web app (required for tests) run: make web - - name: Run tests, formatting, vetting and linting + - + name: Run tests, formatting, vetting and linting run: make check - - name: Run coverage + - + name: Run coverage run: make coverage - - name: Upload coverage to codecov.io + - + name: Upload coverage to codecov.io run: make coverage-upload diff --git a/.goreleaser.yml b/.goreleaser.yml index 3be24147..ea728840 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -157,6 +157,7 @@ universal_binaries: - id: ntfy_darwin_all replace: true + name_template: ntfy checksum: name_template: 'checksums.txt' snapshot: diff --git a/Makefile b/Makefile index b58e62e3..f2e741d5 100644 --- a/Makefile +++ b/Makefile @@ -79,6 +79,18 @@ build: web docs cli update: web-deps-update cli-deps-update docs-deps-update docker pull alpine +# Ubuntu-specific + +build-deps-ubuntu: + sudo apt update + sudo apt install -y \ + curl \ + gcc-aarch64-linux-gnu \ + gcc-arm-linux-gnueabi \ + upx \ + jq + which pip3 || sudo apt install -y python3-pip + # Documentation docs: docs-deps docs-build @@ -114,28 +126,29 @@ web-deps: web-deps-update: cd web && npm update + # Main server/client build cli: cli-deps - goreleaser build --snapshot --rm-dist --debug + goreleaser build --snapshot --rm-dist cli-linux-amd64: cli-deps-static-sites - goreleaser build --snapshot --rm-dist --debug --id ntfy_linux_amd64 + goreleaser build --snapshot --rm-dist --id ntfy_linux_amd64 cli-linux-armv6: cli-deps-static-sites cli-deps-gcc-armv6-armv7 - goreleaser build --snapshot --rm-dist --debug --id ntfy_linux_armv6 + goreleaser build --snapshot --rm-dist --id ntfy_linux_armv6 cli-linux-armv7: cli-deps-static-sites cli-deps-gcc-armv6-armv7 - goreleaser build --snapshot --rm-dist --debug --id ntfy_linux_armv7 + goreleaser build --snapshot --rm-dist --id ntfy_linux_armv7 cli-linux-arm64: cli-deps-static-sites cli-deps-gcc-arm64 - goreleaser build --snapshot --rm-dist --debug --id ntfy_linux_arm64 + goreleaser build --snapshot --rm-dist --id ntfy_linux_arm64 cli-windows-amd64: cli-deps-static-sites - goreleaser build --snapshot --rm-dist --debug --id ntfy_windows_amd64 + goreleaser build --snapshot --rm-dist --id ntfy_windows_amd64 cli-darwin-all: cli-deps-static-sites - goreleaser build --snapshot --rm-dist --debug --id ntfy_darwin_all + goreleaser build --snapshot --rm-dist --id ntfy_darwin_all cli-linux-server: cli-deps-static-sites # This is a target to build the CLI (including the server) manually. @@ -177,6 +190,7 @@ cli-deps-static-sites: cli-deps-all: which upx || { echo "ERROR: upx not installed. On Ubuntu, run: apt install upx"; exit 1; } + go install github.com/goreleaser/goreleaser@latest cli-deps-gcc-armv6-armv7: which arm-linux-gnueabi-gcc || { echo "ERROR: ARMv6/ARMv7 cross compiler not installed. On Ubuntu, run: apt install gcc-arm-linux-gnueabi"; exit 1; } @@ -187,6 +201,18 @@ cli-deps-gcc-arm64: cli-deps-update: go get -u go install honnef.co/go/tools/cmd/staticcheck@latest + go install golang.org/x/lint/golint@latest + go install github.com/goreleaser/goreleaser@latest + +cli-build-results: + cat dist/config.yaml + [ -f dist/artifacts.json ] && cat dist/artifacts.json | jq . || true + [ -f dist/metadata.json ] && cat dist/metadata.json | jq . || true + [ -f dist/checksums.txt ] && cat dist/checksums.txt || true + find dist -maxdepth 2 -type f \ + \( -name '*.deb' -or -name '*.rpm' -or -name '*.zip' -or -name '*.tar.gz' -or -name 'ntfy' \) \ + -and -not -path 'dist/goreleaserdocker*' \ + -exec sha256sum {} \; # Test/check targets @@ -238,13 +264,13 @@ staticcheck: .PHONY # Releasing targets -release: clean update cli-deps release-check-tags docs web check - goreleaser release --rm-dist --debug +release: clean update cli-deps release-checks docs web check + goreleaser release --rm-dist release-snapshot: clean update cli-deps docs web check - goreleaser release --snapshot --skip-publish --rm-dist --debug + goreleaser release --snapshot --skip-publish --rm-dist -release-check-tags: +release-checks: $(eval LATEST_TAG := $(shell git describe --abbrev=0 --tags | cut -c2-)) if ! grep -q $(LATEST_TAG) docs/install.md; then\ echo "ERROR: Must update docs/install.md with latest tag first.";\ @@ -254,6 +280,10 @@ release-check-tags: echo "ERROR: Must update docs/releases.md with latest tag first.";\ exit 1;\ fi + if [ -n "$(shell git status -s)" ]; then\ + echo "ERROR: Git repository is in an unclean state.";\ + exit 1;\ + fi # Installing targets diff --git a/README.md b/README.md index 94ac1d4f..f6969459 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,16 @@ too. [Install / Self-hosting](https://ntfy.sh/docs/install/) | [Building](https://ntfy.sh/docs/develop/) +## Chat +You can directly contact me **[on Discord](https://discord.gg/cT7ECsZj9w)** or [on Matrix](https://matrix.to/#/#ntfy:matrix.org) +(bridged from Discord), or via the [GitHub issues](https://github.com/binwiederhier/ntfy/issues), or find more contact information +[on my website](https://heckel.io/about). + +## Announcements / beta testers +For announcements of new releases and cutting-edge beta versions, please subscribe to the [ntfy.sh/announcements](https://ntfy.sh/announcements) +topic. If you'd like to test the iOS app, join [TestFlight](https://testflight.apple.com/join/P1fFnAm9). For Android betas, +join Discord/Matrix (I'll eventually make a testing channel in Google Play). + ## Contributing I welcome any and all contributions. Just create a PR or an issue. To contribute code, check out the [build instructions](https://ntfy.sh/docs/develop/) for the server and the Android app. @@ -43,11 +53,6 @@ Or, if you'd like to help translate 🇩🇪 🇺🇸 🇧🇬, you can start im Translation status -## Contact me -You can directly contact me **[on Discord](https://discord.gg/cT7ECsZj9w)** or [on Matrix](https://matrix.to/#/#ntfy:matrix.org) -(bridged from Discord), or via the [GitHub issues](https://github.com/binwiederhier/ntfy/issues), or find more contact information -[on my website](https://heckel.io/about). - ## 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). diff --git a/client/client.go b/client/client.go index eaff5673..8b05a393 100644 --- a/client/client.go +++ b/client/client.go @@ -7,9 +7,9 @@ import ( "encoding/json" "errors" "fmt" + "heckel.io/ntfy/log" "heckel.io/ntfy/util" "io" - "log" "net/http" "strings" "sync" @@ -102,6 +102,7 @@ func (c *Client) PublishReader(topic string, body io.Reader, options ...PublishO return nil, err } } + log.Debug("%s Publishing message with headers %s", util.ShortTopicURL(topicURL), req.Header) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err @@ -136,6 +137,7 @@ func (c *Client) Poll(topic string, options ...SubscribeOption) ([]*Message, err msgChan := make(chan *Message) errChan := make(chan error) topicURL := c.expandTopicURL(topic) + log.Debug("%s Polling from topic", util.ShortTopicURL(topicURL)) options = append(options, WithPoll()) go func() { err := performSubscribeRequest(ctx, msgChan, topicURL, "", options...) @@ -171,6 +173,7 @@ func (c *Client) Subscribe(topic string, options ...SubscribeOption) string { defer c.mu.Unlock() subscriptionID := util.RandomString(10) topicURL := c.expandTopicURL(topic) + log.Debug("%s Subscribing to topic", util.ShortTopicURL(topicURL)) ctx, cancel := context.WithCancel(context.Background()) c.subscriptions[subscriptionID] = &subscription{ ID: subscriptionID, @@ -226,11 +229,11 @@ func handleSubscribeConnLoop(ctx context.Context, msgChan chan *Message, topicUR // TODO The retry logic is crude and may lose messages. It should record the last message like the // Android client, use since=, and do incremental backoff too if err := performSubscribeRequest(ctx, msgChan, topicURL, subcriptionID, options...); err != nil { - log.Printf("Connection to %s failed: %s", topicURL, err.Error()) + log.Warn("%s Connection failed: %s", util.ShortTopicURL(topicURL), err.Error()) } select { case <-ctx.Done(): - log.Printf("Connection to %s exited", topicURL) + log.Info("%s Connection exited", util.ShortTopicURL(topicURL)) return case <-time.After(10 * time.Second): // TODO Add incremental backoff } @@ -238,7 +241,9 @@ func handleSubscribeConnLoop(ctx context.Context, msgChan chan *Message, topicUR } func performSubscribeRequest(ctx context.Context, msgChan chan *Message, topicURL string, subscriptionID string, options ...SubscribeOption) error { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/json", topicURL), nil) + streamURL := fmt.Sprintf("%s/json", topicURL) + log.Debug("%s Listening to %s", util.ShortTopicURL(topicURL), streamURL) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, streamURL, nil) if err != nil { return err } @@ -261,10 +266,12 @@ func performSubscribeRequest(ctx context.Context, msgChan chan *Message, topicUR } scanner := bufio.NewScanner(resp.Body) for scanner.Scan() { - m, err := toMessage(scanner.Text(), topicURL, subscriptionID) + messageJSON := scanner.Text() + m, err := toMessage(messageJSON, topicURL, subscriptionID) if err != nil { return err } + log.Trace("%s Message received: %s", util.ShortTopicURL(topicURL), messageJSON) if m.Event == MessageEvent { msgChan <- m } diff --git a/cmd/access.go b/cmd/access.go index e70a68b7..e1f61bc1 100644 --- a/cmd/access.go +++ b/cmd/access.go @@ -19,7 +19,7 @@ const ( ) var flagsAccess = append( - userCommandFlags(), + flagsUser, &cli.BoolFlag{Name: "reset", Aliases: []string{"r"}, Usage: "reset access for user (and topic)"}, ) @@ -28,7 +28,7 @@ var cmdAccess = &cli.Command{ Usage: "Grant/revoke access to a topic, or show access", UsageText: "ntfy access [USERNAME [TOPIC [PERMISSION]]]", Flags: flagsAccess, - Before: initConfigFileInputSourceFunc("config", flagsAccess), + Before: initConfigFileInputSourceFunc("config", flagsAccess, initLogFunc), Action: execUserAccess, Category: categoryServer, Description: `Manage the access control list for the ntfy server. diff --git a/cmd/app.go b/cmd/app.go index 5a0b426f..975fd2fa 100644 --- a/cmd/app.go +++ b/cmd/app.go @@ -3,6 +3,8 @@ package cmd import ( "github.com/urfave/cli/v2" + "github.com/urfave/cli/v2/altsrc" + "heckel.io/ntfy/log" "os" ) @@ -13,6 +15,13 @@ const ( var commands = make([]*cli.Command, 0) +var flagsDefault = []cli.Flag{ + &cli.BoolFlag{Name: "debug", Aliases: []string{"d"}, EnvVars: []string{"NTFY_DEBUG"}, Usage: "enable debug logging"}, + &cli.BoolFlag{Name: "trace", EnvVars: []string{"NTFY_TRACE"}, Usage: "enable tracing (very verbose, be careful)"}, + &cli.BoolFlag{Name: "no-log-dates", Aliases: []string{"no_log_dates"}, EnvVars: []string{"NTFY_NO_LOG_DATES"}, Usage: "disable the date/time prefix"}, + altsrc.NewStringFlag(&cli.StringFlag{Name: "log-level", Aliases: []string{"log_level"}, Value: log.InfoLevel.String(), EnvVars: []string{"NTFY_LOG_LEVEL"}, Usage: "set log level"}), +} + // New creates a new CLI application func New() *cli.App { return &cli.App{ @@ -25,5 +34,21 @@ func New() *cli.App { Writer: os.Stdout, ErrWriter: os.Stderr, Commands: commands, + Flags: flagsDefault, + Before: initLogFunc, } } + +func initLogFunc(c *cli.Context) error { + if c.Bool("trace") { + log.SetLevel(log.TraceLevel) + } else if c.Bool("debug") { + log.SetLevel(log.DebugLevel) + } else { + log.SetLevel(log.ToLevel(c.String("log-level"))) + } + if c.Bool("no-log-dates") { + log.DisableDates() + } + return nil +} diff --git a/cmd/config_loader.go b/cmd/config_loader.go index 7840c6e7..6d984840 100644 --- a/cmd/config_loader.go +++ b/cmd/config_loader.go @@ -11,7 +11,7 @@ import ( // initConfigFileInputSourceFunc is like altsrc.InitInputSourceWithContext and altsrc.NewYamlSourceFromFlagFunc, but checks // if the config flag is exists and only loads it if it does. If the flag is set and the file exists, it fails. -func initConfigFileInputSourceFunc(configFlag string, flags []cli.Flag) cli.BeforeFunc { +func initConfigFileInputSourceFunc(configFlag string, flags []cli.Flag, next cli.BeforeFunc) cli.BeforeFunc { return func(context *cli.Context) error { configFile := context.String(configFlag) if context.IsSet(configFlag) && !util.FileExists(configFile) { @@ -23,7 +23,15 @@ func initConfigFileInputSourceFunc(configFlag string, flags []cli.Flag) cli.Befo if err != nil { return err } - return altsrc.ApplyInputSourceValues(context, inputSource, flags) + if err := altsrc.ApplyInputSourceValues(context, inputSource, flags); err != nil { + return err + } + if next != nil { + if err := next(context); err != nil { + return err + } + } + return nil } } diff --git a/cmd/publish.go b/cmd/publish.go index 06f8d962..c56aecad 100644 --- a/cmd/publish.go +++ b/cmd/publish.go @@ -16,31 +16,35 @@ func init() { commands = append(commands, cmdPublish) } +var flagsPublish = append( + flagsDefault, + &cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG"}, Usage: "client config file"}, + &cli.StringFlag{Name: "title", Aliases: []string{"t"}, EnvVars: []string{"NTFY_TITLE"}, Usage: "message title"}, + &cli.StringFlag{Name: "priority", Aliases: []string{"p"}, EnvVars: []string{"NTFY_PRIORITY"}, Usage: "priority of the message (1=min, 2=low, 3=default, 4=high, 5=max)"}, + &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: "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"}, + &cli.StringFlag{Name: "file", Aliases: []string{"f"}, EnvVars: []string{"NTFY_FILE"}, Usage: "file to upload as an attachment"}, + &cli.StringFlag{Name: "email", Aliases: []string{"mail", "e"}, EnvVars: []string{"NTFY_EMAIL"}, Usage: "also send to e-mail address"}, + &cli.StringFlag{Name: "user", Aliases: []string{"u"}, EnvVars: []string{"NTFY_USER"}, Usage: "username[:password] used to auth against the server"}, + &cli.BoolFlag{Name: "no-cache", Aliases: []string{"C"}, EnvVars: []string{"NTFY_NO_CACHE"}, Usage: "do not cache message server-side"}, + &cli.BoolFlag{Name: "no-firebase", Aliases: []string{"F"}, EnvVars: []string{"NTFY_NO_FIREBASE"}, Usage: "do not forward message to Firebase"}, + &cli.BoolFlag{Name: "env-topic", Aliases: []string{"P"}, EnvVars: []string{"NTFY_ENV_TOPIC"}, Usage: "use topic from NTFY_TOPIC env variable"}, + &cli.BoolFlag{Name: "quiet", Aliases: []string{"q"}, EnvVars: []string{"NTFY_QUIET"}, Usage: "do not print message"}, +) + var cmdPublish = &cli.Command{ Name: "publish", Aliases: []string{"pub", "send", "trigger"}, Usage: "Send message via a ntfy server", - UsageText: "ntfy send [OPTIONS..] TOPIC [MESSAGE]\n NTFY_TOPIC=.. ntfy send [OPTIONS..] -P [MESSAGE]", + UsageText: "ntfy publish [OPTIONS..] TOPIC [MESSAGE]\nNTFY_TOPIC=.. ntfy publish [OPTIONS..] -P [MESSAGE]", Action: execPublish, Category: categoryClient, - Flags: []cli.Flag{ - &cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG"}, Usage: "client config file"}, - &cli.StringFlag{Name: "title", Aliases: []string{"t"}, EnvVars: []string{"NTFY_TITLE"}, Usage: "message title"}, - &cli.StringFlag{Name: "priority", Aliases: []string{"p"}, EnvVars: []string{"NTFY_PRIORITY"}, Usage: "priority of the message (1=min, 2=low, 3=default, 4=high, 5=max)"}, - &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: "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"}, - &cli.StringFlag{Name: "file", Aliases: []string{"f"}, EnvVars: []string{"NTFY_FILE"}, Usage: "file to upload as an attachment"}, - &cli.StringFlag{Name: "email", Aliases: []string{"mail", "e"}, EnvVars: []string{"NTFY_EMAIL"}, Usage: "also send to e-mail address"}, - &cli.StringFlag{Name: "user", Aliases: []string{"u"}, EnvVars: []string{"NTFY_USER"}, Usage: "username[:password] used to auth against the server"}, - &cli.BoolFlag{Name: "no-cache", Aliases: []string{"C"}, EnvVars: []string{"NTFY_NO_CACHE"}, Usage: "do not cache message server-side"}, - &cli.BoolFlag{Name: "no-firebase", Aliases: []string{"F"}, EnvVars: []string{"NTFY_NO_FIREBASE"}, Usage: "do not forward message to Firebase"}, - &cli.BoolFlag{Name: "env-topic", Aliases: []string{"P"}, EnvVars: []string{"NTFY_ENV_TOPIC"}, Usage: "use topic from NTFY_TOPIC env variable"}, - &cli.BoolFlag{Name: "quiet", Aliases: []string{"q"}, EnvVars: []string{"NTFY_QUIET"}, Usage: "do print message"}, - }, + Flags: flagsPublish, + Before: initLogFunc, Description: `Publish a message to a ntfy server. Examples: diff --git a/cmd/serve.go b/cmd/serve.go index 630889d0..50969e03 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -5,10 +5,13 @@ package cmd import ( "errors" "fmt" - "log" + "heckel.io/ntfy/log" "math" "net" + "os" + "os/signal" "strings" + "syscall" "time" "github.com/urfave/cli/v2" @@ -21,8 +24,13 @@ func init() { commands = append(commands, cmdServe) } -var flagsServe = []cli.Flag{ - &cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: "/etc/ntfy/server.yml", DefaultText: "/etc/ntfy/server.yml", Usage: "config file"}, +const ( + defaultServerConfigFile = "/etc/ntfy/server.yml" +) + +var flagsServe = append( + flagsDefault, + &cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: defaultServerConfigFile, DefaultText: defaultServerConfigFile, Usage: "config file"}, altsrc.NewStringFlag(&cli.StringFlag{Name: "base-url", Aliases: []string{"base_url", "B"}, EnvVars: []string{"NTFY_BASE_URL"}, Usage: "externally visible base URL for this host (e.g. https://ntfy.sh)"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-http", Aliases: []string{"listen_http", "l"}, EnvVars: []string{"NTFY_LISTEN_HTTP"}, Value: server.DefaultListenHTTP, Usage: "ip:port used to as HTTP listen address"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-https", Aliases: []string{"listen_https", "L"}, EnvVars: []string{"NTFY_LISTEN_HTTPS"}, Usage: "ip:port used to as HTTPS listen address"}), @@ -59,7 +67,7 @@ var flagsServe = []cli.Flag{ altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-email-limit-burst", Aliases: []string{"visitor_email_limit_burst"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_BURST"}, Value: server.DefaultVisitorEmailLimitBurst, Usage: "initial limit of e-mails per visitor"}), altsrc.NewDurationFlag(&cli.DurationFlag{Name: "visitor-email-limit-replenish", Aliases: []string{"visitor_email_limit_replenish"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_REPLENISH"}, Value: server.DefaultVisitorEmailLimitReplenish, Usage: "interval at which burst limit is replenished (one per x)"}), altsrc.NewBoolFlag(&cli.BoolFlag{Name: "behind-proxy", Aliases: []string{"behind_proxy", "P"}, EnvVars: []string{"NTFY_BEHIND_PROXY"}, Value: false, Usage: "if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting)"}), -} +) var cmdServe = &cli.Command{ Name: "serve", @@ -68,7 +76,7 @@ var cmdServe = &cli.Command{ Action: execServe, Category: categoryServer, Flags: flagsServe, - Before: initConfigFileInputSourceFunc("config", flagsServe), + Before: initConfigFileInputSourceFunc("config", flagsServe, initLogFunc), Description: `Run the ntfy server and listen for incoming requests The command will load the configuration from /etc/ntfy/server.yml. Config options can @@ -85,6 +93,7 @@ func execServe(c *cli.Context) error { } // Read all the options + config := c.String("config") baseURL := c.String("base-url") listenHTTP := c.String("listen-http") listenHTTPS := c.String("listen-https") @@ -192,7 +201,7 @@ func execServe(c *cli.Context) error { for _, host := range visitorRequestLimitExemptHosts { ips, err := net.LookupIP(host) if err != nil { - log.Printf("cannot resolve host %s: %s, ignoring visitor request exemption", host, err.Error()) + log.Warn("cannot resolve host %s: %s, ignoring visitor request exemption", host, err.Error()) continue } for _, ip := range ips { @@ -240,14 +249,18 @@ func execServe(c *cli.Context) error { conf.VisitorEmailLimitReplenish = visitorEmailLimitReplenish conf.BehindProxy = behindProxy conf.EnableWeb = enableWeb + + // Set up hot-reloading of config + go sigHandlerConfigReload(config) + + // Run server s, err := server.New(conf) if err != nil { - log.Fatalln(err) + log.Fatal(err) + } else if err := s.Run(); err != nil { + log.Fatal(err) } - if err := s.Run(); err != nil { - log.Fatalln(err) - } - log.Printf("Exiting.") + log.Info("Exiting.") return nil } @@ -261,3 +274,28 @@ func parseSize(s string, defaultValue int64) (v int64, err error) { } return v, nil } + +func sigHandlerConfigReload(config string) { + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGHUP) + for range sigs { + log.Info("Partially hot reloading configuration ...") + inputSource, err := newYamlSourceFromFile(config, flagsServe) + if err != nil { + log.Warn("Hot reload failed: %s", err.Error()) + continue + } + reloadLogLevel(inputSource) + } +} + +func reloadLogLevel(inputSource altsrc.InputSourceContext) { + newLevelStr, err := inputSource.String("log-level") + if err != nil { + log.Warn("Cannot load log level: %s", err.Error()) + return + } + newLevel := log.ToLevel(newLevelStr) + log.SetLevel(newLevel) + log.Info("Log level is %s", newLevel.String()) +} diff --git a/cmd/subscribe.go b/cmd/subscribe.go index 97f7e410..3cab077c 100644 --- a/cmd/subscribe.go +++ b/cmd/subscribe.go @@ -5,12 +5,13 @@ import ( "fmt" "github.com/urfave/cli/v2" "heckel.io/ntfy/client" + "heckel.io/ntfy/log" "heckel.io/ntfy/util" - "log" "os" "os/exec" "os/user" "path/filepath" + "sort" "strings" ) @@ -24,6 +25,16 @@ const ( clientUserConfigFileWindowsRelative = "ntfy\\client.yml" ) +var flagsSubscribe = append( + flagsDefault, + &cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "client config file"}, + &cli.StringFlag{Name: "since", Aliases: []string{"s"}, Usage: "return events since `SINCE` (Unix timestamp, or all)"}, + &cli.StringFlag{Name: "user", Aliases: []string{"u"}, Usage: "username[:password] used to auth against the server"}, + &cli.BoolFlag{Name: "from-config", Aliases: []string{"C"}, Usage: "read subscriptions from config file (service mode)"}, + &cli.BoolFlag{Name: "poll", Aliases: []string{"p"}, Usage: "return events and exit, do not listen for new events"}, + &cli.BoolFlag{Name: "scheduled", Aliases: []string{"sched", "S"}, Usage: "also return scheduled/delayed events"}, +) + var cmdSubscribe = &cli.Command{ Name: "subscribe", Aliases: []string{"sub"}, @@ -31,15 +42,8 @@ var cmdSubscribe = &cli.Command{ UsageText: "ntfy subscribe [OPTIONS..] [TOPIC]", Action: execSubscribe, Category: categoryClient, - Flags: []cli.Flag{ - &cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "client config file"}, - &cli.StringFlag{Name: "since", Aliases: []string{"s"}, Usage: "return events since `SINCE` (Unix timestamp, or all)"}, - &cli.StringFlag{Name: "user", Aliases: []string{"u"}, Usage: "username[:password] used to auth against the server"}, - &cli.BoolFlag{Name: "from-config", Aliases: []string{"C"}, Usage: "read subscriptions from config file (service mode)"}, - &cli.BoolFlag{Name: "poll", Aliases: []string{"p"}, Usage: "return events and exit, do not listen for new events"}, - &cli.BoolFlag{Name: "scheduled", Aliases: []string{"sched", "S"}, Usage: "also return scheduled/delayed events"}, - &cli.BoolFlag{Name: "verbose", Aliases: []string{"v"}, Usage: "print verbose output"}, - }, + Flags: flagsSubscribe, + Before: initLogFunc, Description: `Subscribe to a topic from a ntfy server, and either print or execute a command for every arriving message. There are 3 modes in which the command can be run: @@ -186,6 +190,7 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic, if !ok { continue } + log.Debug("%s Dispatching received message: %s", logMessagePrefix(m), m.Raw) printMessageOrRunCommand(c, m, cmd) } return nil @@ -195,26 +200,26 @@ func printMessageOrRunCommand(c *cli.Context, m *client.Message, command string) if command != "" { runCommand(c, command, m) } else { + log.Debug("%s Printing raw message", logMessagePrefix(m)) fmt.Fprintln(c.App.Writer, m.Raw) } } func runCommand(c *cli.Context, command string, m *client.Message) { if err := runCommandInternal(c, command, m); err != nil { - fmt.Fprintf(c.App.ErrWriter, "Command failed: %s\n", err.Error()) + log.Warn("%s Command failed: %s", logMessagePrefix(m), err.Error()) } } func runCommandInternal(c *cli.Context, script string, m *client.Message) error { scriptFile := fmt.Sprintf("%s/ntfy-subscribe-%s.%s", os.TempDir(), util.RandomString(10), scriptExt) - if err := os.WriteFile(scriptFile, []byte(scriptHeader+script), 0700); err != nil { + log.Debug("%s Running command '%s' via temporary script %s", logMessagePrefix(m), script, scriptFile) + script = scriptHeader + script + if err := os.WriteFile(scriptFile, []byte(script), 0700); err != nil { return err } defer os.Remove(scriptFile) - verbose := c.Bool("verbose") - if verbose { - log.Printf("[%s] Executing: %s (for message: %s)", util.ShortTopicURL(m.TopicURL), script, m.Raw) - } + log.Debug("%s Executing script %s", logMessagePrefix(m), scriptFile) cmd := exec.Command(scriptLauncher[0], append(scriptLauncher[1:], scriptFile)...) cmd.Stdin = c.App.Reader cmd.Stdout = c.App.Writer @@ -224,7 +229,7 @@ func runCommandInternal(c *cli.Context, script string, m *client.Message) error } func envVars(m *client.Message) []string { - env := os.Environ() + env := make([]string, 0) env = append(env, envVar(m.ID, "NTFY_ID", "id")...) env = append(env, envVar(m.Topic, "NTFY_TOPIC", "topic")...) env = append(env, envVar(fmt.Sprintf("%d", m.Time), "NTFY_TIME", "time")...) @@ -233,7 +238,11 @@ func envVars(m *client.Message) []string { env = append(env, envVar(fmt.Sprintf("%d", m.Priority), "NTFY_PRIORITY", "priority", "prio", "p")...) env = append(env, envVar(strings.Join(m.Tags, ","), "NTFY_TAGS", "tags", "tag", "ta")...) env = append(env, envVar(m.Raw, "NTFY_RAW", "raw")...) - return env + sort.Strings(env) + if log.IsTrace() { + log.Trace("%s With environment:\n%s", logMessagePrefix(m), strings.Join(env, "\n")) + } + return append(os.Environ(), env...) } func envVar(value string, vars ...string) []string { @@ -249,7 +258,7 @@ func loadConfig(c *cli.Context) (*client.Config, error) { if filename != "" { return client.LoadConfig(filename) } - configFile := defaultConfigFile() + configFile := defaultClientConfigFile() if s, _ := os.Stat(configFile); s != nil { return client.LoadConfig(configFile) } @@ -257,7 +266,7 @@ func loadConfig(c *cli.Context) (*client.Config, error) { } //lint:ignore U1000 Conditionally used in different builds -func defaultConfigFileUnix() string { +func defaultClientConfigFileUnix() string { u, _ := user.Current() configFile := clientRootConfigFileUnixAbsolute if u.Uid != "0" { @@ -268,7 +277,11 @@ func defaultConfigFileUnix() string { } //lint:ignore U1000 Conditionally used in different builds -func defaultConfigFileWindows() string { +func defaultClientConfigFileWindows() string { homeDir, _ := os.UserConfigDir() return filepath.Join(homeDir, clientUserConfigFileWindowsRelative) } + +func logMessagePrefix(m *client.Message) string { + return fmt.Sprintf("%s/%s", util.ShortTopicURL(m.TopicURL), m.ID) +} diff --git a/cmd/subscribe_darwin.go b/cmd/subscribe_darwin.go index e4f44ed6..0372a79f 100644 --- a/cmd/subscribe_darwin.go +++ b/cmd/subscribe_darwin.go @@ -11,6 +11,6 @@ var ( scriptLauncher = []string{"sh", "-c"} ) -func defaultConfigFile() string { - return defaultConfigFileUnix() +func defaultClientConfigFile() string { + return defaultClientConfigFileUnix() } diff --git a/cmd/subscribe_linux.go b/cmd/subscribe_linux.go index c57660e8..346606bd 100644 --- a/cmd/subscribe_linux.go +++ b/cmd/subscribe_linux.go @@ -11,6 +11,6 @@ var ( scriptLauncher = []string{"sh", "-c"} ) -func defaultConfigFile() string { - return defaultConfigFileUnix() +func defaultClientConfigFile() string { + return defaultClientConfigFileUnix() } diff --git a/cmd/subscribe_windows.go b/cmd/subscribe_windows.go index 1d5c6655..e8f1a271 100644 --- a/cmd/subscribe_windows.go +++ b/cmd/subscribe_windows.go @@ -10,6 +10,6 @@ var ( scriptLauncher = []string{"cmd.exe", "/Q", "/C"} ) -func defaultConfigFile() string { - return defaultConfigFileWindows() +func defaultClientConfigFile() string { + return defaultClientConfigFileWindows() } diff --git a/cmd/user.go b/cmd/user.go index 5ccc5b15..acc06d4c 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -17,14 +17,19 @@ func init() { commands = append(commands, cmdUser) } -var flagsUser = userCommandFlags() +var flagsUser = append( + flagsDefault, + &cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: "/etc/ntfy/server.yml", DefaultText: "/etc/ntfy/server.yml", Usage: "config file"}, + altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}), +) var cmdUser = &cli.Command{ Name: "user", Usage: "Manage/show users", UsageText: "ntfy user [list|add|remove|change-pass|change-role] ...", Flags: flagsUser, - Before: initConfigFileInputSourceFunc("config", flagsUser), + Before: initConfigFileInputSourceFunc("config", flagsUser, initLogFunc), Category: categoryServer, Subcommands: []*cli.Command{ { @@ -269,11 +274,3 @@ func readPasswordAndConfirm(c *cli.Context) (string, error) { } return string(password), nil } - -func userCommandFlags() []cli.Flag { - return []cli.Flag{ - &cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: "/etc/ntfy/server.yml", DefaultText: "/etc/ntfy/server.yml", Usage: "config file"}, - altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}), - altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}), - } -} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..d39492e8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +version: "2.1" +services: + ntfy: + image: binwiederhier/ntfy + container_name: ntfy + command: + - serve + environment: + - TZ=UTC # optional: Change to your desired timezone + user: UID:GID # optional: Set custom user/group or uid/gid + volumes: + - /var/cache/ntfy:/var/cache/ntfy + - /etc/ntfy:/etc/ntfy + ports: + - 80:80 + restart: unless-stopped + diff --git a/docs/config.md b/docs/config.md index 15b547bb..db664c7c 100644 --- a/docs/config.md +++ b/docs/config.md @@ -643,10 +643,18 @@ In case you're curious, here's an example of the entire flow: - In the iOS app, you subscribe to `https://ntfy.example.com/mytopic` - The app subscribes to the Firebase topic `6de73be8dfb7d69e...` (the SHA256 of the topic URL) - When you publish a message to `https://ntfy.example.com/mytopic`, your ntfy server will publish a - poll request to `https://ntfy.sh/6de73be8dfb7d69e...` (passing the message ID in the `X-Poll-ID` header) -- The ntfy.sh server publishes the message to Firebase, which forwards it to APNS, which forwards it to your iOS device + poll request to `https://ntfy.sh/6de73be8dfb7d69e...`. The request from your server to the upstream server + contains only the message ID (in the `X-Poll-ID` header), and the SHA256 checksum of the topic URL (as upstream topic). +- The ntfy.sh server publishes the poll request message to Firebase, which forwards it to APNS, which forwards it to your iOS device - Your iOS device receives the poll request, and fetches the actual message from your server, and then displays it +Here's an example of what the self-hosted server forwards to the upstream server. The request is equivalent to this curl: + +``` +curl -X POST -H "X-Poll-ID: s4PdJozxM8na" https://ntfy.sh/6de73be8dfb7d69e32fb2c00c23fe7adbd8b5504406e3068c273aa24cef4055b +{"id":"4HsClFEuCIcs","time":1654087955,"event":"poll_request","topic":"6de73be8dfb7d69e32fb2c00c23fe7adbd8b5504406e3068c273aa24cef4055b","message":"New message","poll_id":"s4PdJozxM8na"} +``` + ## Rate limiting !!! info Be aware that if you are running ntfy behind a proxy, you must set the `behind-proxy` flag. @@ -700,6 +708,23 @@ are enabled): * `visitor-email-limit-burst` is the initial bucket of emails each visitor has. This defaults to 16. * `visitor-email-limit-replenish` is the rate at which the bucket is refilled (one email per x). Defaults to 1h. +### Firebase limits +If [Firebase is configured](#firebase-fcm), all messages are also published to a Firebase topic (unless `Firebase: no` +is set). Firebase enforces [its own limits](https://firebase.google.com/docs/cloud-messaging/concept-options#topics_throttling) +on how many messages can be published. Unfortunately these limits are a little vague and can change depending on the time +of day. In practice, I have only ever observed `429 Quota exceeded` responses from Firebase if **too many messages are published to +the same topic**. + +In ntfy, if Firebase responds with a 429 after publishing to a topic, the visitor (= IP address) who published the message +is **banned from publishing to Firebase for 10 minutes** (not configurable). Because publishing to Firebase happens asynchronously, +there is no indication of the user that this has happened. Non-Firebase subscribers (WebSocket or HTTP stream) are not affected. +After the 10 minutes are up, messages forwarding to Firebase is resumed for this visitor. + +If this ever happens, there will be a log message that looks something like this: +``` +WARN Firebase quota exceeded (likely for topic), temporarily denying Firebase access to visitor +``` + ## Tuning for scale If you're running ntfy for your home server, you probably don't need to worry about scale at all. In its default config, if it's not behind a proxy, the ntfy server can keep about **as many connections as the open file limit allows**. @@ -799,6 +824,26 @@ and [here](https://easyengine.io/tutorials/nginx/block-wp-login-php-bruteforce-a maxretry = 10 ``` +## Debugging/tracing +If something's not working right, you can debug/trace through what the ntfy server is doing by setting the `log-level` +to `DEBUG` or `TRACE`. The `DEBUG` setting will output information about each published message, but not the message +contents. The `TRACE` setting will also print the message contents. + +!!! warning + Both options are very verbose and should only be enabled in production for short periods of time. Otherwise, + you're going to run out of disk space pretty quickly. + +You can also hot-reload the `log-level` by sending the `SIGHUP` signal to the process after editing the `server.yml` file. +You can do so by calling `systemctl reload ntfy` (if ntfy is running inside systemd), or by calling `kill -HUP $(pidof ntfy)`. +If successful, you'll see something like this: + +``` +$ ntfy serve +2022/06/02 10:29:28 INFO Listening on :2586[http] :1025[smtp], log level is INFO +2022/06/02 10:29:34 INFO Partially hot reloading configuration ... +2022/06/02 10:29:34 INFO Log level is TRACE +``` + ## Config options Each config option can be set in the config file `/etc/ntfy/server.yml` (e.g. `listen-http: :80`) or as a CLI option (e.g. `--listen-http :80`. Here's a list of all available options. Alternatively, you can set an environment @@ -809,43 +854,44 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`). `cache_duration` and `cache-duration` are both supported. This is to support stricter YAML parsers that do not support dashes. -| Config option | Env variable | Format | Default | Description | -|--------------------------------------------|-------------------------------------------------|-----------------------------------------------------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `base-url` | `NTFY_BASE_URL` | *URL* | - | Public facing base URL of the service (e.g. `https://ntfy.sh`) | -| `listen-http` | `NTFY_LISTEN_HTTP` | `[host]:port` | `:80` | Listen address for the HTTP web server | -| `listen-https` | `NTFY_LISTEN_HTTPS` | `[host]:port` | - | Listen address for the HTTPS web server. If set, you also need to set `key-file` and `cert-file`. | -| `listen-unix` | `NTFY_LISTEN_UNIX` | *filename* | - | Path to a Unix socket to listen on | -| `key-file` | `NTFY_KEY_FILE` | *filename* | - | HTTPS/TLS private key file, only used if `listen-https` is set. | -| `cert-file` | `NTFY_CERT_FILE` | *filename* | - | HTTPS/TLS certificate file, only used if `listen-https` is set. | -| `firebase-key-file` | `NTFY_FIREBASE_KEY_FILE` | *filename* | - | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. See [Firebase (FCM](#firebase-fcm). | -| `cache-file` | `NTFY_CACHE_FILE` | *filename* | - | If set, messages are cached in a local SQLite database instead of only in-memory. This allows for service restarts without losing messages in support of the since= parameter. See [message cache](#message-cache). | -| `cache-duration` | `NTFY_CACHE_DURATION` | *duration* | 12h | Duration for which messages will be buffered before they are deleted. This is required to support the `since=...` and `poll=1` parameter. Set this to `0` to disable the cache entirely. | -| `auth-file` | `NTFY_AUTH_FILE` | *filename* | - | Auth database file used for access control. If set, enables authentication and access control. See [access control](#access-control). | -| `auth-default-access` | `NTFY_AUTH_DEFAULT_ACCESS` | `read-write`, `read-only`, `write-only`, `deny-all` | `read-write` | Default permissions if no matching entries in the auth database are found. Default is `read-write`. | -| `behind-proxy` | `NTFY_BEHIND_PROXY` | *bool* | false | If set, the X-Forwarded-For header is used to determine the visitor IP address instead of the remote address of the connection. | -| `attachment-cache-dir` | `NTFY_ATTACHMENT_CACHE_DIR` | *directory* | - | Cache directory for attached files. To enable attachments, this has to be set. | -| `attachment-total-size-limit` | `NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT` | *size* | 5G | Limit of the on-disk attachment cache directory. If the limits is exceeded, new attachments will be rejected. | -| `attachment-file-size-limit` | `NTFY_ATTACHMENT_FILE_SIZE_LIMIT` | *size* | 15M | Per-file attachment size limit (e.g. 300k, 2M, 100M). Larger attachment will be rejected. | -| `attachment-expiry-duration` | `NTFY_ATTACHMENT_EXPIRY_DURATION` | *duration* | 3h | Duration after which uploaded attachments will be deleted (e.g. 3h, 20h). Strongly affects `visitor-attachment-total-size-limit`. | -| `smtp-sender-addr` | `NTFY_SMTP_SENDER_ADDR` | `host:port` | - | SMTP server address to allow email sending | -| `smtp-sender-user` | `NTFY_SMTP_SENDER_USER` | *string* | - | SMTP user; only used if e-mail sending is enabled | -| `smtp-sender-pass` | `NTFY_SMTP_SENDER_PASS` | *string* | - | SMTP password; only used if e-mail sending is enabled | -| `smtp-sender-from` | `NTFY_SMTP_SENDER_FROM` | *e-mail address* | - | SMTP sender e-mail address; only used if e-mail sending is enabled | -| `smtp-server-listen` | `NTFY_SMTP_SERVER_LISTEN` | `[ip]:port` | - | Defines the IP address and port the SMTP server will listen on, e.g. `:25` or `1.2.3.4:25` | -| `smtp-server-domain` | `NTFY_SMTP_SERVER_DOMAIN` | *domain name* | - | SMTP server e-mail domain, e.g. `ntfy.sh` | -| `smtp-server-addr-prefix` | `NTFY_SMTP_SERVER_ADDR_PREFIX` | `[ip]:port` | - | Optional prefix for the e-mail addresses to prevent spam, e.g. `ntfy-` | -| `keepalive-interval` | `NTFY_KEEPALIVE_INTERVAL` | *duration* | 45s | Interval in which keepalive messages are sent to the client. This is to prevent intermediaries closing the connection for inactivity. Note that the Android app has a hardcoded timeout at 77s, so it should be less than that. | -| `manager-interval` | `$NTFY_MANAGER_INTERVAL` | *duration* | 1m | Interval in which the manager prunes old messages, deletes topics and prints the stats. | -| `web-root` | `NTFY_WEB_ROOT` | `app`, `home` or `disable` | `app` | Sets web root to landing page (home), web app (app) or disables the web app entirely (disable) | -| `global-topic-limit` | `NTFY_GLOBAL_TOPIC_LIMIT` | *number* | 15,000 | Rate limiting: Total number of topics before the server rejects new topics. | -| `visitor-subscription-limit` | `NTFY_VISITOR_SUBSCRIPTION_LIMIT` | *number* | 30 | Rate limiting: Number of subscriptions per visitor (IP address) | -| `visitor-attachment-total-size-limit` | `NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT` | *size* | 100M | Rate limiting: Total storage limit used for attachments per visitor, for all attachments combined. Storage is freed after attachments expire. See `attachment-expiry-duration`. | -| `visitor-attachment-daily-bandwidth-limit` | `NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT` | *size* | 500M | Rate limiting: Total daily attachment download/upload traffic limit per visitor. This is to protect your bandwidth costs from exploding. | -| `visitor-request-limit-burst` | `NTFY_VISITOR_REQUEST_LIMIT_BURST` | *number* | 60 | Rate limiting: Allowed GET/PUT/POST requests per second, per visitor. This setting is the initial bucket of requests each visitor has | -| `visitor-request-limit-replenish` | `NTFY_VISITOR_REQUEST_LIMIT_REPLENISH` | *duration* | 5s | Rate limiting: Strongly related to `visitor-request-limit-burst`: The rate at which the bucket is refilled | -| `visitor-request-limit-exempt-hosts` | `NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS` | *comma-separated host/IP list* | - | Rate limiting: List of hostnames and IPs to be exempt from request rate limiting | -| `visitor-email-limit-burst` | `NTFY_VISITOR_EMAIL_LIMIT_BURST` | *number* | 16 | Rate limiting:Initial limit of e-mails per visitor | -| `visitor-email-limit-replenish` | `NTFY_VISITOR_EMAIL_LIMIT_REPLENISH` | *duration* | 1h | Rate limiting: Strongly related to `visitor-email-limit-burst`: The rate at which the bucket is refilled | +| Config option | Env variable | Format | Default | Description | +|--------------------------------------------|-------------------------------------------------|-----------------------------------------------------|-------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `base-url` | `NTFY_BASE_URL` | *URL* | - | Public facing base URL of the service (e.g. `https://ntfy.sh`) | +| `listen-http` | `NTFY_LISTEN_HTTP` | `[host]:port` | `:80` | Listen address for the HTTP web server | +| `listen-https` | `NTFY_LISTEN_HTTPS` | `[host]:port` | - | Listen address for the HTTPS web server. If set, you also need to set `key-file` and `cert-file`. | +| `listen-unix` | `NTFY_LISTEN_UNIX` | *filename* | - | Path to a Unix socket to listen on | +| `key-file` | `NTFY_KEY_FILE` | *filename* | - | HTTPS/TLS private key file, only used if `listen-https` is set. | +| `cert-file` | `NTFY_CERT_FILE` | *filename* | - | HTTPS/TLS certificate file, only used if `listen-https` is set. | +| `firebase-key-file` | `NTFY_FIREBASE_KEY_FILE` | *filename* | - | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. See [Firebase (FCM](#firebase-fcm). | +| `cache-file` | `NTFY_CACHE_FILE` | *filename* | - | If set, messages are cached in a local SQLite database instead of only in-memory. This allows for service restarts without losing messages in support of the since= parameter. See [message cache](#message-cache). | +| `cache-duration` | `NTFY_CACHE_DURATION` | *duration* | 12h | Duration for which messages will be buffered before they are deleted. This is required to support the `since=...` and `poll=1` parameter. Set this to `0` to disable the cache entirely. | +| `auth-file` | `NTFY_AUTH_FILE` | *filename* | - | Auth database file used for access control. If set, enables authentication and access control. See [access control](#access-control). | +| `auth-default-access` | `NTFY_AUTH_DEFAULT_ACCESS` | `read-write`, `read-only`, `write-only`, `deny-all` | `read-write` | Default permissions if no matching entries in the auth database are found. Default is `read-write`. | +| `behind-proxy` | `NTFY_BEHIND_PROXY` | *bool* | false | If set, the X-Forwarded-For header is used to determine the visitor IP address instead of the remote address of the connection. | +| `attachment-cache-dir` | `NTFY_ATTACHMENT_CACHE_DIR` | *directory* | - | Cache directory for attached files. To enable attachments, this has to be set. | +| `attachment-total-size-limit` | `NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT` | *size* | 5G | Limit of the on-disk attachment cache directory. If the limits is exceeded, new attachments will be rejected. | +| `attachment-file-size-limit` | `NTFY_ATTACHMENT_FILE_SIZE_LIMIT` | *size* | 15M | Per-file attachment size limit (e.g. 300k, 2M, 100M). Larger attachment will be rejected. | +| `attachment-expiry-duration` | `NTFY_ATTACHMENT_EXPIRY_DURATION` | *duration* | 3h | Duration after which uploaded attachments will be deleted (e.g. 3h, 20h). Strongly affects `visitor-attachment-total-size-limit`. | +| `smtp-sender-addr` | `NTFY_SMTP_SENDER_ADDR` | `host:port` | - | SMTP server address to allow email sending | +| `smtp-sender-user` | `NTFY_SMTP_SENDER_USER` | *string* | - | SMTP user; only used if e-mail sending is enabled | +| `smtp-sender-pass` | `NTFY_SMTP_SENDER_PASS` | *string* | - | SMTP password; only used if e-mail sending is enabled | +| `smtp-sender-from` | `NTFY_SMTP_SENDER_FROM` | *e-mail address* | - | SMTP sender e-mail address; only used if e-mail sending is enabled | +| `smtp-server-listen` | `NTFY_SMTP_SERVER_LISTEN` | `[ip]:port` | - | Defines the IP address and port the SMTP server will listen on, e.g. `:25` or `1.2.3.4:25` | +| `smtp-server-domain` | `NTFY_SMTP_SERVER_DOMAIN` | *domain name* | - | SMTP server e-mail domain, e.g. `ntfy.sh` | +| `smtp-server-addr-prefix` | `NTFY_SMTP_SERVER_ADDR_PREFIX` | `[ip]:port` | - | Optional prefix for the e-mail addresses to prevent spam, e.g. `ntfy-` | +| `keepalive-interval` | `NTFY_KEEPALIVE_INTERVAL` | *duration* | 45s | Interval in which keepalive messages are sent to the client. This is to prevent intermediaries closing the connection for inactivity. Note that the Android app has a hardcoded timeout at 77s, so it should be less than that. | +| `manager-interval` | `$NTFY_MANAGER_INTERVAL` | *duration* | 1m | Interval in which the manager prunes old messages, deletes topics and prints the stats. | +| `global-topic-limit` | `NTFY_GLOBAL_TOPIC_LIMIT` | *number* | 15,000 | Rate limiting: Total number of topics before the server rejects new topics. | +| `upstream-base-url` | `NTFY_UPSTREAM_BASE_URL` | *URL* | `https://ntfy.sh` | Forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers | +| `visitor-attachment-total-size-limit` | `NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT` | *size* | 100M | Rate limiting: Total storage limit used for attachments per visitor, for all attachments combined. Storage is freed after attachments expire. See `attachment-expiry-duration`. | +| `visitor-attachment-daily-bandwidth-limit` | `NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT` | *size* | 500M | Rate limiting: Total daily attachment download/upload traffic limit per visitor. This is to protect your bandwidth costs from exploding. | +| `visitor-email-limit-burst` | `NTFY_VISITOR_EMAIL_LIMIT_BURST` | *number* | 16 | Rate limiting:Initial limit of e-mails per visitor | +| `visitor-email-limit-replenish` | `NTFY_VISITOR_EMAIL_LIMIT_REPLENISH` | *duration* | 1h | Rate limiting: Strongly related to `visitor-email-limit-burst`: The rate at which the bucket is refilled | +| `visitor-request-limit-burst` | `NTFY_VISITOR_REQUEST_LIMIT_BURST` | *number* | 60 | Rate limiting: Allowed GET/PUT/POST requests per second, per visitor. This setting is the initial bucket of requests each visitor has | +| `visitor-request-limit-replenish` | `NTFY_VISITOR_REQUEST_LIMIT_REPLENISH` | *duration* | 5s | Rate limiting: Strongly related to `visitor-request-limit-burst`: The rate at which the bucket is refilled | +| `visitor-request-limit-exempt-hosts` | `NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS` | *comma-separated host/IP list* | - | Rate limiting: List of hostnames and IPs to be exempt from request rate limiting | +| `visitor-subscription-limit` | `NTFY_VISITOR_SUBSCRIPTION_LIMIT` | *number* | 30 | Rate limiting: Number of subscriptions per visitor (IP address) | +| `web-root` | `NTFY_WEB_ROOT` | `app`, `home` or `disable` | `app` | Sets web root to landing page (home), web app (app) or disables the web app entirely (disable) | The format for a *duration* is: `(smh)`, e.g. 30s, 20m or 1h. The format for a *size* is: `(GMK)`, e.g. 1G, 200M or 4000k. @@ -873,42 +919,46 @@ DESCRIPTION: ntfy serve --listen-http :8080 # Starts server with alternate port OPTIONS: - --config value, -c value config file (default: /etc/ntfy/server.yml) [$NTFY_CONFIG_FILE] + --attachment-cache-dir value, --attachment_cache_dir value cache directory for attached files [$NTFY_ATTACHMENT_CACHE_DIR] + --attachment-expiry-duration value, --attachment_expiry_duration value, -X value duration after which uploaded attachments will be deleted (e.g. 3h, 20h) (default: 3h) [$NTFY_ATTACHMENT_EXPIRY_DURATION] + --attachment-file-size-limit value, --attachment_file_size_limit value, -Y value per-file attachment size limit (e.g. 300k, 2M, 100M) (default: 15M) [$NTFY_ATTACHMENT_FILE_SIZE_LIMIT] + --attachment-total-size-limit value, --attachment_total_size_limit value, -A value limit of the on-disk attachment cache (default: 5G) [$NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT] + --auth-default-access value, --auth_default_access value, -p value default permissions if no matching entries in the auth database are found (default: "read-write") [$NTFY_AUTH_DEFAULT_ACCESS] + --auth-file value, --auth_file value, -H value auth database file used for access control [$NTFY_AUTH_FILE] --base-url value, --base_url value, -B value externally visible base URL for this host (e.g. https://ntfy.sh) [$NTFY_BASE_URL] + --behind-proxy, --behind_proxy, -P if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting) (default: false) [$NTFY_BEHIND_PROXY] + --cache-duration since, --cache_duration since, -b since buffer messages for this time to allow since requests (default: 12h0m0s) [$NTFY_CACHE_DURATION] + --cache-file value, --cache_file value, -C value cache file used for message caching [$NTFY_CACHE_FILE] + --cert-file value, --cert_file value, -E value certificate file, if listen-https is set [$NTFY_CERT_FILE] + --config value, -c value config file (default: /etc/ntfy/server.yml) [$NTFY_CONFIG_FILE] + --debug, -d enable debug logging (default: false) [$NTFY_DEBUG] + --firebase-key-file value, --firebase_key_file value, -F value Firebase credentials file; if set additionally publish to FCM topic [$NTFY_FIREBASE_KEY_FILE] + --global-topic-limit value, --global_topic_limit value, -T value total number of topics allowed (default: 15000) [$NTFY_GLOBAL_TOPIC_LIMIT] + --keepalive-interval value, --keepalive_interval value, -k value interval of keepalive messages (default: 45s) [$NTFY_KEEPALIVE_INTERVAL] + --key-file value, --key_file value, -K value private key file, if listen-https is set [$NTFY_KEY_FILE] --listen-http value, --listen_http value, -l value ip:port used to as HTTP listen address (default: ":80") [$NTFY_LISTEN_HTTP] --listen-https value, --listen_https value, -L value ip:port used to as HTTPS listen address [$NTFY_LISTEN_HTTPS] --listen-unix value, --listen_unix value, -U value listen on unix socket path [$NTFY_LISTEN_UNIX] - --key-file value, --key_file value, -K value private key file, if listen-https is set [$NTFY_KEY_FILE] - --cert-file value, --cert_file value, -E value certificate file, if listen-https is set [$NTFY_CERT_FILE] - --firebase-key-file value, --firebase_key_file value, -F value Firebase credentials file; if set additionally publish to FCM topic [$NTFY_FIREBASE_KEY_FILE] - --cache-file value, --cache_file value, -C value cache file used for message caching [$NTFY_CACHE_FILE] - --cache-duration since, --cache_duration since, -b since buffer messages for this time to allow since requests (default: 12h0m0s) [$NTFY_CACHE_DURATION] - --auth-file value, --auth_file value, -H value auth database file used for access control [$NTFY_AUTH_FILE] - --auth-default-access value, --auth_default_access value, -p value default permissions if no matching entries in the auth database are found (default: "read-write") [$NTFY_AUTH_DEFAULT_ACCESS] - --attachment-cache-dir value, --attachment_cache_dir value cache directory for attached files [$NTFY_ATTACHMENT_CACHE_DIR] - --attachment-total-size-limit value, --attachment_total_size_limit value, -A value limit of the on-disk attachment cache (default: 5G) [$NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT] - --attachment-file-size-limit value, --attachment_file_size_limit value, -Y value per-file attachment size limit (e.g. 300k, 2M, 100M) (default: 15M) [$NTFY_ATTACHMENT_FILE_SIZE_LIMIT] - --attachment-expiry-duration value, --attachment_expiry_duration value, -X value duration after which uploaded attachments will be deleted (e.g. 3h, 20h) (default: 3h) [$NTFY_ATTACHMENT_EXPIRY_DURATION] - --keepalive-interval value, --keepalive_interval value, -k value interval of keepalive messages (default: 45s) [$NTFY_KEEPALIVE_INTERVAL] + --log-level value, --log_level value set log level (default: "INFO") [$NTFY_LOG_LEVEL] --manager-interval value, --manager_interval value, -m value interval of for message pruning and stats printing (default: 1m0s) [$NTFY_MANAGER_INTERVAL] - --web-root value, --web_root value sets web root to landing page (home), web app (app) or disabled (disable) (default: "app") [$NTFY_WEB_ROOT] + --no-log-dates, --no_log_dates disable the date/time prefix (default: false) [$NTFY_NO_LOG_DATES] --smtp-sender-addr value, --smtp_sender_addr value SMTP server address (host:port) for outgoing emails [$NTFY_SMTP_SENDER_ADDR] - --smtp-sender-user value, --smtp_sender_user value SMTP user (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_USER] - --smtp-sender-pass value, --smtp_sender_pass value SMTP password (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_PASS] --smtp-sender-from value, --smtp_sender_from value SMTP sender address (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_FROM] - --smtp-server-listen value, --smtp_server_listen value SMTP server address (ip:port) for incoming emails, e.g. :25 [$NTFY_SMTP_SERVER_LISTEN] - --smtp-server-domain value, --smtp_server_domain value SMTP domain for incoming e-mail, e.g. ntfy.sh [$NTFY_SMTP_SERVER_DOMAIN] + --smtp-sender-pass value, --smtp_sender_pass value SMTP password (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_PASS] + --smtp-sender-user value, --smtp_sender_user value SMTP user (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_USER] --smtp-server-addr-prefix value, --smtp_server_addr_prefix value SMTP email address prefix for topics to prevent spam (e.g. 'ntfy-') [$NTFY_SMTP_SERVER_ADDR_PREFIX] - --global-topic-limit value, --global_topic_limit value, -T value total number of topics allowed (default: 15000) [$NTFY_GLOBAL_TOPIC_LIMIT] - --visitor-subscription-limit value, --visitor_subscription_limit value number of subscriptions per visitor (default: 30) [$NTFY_VISITOR_SUBSCRIPTION_LIMIT] - --visitor-attachment-total-size-limit value, --visitor_attachment_total_size_limit value total storage limit used for attachments per visitor (default: "100M") [$NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT] + --smtp-server-domain value, --smtp_server_domain value SMTP domain for incoming e-mail, e.g. ntfy.sh [$NTFY_SMTP_SERVER_DOMAIN] + --smtp-server-listen value, --smtp_server_listen value SMTP server address (ip:port) for incoming emails, e.g. :25 [$NTFY_SMTP_SERVER_LISTEN] + --trace enable tracing (very verbose, be careful) (default: false) [$NTFY_TRACE] + --upstream-base-url value, --upstream_base_url value forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers [$NTFY_UPSTREAM_BASE_URL] --visitor-attachment-daily-bandwidth-limit value, --visitor_attachment_daily_bandwidth_limit value total daily attachment download/upload bandwidth limit per visitor (default: "500M") [$NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT] - --visitor-request-limit-burst value, --visitor_request_limit_burst value initial limit of requests per visitor (default: 60) [$NTFY_VISITOR_REQUEST_LIMIT_BURST] - --visitor-request-limit-replenish value, --visitor_request_limit_replenish value interval at which burst limit is replenished (one per x) (default: 5s) [$NTFY_VISITOR_REQUEST_LIMIT_REPLENISH] - --visitor-request-limit-exempt-hosts value, --visitor_request_limit_exempt_hosts value hostnames and/or IP addresses of hosts that will be exempt from the visitor request limit [$NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS] + --visitor-attachment-total-size-limit value, --visitor_attachment_total_size_limit value total storage limit used for attachments per visitor (default: "100M") [$NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT] --visitor-email-limit-burst value, --visitor_email_limit_burst value initial limit of e-mails per visitor (default: 16) [$NTFY_VISITOR_EMAIL_LIMIT_BURST] --visitor-email-limit-replenish value, --visitor_email_limit_replenish value interval at which burst limit is replenished (one per x) (default: 1h0m0s) [$NTFY_VISITOR_EMAIL_LIMIT_REPLENISH] - --behind-proxy, --behind_proxy, -P if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting) (default: false) [$NTFY_BEHIND_PROXY] - --help, -h show help (default: false) + --visitor-request-limit-burst value, --visitor_request_limit_burst value initial limit of requests per visitor (default: 60) [$NTFY_VISITOR_REQUEST_LIMIT_BURST] + --visitor-request-limit-exempt-hosts value, --visitor_request_limit_exempt_hosts value hostnames and/or IP addresses of hosts that will be exempt from the visitor request limit [$NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS] + --visitor-request-limit-replenish value, --visitor_request_limit_replenish value interval at which burst limit is replenished (one per x) (default: 5s) [$NTFY_VISITOR_REQUEST_LIMIT_REPLENISH] + --visitor-subscription-limit value, --visitor_subscription_limit value number of subscriptions per visitor (default: 30) [$NTFY_VISITOR_SUBSCRIPTION_LIMIT] + --web-root value, --web_root value sets web root to landing page (home), web app (app) or disabled (disable) (default: "app") [$NTFY_WEB_ROOT] ``` diff --git a/docs/examples.md b/docs/examples.md index 8c94daf6..26be6b30 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -44,7 +44,7 @@ fi ## Server-sent messages in your web app Just as you can [subscribe to topics in the Web UI](subscribe/web.md), you can use ntfy in your own -web application. Check out the live example or just look the source of this page. +web application. Check out the live example. ## Notify on SSH login Years ago my home server was broken into. That shook me hard, so every time someone logs into any machine that I diff --git a/docs/install.md b/docs/install.md index 0d7f1ed4..740a2699 100644 --- a/docs/install.md +++ b/docs/install.md @@ -26,37 +26,37 @@ deb/rpm packages. === "x86_64/amd64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_x86_64.tar.gz - tar zxvf ntfy_1.24.0_linux_x86_64.tar.gz - sudo cp -a ntfy_1.24.0_linux_x86_64/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_1.24.0_linux_x86_64/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.2/ntfy_1.25.2_linux_x86_64.tar.gz + tar zxvf ntfy_1.25.2_linux_x86_64.tar.gz + sudo cp -a ntfy_1.25.2_linux_x86_64/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_1.25.2_linux_x86_64/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` === "armv6" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_armv6.tar.gz - tar zxvf ntfy_1.24.0_linux_armv6.tar.gz - sudo cp -a ntfy_1.24.0_linux_armv6/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_1.24.0_linux_armv6/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.2/ntfy_1.25.2_linux_armv6.tar.gz + tar zxvf ntfy_1.25.2_linux_armv6.tar.gz + sudo cp -a ntfy_1.25.2_linux_armv6/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_1.25.2_linux_armv6/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` === "armv7/armhf" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_armv7.tar.gz - tar zxvf ntfy_1.24.0_linux_armv7.tar.gz - sudo cp -a ntfy_1.24.0_linux_armv7/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_1.24.0_linux_armv7/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.2/ntfy_1.25.2_linux_armv7.tar.gz + tar zxvf ntfy_1.25.2_linux_armv7.tar.gz + sudo cp -a ntfy_1.25.2_linux_armv7/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_1.25.2_linux_armv7/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` === "arm64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_arm64.tar.gz - tar zxvf ntfy_1.24.0_linux_arm64.tar.gz - sudo cp -a ntfy_1.24.0_linux_arm64/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_1.24.0_linux_arm64/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.2/ntfy_1.25.2_linux_arm64.tar.gz + tar zxvf ntfy_1.25.2_linux_arm64.tar.gz + sudo cp -a ntfy_1.25.2_linux_arm64/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_1.25.2_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.24.0/ntfy_1.24.0_linux_amd64.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.2/ntfy_1.25.2_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.24.0/ntfy_1.24.0_linux_armv6.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.2/ntfy_1.25.2_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.24.0/ntfy_1.24.0_linux_armv7.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.2/ntfy_1.25.2_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.24.0/ntfy_1.24.0_linux_arm64.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.2/ntfy_1.25.2_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.24.0/ntfy_1.24.0_linux_amd64.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.25.2/ntfy_1.25.2_linux_amd64.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` === "armv6" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_armv6.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.25.2/ntfy_1.25.2_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.24.0/ntfy_1.24.0_linux_armv7.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.25.2/ntfy_1.25.2_linux_armv7.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` === "arm64" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_arm64.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.25.2/ntfy_1.25.2_linux_arm64.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` @@ -176,6 +176,12 @@ cd ntfysh-bin makepkg -si ``` +## NixOS / Nix +ntfy is packaged in nixpkgs as `ntfy-sh`. It can be installed by adding the package name to the configuration file and calling `nixos-rebuild`. Alternatively, the following command can be used to install ntfy in the current user environment: +``` +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, extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`). @@ -184,11 +190,11 @@ If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For a `~/Library/Application Support/ntfy/client.yml` (sample included in the tarball). ```bash -curl https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_macOS_all.tar.gz > ntfy_1.24.0_macOS_all.tar.gz -tar zxvf ntfy_1.24.0_macOS_all.tar.gz -sudo cp -a ntfy_1.24.0_macOS_all/ntfy /usr/local/bin/ntfy +curl -L https://github.com/binwiederhier/ntfy/releases/download/v1.25.2/ntfy_1.25.2_macOS_all.tar.gz > ntfy_1.25.2_macOS_all.tar.gz +tar zxvf ntfy_1.25.2_macOS_all.tar.gz +sudo cp -a ntfy_1.25.2_macOS_all/ntfy /usr/local/bin/ntfy mkdir ~/Library/Application\ Support/ntfy -cp ntfy_1.24.0_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml +cp ntfy_1.25.2_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml ntfy --help ``` @@ -200,11 +206,15 @@ 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.24.0/ntfy_v1.24.0_windows_x86_64.zip), +To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v1.25.2/ntfy_v1.25.2_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). +Also available in [Scoop's](https://scoop.sh) Main repository: + +`scoop install ntfy` + !!! info There is currently no installer for Windows, and the binary is not signed. If this is desired, please create a [GitHub issue](https://github.com/binwiederhier/ntfy/issues) to let me know. @@ -233,17 +243,19 @@ docker run \ serve ``` -With other config options (configured via `/etc/ntfy/server.yml`, see [configuration](config.md) for details): +With other config options, timezone, and non-root user (configured via `/etc/ntfy/server.yml`, see [configuration](config.md) for details): ```bash docker run \ -v /etc/ntfy:/etc/ntfy \ + -e TZ=UTC \ -p 80:80 \ + -u UID:GID \ -it \ binwiederhier/ntfy \ serve ``` -Using docker-compose: +Using docker-compose with non-root user: ```yaml version: "2.1" @@ -253,6 +265,9 @@ services: container_name: ntfy command: - serve + environment: + - TZ=UTC # optional: set desired timezone + user: UID:GID # optional: replace with your own user/group or uid/gid volumes: - /var/cache/ntfy:/var/cache/ntfy - /etc/ntfy:/etc/ntfy @@ -261,6 +276,8 @@ services: restart: unless-stopped ``` +If using a non-root user when running the docker version, be sure to chown the server.yml, user.db, and cache.db files to the same uid/gid. + Alternatively, you may wish to build a customized Docker image that can be run with fewer command-line arguments and without delivering the configuration file separately. ``` FROM binwiederhier/ntfy diff --git a/docs/privacy.md b/docs/privacy.md index 5a36a1c8..f89f9aaa 100644 --- a/docs/privacy.md +++ b/docs/privacy.md @@ -8,5 +8,5 @@ any outside service. All data is exclusively used to make the service function p I use is Firebase Cloud Messaging (FCM) service, which is required to provide instant Android notifications (see [FAQ](faq.md) for details). To avoid FCM altogether, download the F-Droid version. -The web server does not log or otherwise store request paths, remote IP addresses or even topics or messages, -aside from a short on-disk cache to support service restarts. +For debugging purposes, the ntfy server may temporarily log request paths, remote IP addresses or even topics +or messages, though typically this is turned off. diff --git a/docs/releases.md b/docs/releases.md index 1fdf36bc..a6ff64fe 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -4,7 +4,67 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release + +## ntfy server v1.25.2 +Released June 2, 2022 + +This release adds the ability to set a log level to facilitate easier debugging of live systems. It also solves a +production problem with a few over-users that resulted in Firebase quota problems (only applying to the over-users). +We now block visitors from using Firebase if they trigger a quota exceeded response. + +On top of that, we updated the Firebase SDK and are now building the release in GitHub Actions. We've also got two +more translations: Chinese/Simplified and Dutch. + +**Features:** + +* Advanced logging, with different log levels and hot reloading of the log level ([#284](https://github.com/binwiederhier/ntfy/pull/284)) + +**Bugs**: + +* Respect Firebase "quota exceeded" response for topics, block Firebase publishing for user for 10min ([#289](https://github.com/binwiederhier/ntfy/issues/289)) +* Fix documentation header blue header due to mkdocs-material theme update (no ticket) + +**Maintenance:** + +* Upgrade Firebase Admin SDK to 4.x ([#274](https://github.com/binwiederhier/ntfy/issues/274)) +* CI: Build from pipeline instead of locally ([#36](https://github.com/binwiederhier/ntfy/issues/36)) + +**Documentation**: + +* ⚠️ [Privacy policy](privacy.md) updated to reflect additional debug/tracing feature (no ticket) +* [Examples](examples.md) for [Home Assistant](https://www.home-assistant.io/) ([#282](https://github.com/binwiederhier/ntfy/pull/282), thanks to [@poblabs](https://github.com/poblabs)) +* Install instructions for [NixOS/Nix](https://ntfy.sh/docs/install/#nixos-nix) ([#282](https://github.com/binwiederhier/ntfy/pull/282), thanks to [@arjan-s](https://github.com/arjan-s)) +* Clarify `poll_request` wording for [iOS push notifications](https://ntfy.sh/docs/config/#ios-instant-notifications) ([#300](https://github.com/binwiederhier/ntfy/issues/300), thanks to [@prabirshrestha](https://github.com/prabirshrestha) for reporting) +* Example for using ntfy with docker-compose.yml without root privileges ([#304](https://github.com/binwiederhier/ntfy/pull/304), thanks to [@ksurl](https://github.com/ksurl)) + +**Additional translations:** + +* Chinese/Simplified (thanks to [@yufei.im](https://hosted.weblate.org/user/yufei.im/)) +* Dutch (thanks to [@SchoNie](https://hosted.weblate.org/user/SchoNie/)) + +## ntfy iOS app v1.1 +Released May 31, 2022 + +In this release of the iOS app, we add message priorities (mapped to iOS interruption levels), tags and emojis, +action buttons to open websites or perform HTTP requests (in the notification and the detail view), a custom click +action when the notification is tapped, and various other fixes. + +It also adds support for self-hosted servers (albeit not supporting auth yet). The self-hosted server needs to be +configured to forward poll requests to upstream ntfy.sh for push notifications to work (see [iOS push notifications](https://ntfy.sh/docs/config/#ios-instant-notifications) +for details). **Features:** @@ -21,22 +81,6 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * iOS UI not always updating properly ([#267](https://github.com/binwiederhier/ntfy/issues/267)) - -## ntfy Android app v1.14.0 (UNRELEASED) - -**Additional translations:** - -* Italian (thanks to [@Genio2003](https://hosted.weblate.org/user/Genio2003/)) - - -## ntfy server v1.25.0 (UNRELEASED) - -**Documentation**: - -* [Examples](examples.md) for [Home Assistant](https://www.home-assistant.io/) ([#282](https://github.com/binwiederhier/ntfy/pull/282), thanks to [@poblabs](https://github.com/poblabs)) - ---> - ## ntfy server v1.24.0 Released May 28, 2022 diff --git a/docs/static/css/extra.css b/docs/static/css/extra.css index cb71a018..a7370399 100644 --- a/docs/static/css/extra.css +++ b/docs/static/css/extra.css @@ -1,4 +1,4 @@ -:root { +:root > * { --md-primary-fg-color: #338574; --md-primary-fg-color--light: #338574; --md-primary-fg-color--dark: #338574; diff --git a/docs/static/js/extra.js b/docs/static/js/extra.js index 0aa380a7..6ddf07a9 100644 --- a/docs/static/js/extra.js +++ b/docs/static/js/extra.js @@ -1,8 +1,8 @@ // Link tabs, as per https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/#linked-tabs -const savedTab = localStorage.getItem('savedTab') -const tabs = document.querySelectorAll(".tabbed-set > input") -for (const tab of tabs) { +const savedCodeTab = localStorage.getItem('savedTab') +const codeTabs = document.querySelectorAll(".tabbed-set > input") +for (const tab of codeTabs) { tab.addEventListener("click", () => { const current = document.querySelector(`label[for=${tab.id}]`) const pos = current.getBoundingClientRect().top @@ -25,7 +25,7 @@ for (const tab of tabs) { // Select saved tab const current = document.querySelector(`label[for=${tab.id}]`) const labelContent = current.innerHTML - if (savedTab === labelContent) { + if (savedCodeTab === labelContent) { tab.checked = true } } diff --git a/go.mod b/go.mod index f00c1c33..6c36e8fc 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.17 require ( cloud.google.com/go/firestore v1.6.1 // indirect cloud.google.com/go/storage v1.22.1 // indirect - firebase.google.com/go v3.13.0+incompatible github.com/BurntSushi/toml v1.1.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/emersion/go-smtp v0.15.0 @@ -17,15 +16,17 @@ require ( github.com/urfave/cli/v2 v2.8.1 golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401 // indirect - golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 + 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-20220411224347-583f2d630306 - google.golang.org/api v0.81.0 + google.golang.org/api v0.82.0 gopkg.in/yaml.v2 v2.4.0 ) require github.com/pkg/errors v0.9.1 // indirect +require firebase.google.com/go/v4 v4.8.0 + require ( cloud.google.com/go v0.102.0 // indirect cloud.google.com/go/compute v1.6.1 // indirect @@ -43,13 +44,14 @@ require ( 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-20220526153639-5463443f8c37 // indirect + golang.org/x/net v0.0.0-20220531201128-c960675eff93 // indirect golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58 // indirect - google.golang.org/grpc v1.46.2 // indirect + google.golang.org/appengine/v2 v2.0.1 // indirect + google.golang.org/genproto v0.0.0-20220602131408-e326c6e8e9c8 // indirect + google.golang.org/grpc v1.47.0 // indirect google.golang.org/protobuf v1.28.0 // indirect gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect ) diff --git a/go.sum b/go.sum index 4a4b581a..e0f0204e 100644 --- a/go.sum +++ b/go.sum @@ -26,6 +26,7 @@ cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+Y cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +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 h1:DAq3r8y4mDgyB/ZPJ9v/5VJNqjgJAxTn6ZYLlUywOu8= cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= @@ -36,6 +37,7 @@ cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUM 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/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= @@ -45,6 +47,7 @@ cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7 cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 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/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/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= @@ -56,11 +59,12 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 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 h1:F6IlQJZrZM++apn9V5/VfS3gbTUYg98PS3EMQAzqtfg= cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -firebase.google.com/go v3.13.0+incompatible h1:3TdYC3DDi6aHn20qoRkxwGqNgdjtblwVAyRLQwGn/+4= -firebase.google.com/go v3.13.0+incompatible/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIwjt8toICdV5Wh9ptHs= +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= github.com/AlekSi/pointer v1.0.0/go.mod h1:1kjywbfcPFCmncIxtk6fIEub6LKrfMz3gc5QKVOSOA8= github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= @@ -337,9 +341,9 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220526153639-5463443f8c37 h1:lUkvobShwKsOesNfWWlCS5q7fnbG1MEliIzwu886fn8= golang.org/x/net v0.0.0-20220526153639-5463443f8c37/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220531201128-c960675eff93 h1:MYimHLfoXEpOhqd/zgoA/uoXzHB86AEky4LAx5ij9xA= +golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 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= @@ -373,8 +377,9 @@ 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-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4= golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/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/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= @@ -425,9 +430,11 @@ golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -543,15 +550,19 @@ google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdr google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.64.0/go.mod h1:931CdxA8Rm4t6zqTFGSsgwbAEZ2+GMYurbndwSimebM= +google.golang.org/api v0.66.0/go.mod h1:I1dmXYpX7HGwz/ejRxwQp2qj5bFAz93HiCU1C1oYd9M= google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.69.0/go.mod h1:boanBiw+h5c3s+tBPgEzLDRHfFLWV0qXxRHz3ws7C80= google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +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.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= -google.golang.org/api v0.81.0 h1:o8WF5AvfidafWbFjsRyupxyEQJNUWxLZJCK5NXrxZZ8= -google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko= +google.golang.org/api v0.82.0 h1:h6EGeZuzhoKSS7BUznzkW+2wHZ+4Ubd6rsVvvh3dRkw= +google.golang.org/api v0.82.0/go.mod h1:Ld58BeTlL9DIYr2M2ajvoSqmGLei0BMn+kVBmkam1os= 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= @@ -560,6 +571,8 @@ 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/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= @@ -623,8 +636,14 @@ google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211223182754-3ac035c7e7cb/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220111164026-67b88f271998/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220114231437-d2e6a121cae0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220201184016-50beb8ab5c44/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220211171837-173942840c17/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220216160803-4663080d8bc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= @@ -637,10 +656,10 @@ google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= 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-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58 h1:a221mAAEAzq4Lz6ZWRkcS8ptb2mxoxYSt4N68aRyQHM= google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58/go.mod h1:yKyY4AMRwFiC8yMMNaMi+RkCnjZJt9LoWuvhXjMs+To= +google.golang.org/genproto v0.0.0-20220602131408-e326c6e8e9c8 h1:qRu95HZ148xXw+XeZ3dvqe85PxH4X8+jIo0iRPKcEnM= +google.golang.org/genproto v0.0.0-20220602131408-e326c6e8e9c8/go.mod h1:yKyY4AMRwFiC8yMMNaMi+RkCnjZJt9LoWuvhXjMs+To= 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= @@ -670,8 +689,9 @@ google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= 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 h1:u+MLGgVf7vRdjEYZ8wDFhAVNmhkbJ5hmrA1LMWK1CAQ= 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/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= diff --git a/log/log.go b/log/log.go new file mode 100644 index 00000000..5dac2327 --- /dev/null +++ b/log/log.go @@ -0,0 +1,129 @@ +package log + +import ( + "log" + "strings" + "sync" +) + +// Level is a well-known log level, as defined below +type Level int + +// Well known log levels +const ( + TraceLevel Level = iota + DebugLevel + InfoLevel + WarnLevel + ErrorLevel +) + +func (l Level) String() string { + switch l { + case TraceLevel: + return "TRACE" + case DebugLevel: + return "DEBUG" + case InfoLevel: + return "INFO" + case WarnLevel: + return "WARN" + case ErrorLevel: + return "ERROR" + } + return "unknown" +} + +var ( + level = InfoLevel + mu = &sync.Mutex{} +) + +// Trace prints the given message, if the current log level is TRACE +func Trace(message string, v ...interface{}) { + logIf(TraceLevel, message, v...) +} + +// Debug prints the given message, if the current log level is DEBUG or lower +func Debug(message string, v ...interface{}) { + logIf(DebugLevel, message, v...) +} + +// Info prints the given message, if the current log level is INFO or lower +func Info(message string, v ...interface{}) { + logIf(InfoLevel, message, v...) +} + +// Warn prints the given message, if the current log level is WARN or lower +func Warn(message string, v ...interface{}) { + logIf(WarnLevel, message, v...) +} + +// Error prints the given message, if the current log level is ERROR or lower +func Error(message string, v ...interface{}) { + logIf(ErrorLevel, message, v...) +} + +// Fatal prints the given message, and exits the program +func Fatal(v ...interface{}) { + log.Fatalln(v...) +} + +// CurrentLevel returns the current log level +func CurrentLevel() Level { + mu.Lock() + defer mu.Unlock() + return level +} + +// SetLevel sets a new log level +func SetLevel(newLevel Level) { + mu.Lock() + defer mu.Unlock() + level = newLevel +} + +// DisableDates disables the date/time prefix +func DisableDates() { + log.SetFlags(0) +} + +// ToLevel converts a string to a Level. It returns InfoLevel if the string +// does not match any known log levels. +func ToLevel(s string) Level { + switch strings.ToUpper(s) { + case "TRACE": + return TraceLevel + case "DEBUG": + return DebugLevel + case "INFO": + return InfoLevel + case "WARN", "WARNING": + return WarnLevel + case "ERROR": + return ErrorLevel + default: + return InfoLevel + } +} + +// Loggable returns true if the given log level is lower or equal to the current log level +func Loggable(l Level) bool { + return CurrentLevel() <= l +} + +// IsTrace returns true if the current log level is TraceLevel +func IsTrace() bool { + return Loggable(TraceLevel) +} + +// IsDebug returns true if the current log level is DebugLevel or below +func IsDebug() bool { + return Loggable(DebugLevel) +} + +func logIf(l Level, message string, v ...interface{}) { + if CurrentLevel() <= l { + log.Printf(l.String()+" "+message, v...) + } +} diff --git a/server/config.go b/server/config.go index 4db52a33..60d3cdf9 100644 --- a/server/config.go +++ b/server/config.go @@ -6,15 +6,16 @@ import ( // Defines default config settings (excluding limits, see below) const ( - DefaultListenHTTP = ":80" - DefaultCacheDuration = 12 * time.Hour - DefaultKeepaliveInterval = 45 * time.Second // Not too frequently to save battery (Android read timeout used to be 77s!) - DefaultManagerInterval = time.Minute - DefaultAtSenderInterval = 10 * time.Second - DefaultMinDelay = 10 * time.Second - DefaultMaxDelay = 3 * 24 * time.Hour - DefaultFirebaseKeepaliveInterval = 3 * time.Hour // ~control topic (Android), not too frequently to save battery - DefaultFirebasePollInterval = 20 * time.Minute // ~poll topic (iOS), max. 2-3 times per hour (see docs) + DefaultListenHTTP = ":80" + DefaultCacheDuration = 12 * time.Hour + DefaultKeepaliveInterval = 45 * time.Second // Not too frequently to save battery (Android read timeout used to be 77s!) + DefaultManagerInterval = time.Minute + DefaultDelayedSenderInterval = 10 * time.Second + DefaultMinDelay = 10 * time.Second + DefaultMaxDelay = 3 * 24 * time.Hour + DefaultFirebaseKeepaliveInterval = 3 * time.Hour // ~control topic (Android), not too frequently to save battery + DefaultFirebasePollInterval = 20 * time.Minute // ~poll topic (iOS), max. 2-3 times per hour (see docs) + DefaultFirebaseQuotaExceededPenaltyDuration = 10 * time.Minute // Time that over-users are locked out of Firebase if it returns "quota exceeded" ) // Defines all global and per-visitor limits @@ -66,9 +67,10 @@ type Config struct { KeepaliveInterval time.Duration ManagerInterval time.Duration WebRootIsApp bool - AtSenderInterval time.Duration + DelayedSenderInterval time.Duration FirebaseKeepaliveInterval time.Duration FirebasePollInterval time.Duration + FirebaseQuotaExceededPenaltyDuration time.Duration UpstreamBaseURL string SMTPSenderAddr string SMTPSenderUser string @@ -118,9 +120,10 @@ func NewConfig() *Config { MessageLimit: DefaultMessageLengthLimit, MinDelay: DefaultMinDelay, MaxDelay: DefaultMaxDelay, - AtSenderInterval: DefaultAtSenderInterval, + DelayedSenderInterval: DefaultDelayedSenderInterval, FirebaseKeepaliveInterval: DefaultFirebaseKeepaliveInterval, FirebasePollInterval: DefaultFirebasePollInterval, + FirebaseQuotaExceededPenaltyDuration: DefaultFirebaseQuotaExceededPenaltyDuration, TotalTopicLimit: DefaultTotalTopicLimit, VisitorSubscriptionLimit: DefaultVisitorSubscriptionLimit, VisitorAttachmentTotalSizeLimit: DefaultVisitorAttachmentTotalSizeLimit, diff --git a/server/message_cache.go b/server/message_cache.go index b55c34ba..77aa4f78 100644 --- a/server/message_cache.go +++ b/server/message_cache.go @@ -6,8 +6,8 @@ import ( "errors" "fmt" _ "github.com/mattn/go-sqlite3" // SQLite driver + "heckel.io/ntfy/log" "heckel.io/ntfy/util" - "log" "strings" "time" ) @@ -36,7 +36,7 @@ const ( attachment_size INT NOT NULL, attachment_expires INT NOT NULL, attachment_url TEXT NOT NULL, - attachment_owner TEXT NOT NULL, + sender TEXT NOT NULL, encoding TEXT NOT NULL, published INT NOT NULL ); @@ -45,37 +45,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, attachment_owner, encoding, published) + 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` pruneMessagesQuery = `DELETE FROM messages WHERE time < ? AND published = 1` selectRowIDFromMessageID = `SELECT id FROM messages WHERE topic = ? AND mid = ?` selectMessagesSinceTimeQuery = ` - SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding + SELECT mid, time, topic, message, title, priority, tags, click, 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, attachment_owner, encoding + SELECT mid, time, topic, message, title, priority, tags, click, 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, attachment_owner, encoding + SELECT mid, time, topic, message, title, priority, tags, click, 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, attachment_owner, encoding + SELECT mid, time, topic, message, title, priority, tags, click, 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, attachment_owner, encoding + SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding FROM messages WHERE time <= ? AND published = 0 ORDER BY time, id @@ -84,13 +84,13 @@ const ( selectMessagesCountQuery = `SELECT COUNT(*) FROM messages` selectMessageCountForTopicQuery = `SELECT COUNT(*) FROM messages WHERE topic = ?` selectTopicsQuery = `SELECT topic FROM messages GROUP BY topic` - selectAttachmentsSizeQuery = `SELECT IFNULL(SUM(attachment_size), 0) FROM messages WHERE attachment_owner = ? AND attachment_expires >= ?` + selectAttachmentsSizeQuery = `SELECT IFNULL(SUM(attachment_size), 0) FROM messages WHERE sender = ? AND attachment_expires >= ?` selectAttachmentsExpiredQuery = `SELECT mid FROM messages WHERE attachment_expires > 0 AND attachment_expires < ?` ) // Schema management queries const ( - currentSchemaVersion = 6 + currentSchemaVersion = 7 createSchemaVersionTableQuery = ` CREATE TABLE IF NOT EXISTS schemaVersion ( id INT PRIMARY KEY, @@ -173,6 +173,11 @@ const ( migrate5To6AlterMessagesTableQuery = ` ALTER TABLE messages ADD COLUMN actions TEXT NOT NULL DEFAULT(''); ` + + // 6 -> 7 + migrate6To7AlterMessagesTableQuery = ` + ALTER TABLE messages RENAME COLUMN attachment_owner TO sender; + ` ) type messageCache struct { @@ -225,7 +230,7 @@ func (c *messageCache) AddMessage(m *message) error { } published := m.Time <= time.Now().Unix() tags := strings.Join(m.Tags, ",") - var attachmentName, attachmentType, attachmentURL, attachmentOwner string + var attachmentName, attachmentType, attachmentURL string var attachmentSize, attachmentExpires int64 if m.Attachment != nil { attachmentName = m.Attachment.Name @@ -233,7 +238,6 @@ func (c *messageCache) AddMessage(m *message) error { attachmentSize = m.Attachment.Size attachmentExpires = m.Attachment.Expires attachmentURL = m.Attachment.URL - attachmentOwner = m.Attachment.Owner } var actionsStr string if len(m.Actions) > 0 { @@ -259,7 +263,7 @@ func (c *messageCache) AddMessage(m *message) error { attachmentSize, attachmentExpires, attachmentURL, - attachmentOwner, + m.Sender, m.Encoding, published, ) @@ -371,8 +375,8 @@ func (c *messageCache) Prune(olderThan time.Time) error { return err } -func (c *messageCache) AttachmentBytesUsed(owner string) (int64, error) { - rows, err := c.db.Query(selectAttachmentsSizeQuery, owner, time.Now().Unix()) +func (c *messageCache) AttachmentBytesUsed(sender string) (int64, error) { + rows, err := c.db.Query(selectAttachmentsSizeQuery, sender, time.Now().Unix()) if err != nil { return 0, err } @@ -415,7 +419,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, attachmentOwner, encoding string + var id, topic, msg, title, tagsStr, click, actionsStr, attachmentName, attachmentType, attachmentURL, sender, encoding string err := rows.Scan( &id, ×tamp, @@ -431,7 +435,7 @@ func readMessages(rows *sql.Rows) ([]*message, error) { &attachmentSize, &attachmentExpires, &attachmentURL, - &attachmentOwner, + &sender, &encoding, ) if err != nil { @@ -455,7 +459,6 @@ func readMessages(rows *sql.Rows) ([]*message, error) { Size: attachmentSize, Expires: attachmentExpires, URL: attachmentURL, - Owner: attachmentOwner, } } messages = append(messages, &message{ @@ -470,6 +473,7 @@ func readMessages(rows *sql.Rows) ([]*message, error) { Click: click, Actions: actions, Attachment: att, + Sender: sender, Encoding: encoding, }) } @@ -516,6 +520,8 @@ func setupCacheDB(db *sql.DB) error { return migrateFrom4(db) } else if schemaVersion == 5 { return migrateFrom5(db) + } else if schemaVersion == 6 { + return migrateFrom6(db) } return fmt.Errorf("unexpected schema version found: %d", schemaVersion) } @@ -534,7 +540,7 @@ func setupNewCacheDB(db *sql.DB) error { } func migrateFrom0(db *sql.DB) error { - log.Print("Migrating cache database schema: from 0 to 1") + log.Info("Migrating cache database schema: from 0 to 1") if _, err := db.Exec(migrate0To1AlterMessagesTableQuery); err != nil { return err } @@ -548,7 +554,7 @@ func migrateFrom0(db *sql.DB) error { } func migrateFrom1(db *sql.DB) error { - log.Print("Migrating cache database schema: from 1 to 2") + log.Info("Migrating cache database schema: from 1 to 2") if _, err := db.Exec(migrate1To2AlterMessagesTableQuery); err != nil { return err } @@ -559,7 +565,7 @@ func migrateFrom1(db *sql.DB) error { } func migrateFrom2(db *sql.DB) error { - log.Print("Migrating cache database schema: from 2 to 3") + log.Info("Migrating cache database schema: from 2 to 3") if _, err := db.Exec(migrate2To3AlterMessagesTableQuery); err != nil { return err } @@ -570,7 +576,7 @@ func migrateFrom2(db *sql.DB) error { } func migrateFrom3(db *sql.DB) error { - log.Print("Migrating cache database schema: from 3 to 4") + log.Info("Migrating cache database schema: from 3 to 4") if _, err := db.Exec(migrate3To4AlterMessagesTableQuery); err != nil { return err } @@ -581,7 +587,7 @@ func migrateFrom3(db *sql.DB) error { } func migrateFrom4(db *sql.DB) error { - log.Print("Migrating cache database schema: from 4 to 5") + log.Info("Migrating cache database schema: from 4 to 5") if _, err := db.Exec(migrate4To5AlterMessagesTableQuery); err != nil { return err } @@ -592,12 +598,23 @@ func migrateFrom4(db *sql.DB) error { } func migrateFrom5(db *sql.DB) error { - log.Print("Migrating cache database schema: from 5 to 6") + log.Info("Migrating cache database schema: from 5 to 6") if _, err := db.Exec(migrate5To6AlterMessagesTableQuery); err != nil { return err } if _, err := db.Exec(updateSchemaVersion, 6); err != nil { return err } + return migrateFrom6(db) +} + +func migrateFrom6(db *sql.DB) error { + log.Info("Migrating cache database schema: from 6 to 7") + if _, err := db.Exec(migrate6To7AlterMessagesTableQuery); err != nil { + return err + } + if _, err := db.Exec(updateSchemaVersion, 7); err != nil { + return err + } return nil // Update this when a new version is added } diff --git a/server/message_cache_test.go b/server/message_cache_test.go index cb888b42..398f21e4 100644 --- a/server/message_cache_test.go +++ b/server/message_cache_test.go @@ -281,39 +281,39 @@ func testCacheAttachments(t *testing.T, c *messageCache) { expires1 := time.Now().Add(-4 * time.Hour).Unix() m := newDefaultMessage("mytopic", "flower for you") m.ID = "m1" + m.Sender = "1.2.3.4" m.Attachment = &attachment{ Name: "flower.jpg", Type: "image/jpeg", Size: 5000, Expires: expires1, URL: "https://ntfy.sh/file/AbDeFgJhal.jpg", - Owner: "1.2.3.4", } require.Nil(t, c.AddMessage(m)) expires2 := time.Now().Add(2 * time.Hour).Unix() // Future m = newDefaultMessage("mytopic", "sending you a car") m.ID = "m2" + m.Sender = "1.2.3.4" m.Attachment = &attachment{ Name: "car.jpg", Type: "image/jpeg", Size: 10000, Expires: expires2, URL: "https://ntfy.sh/file/aCaRURL.jpg", - Owner: "1.2.3.4", } require.Nil(t, c.AddMessage(m)) expires3 := time.Now().Add(1 * time.Hour).Unix() // Future m = newDefaultMessage("another-topic", "sending you another car") m.ID = "m3" + m.Sender = "1.2.3.4" m.Attachment = &attachment{ Name: "another-car.jpg", Type: "image/jpeg", Size: 20000, Expires: expires3, URL: "https://ntfy.sh/file/zakaDHFW.jpg", - Owner: "1.2.3.4", } require.Nil(t, c.AddMessage(m)) @@ -327,7 +327,7 @@ func testCacheAttachments(t *testing.T, c *messageCache) { require.Equal(t, int64(5000), messages[0].Attachment.Size) require.Equal(t, expires1, messages[0].Attachment.Expires) require.Equal(t, "https://ntfy.sh/file/AbDeFgJhal.jpg", messages[0].Attachment.URL) - require.Equal(t, "1.2.3.4", messages[0].Attachment.Owner) + require.Equal(t, "1.2.3.4", messages[0].Sender) require.Equal(t, "sending you a car", messages[1].Message) require.Equal(t, "car.jpg", messages[1].Attachment.Name) @@ -335,7 +335,7 @@ func testCacheAttachments(t *testing.T, c *messageCache) { require.Equal(t, int64(10000), messages[1].Attachment.Size) require.Equal(t, expires2, messages[1].Attachment.Expires) require.Equal(t, "https://ntfy.sh/file/aCaRURL.jpg", messages[1].Attachment.URL) - require.Equal(t, "1.2.3.4", messages[1].Attachment.Owner) + require.Equal(t, "1.2.3.4", messages[1].Sender) size, err := c.AttachmentBytesUsed("1.2.3.4") require.Nil(t, err) diff --git a/server/ntfy.service b/server/ntfy.service index 6645b21f..8bf250a5 100644 --- a/server/ntfy.service +++ b/server/ntfy.service @@ -5,7 +5,8 @@ After=network.target [Service] User=ntfy Group=ntfy -ExecStart=/usr/bin/ntfy serve +ExecStart=/usr/bin/ntfy serve --no-log-dates +ExecReload=/bin/kill --signal HUP $MAINPID Restart=on-failure AmbientCapabilities=CAP_NET_BIND_SERVICE LimitNOFILE=10000 diff --git a/server/server.go b/server/server.go index 86ed7539..0b909b80 100644 --- a/server/server.go +++ b/server/server.go @@ -7,13 +7,11 @@ import ( "embed" "encoding/base64" "encoding/json" - "errors" "fmt" + "heckel.io/ntfy/log" "io" - "log" "net" "net/http" - "net/http/httptest" "net/url" "os" "path" @@ -34,22 +32,22 @@ import ( // Server is the main server, providing the UI and API for ntfy type Server struct { - config *Config - httpServer *http.Server - httpsServer *http.Server - unixListener net.Listener - smtpServer *smtp.Server - smtpBackend *smtpBackend - topics map[string]*topic - visitors map[string]*visitor - firebase subscriber - mailer mailer - messages int64 - auth auth.Auther - messageCache *messageCache - fileCache *fileCache - closeChan chan bool - mu sync.Mutex + config *Config + httpServer *http.Server + httpsServer *http.Server + unixListener net.Listener + smtpServer *smtp.Server + smtpServerBackend *smtpBackend + smtpSender mailer + topics map[string]*topic + visitors map[string]*visitor + firebaseClient *firebaseClient + messages int64 + auth auth.Auther + messageCache *messageCache + fileCache *fileCache + closeChan chan bool + mu sync.Mutex } // handleFunc extends the normal http.HandlerFunc to be able to easily return errors @@ -136,23 +134,23 @@ func New(conf *Config) (*Server, error) { return nil, err } } - var firebaseSubscriber subscriber + var firebaseClient *firebaseClient if conf.FirebaseKeyFile != "" { - var err error - firebaseSubscriber, err = createFirebaseSubscriber(conf.FirebaseKeyFile, auther) + sender, err := newFirebaseSender(conf.FirebaseKeyFile) if err != nil { return nil, err } + firebaseClient = newFirebaseClient(sender, auther) } return &Server{ - config: conf, - messageCache: messageCache, - fileCache: fileCache, - firebase: firebaseSubscriber, - mailer: mailer, - topics: topics, - auth: auther, - visitors: make(map[string]*visitor), + config: conf, + messageCache: messageCache, + fileCache: fileCache, + firebaseClient: firebaseClient, + smtpSender: mailer, + topics: topics, + auth: auther, + visitors: make(map[string]*visitor), }, nil } @@ -181,7 +179,7 @@ func (s *Server) Run() error { if s.config.SMTPServerListen != "" { listenStr += fmt.Sprintf(" %s[smtp]", s.config.SMTPServerListen) } - log.Printf("Listening on%s", listenStr) + log.Info("Listening on%s, log level is %s", listenStr, log.CurrentLevel().String()) mux := http.NewServeMux() mux.HandleFunc("/", s.handle) errChan := make(chan error) @@ -221,7 +219,7 @@ func (s *Server) Run() error { } s.mu.Unlock() go s.runManager() - go s.runAtSender() + go s.runDelayedSender() go s.runFirebaseKeepaliver() return <-errChan @@ -248,16 +246,27 @@ func (s *Server) Stop() { func (s *Server) handle(w http.ResponseWriter, r *http.Request) { v := s.visitor(r) + log.Debug("%s Dispatching request", logHTTPPrefix(v, r)) if err := s.handleInternal(w, r, v); err != nil { if websocket.IsWebSocketUpgrade(r) { - log.Printf("[%s] WS %s %s - %s", v.ip, r.Method, r.URL.Path, err.Error()) + isNormalError := strings.Contains(err.Error(), "i/o timeout") + if isNormalError { + log.Debug("%s WebSocket error (this error is okay, it happens a lot): %s", logHTTPPrefix(v, r), err.Error()) + } else { + log.Info("%s WebSocket error: %s", logHTTPPrefix(v, r), err.Error()) + } return // Do not attempt to write to upgraded connection } httpErr, ok := err.(*errHTTP) if !ok { httpErr = errHTTPInternalError } - log.Printf("[%s] HTTP %s %s - %d - %d - %s", v.ip, r.Method, r.URL.Path, httpErr.HTTPCode, httpErr.Code, err.Error()) + isNormalError := httpErr.HTTPCode == http.StatusNotFound || httpErr.HTTPCode == http.StatusBadRequest + if isNormalError { + log.Debug("%s Connection closed with HTTP %d (ntfy error %d): %s", logHTTPPrefix(v, r), httpErr.HTTPCode, httpErr.Code, err.Error()) + } else { + log.Info("%s Connection closed with HTTP %d (ntfy error %d): %s", logHTTPPrefix(v, r), httpErr.HTTPCode, httpErr.Code, err.Error()) + } w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests w.WriteHeader(httpErr.HTTPCode) @@ -434,19 +443,26 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito m.Message = emptyMessageBody } delayed := m.Time > time.Now().Unix() + log.Debug("%s Received message: event=%s, body=%d byte(s), delayed=%t, firebase=%t, cache=%t, up=%t, email=%s", + logMessagePrefix(v, m), m.Event, len(m.Message), delayed, firebase, cache, unifiedpush, email) + if log.IsTrace() { + log.Trace("%s Message body: %s", logMessagePrefix(v, m), util.MaybeMarshalJSON(m)) + } if !delayed { - if err := t.Publish(m); err != nil { + if err := t.Publish(v, m); err != nil { return err } - } - if s.firebase != nil && firebase && !delayed { - go s.sendToFirebase(v, m) - } - if s.mailer != nil && email != "" && !delayed { - go s.sendEmail(v, m, email) - } - if s.config.UpstreamBaseURL != "" { - go s.forwardPollRequest(v, m) + if s.firebaseClient != nil && firebase { + go s.sendToFirebase(v, m) + } + if s.smtpSender != nil && email != "" { + go s.sendEmail(v, m, email) + } + if s.config.UpstreamBaseURL != "" { + go s.forwardPollRequest(v, m) + } + } else { + log.Debug("%s Message delayed, will process later", logMessagePrefix(v, m)) } if cache { if err := s.messageCache.AddMessage(m); err != nil { @@ -465,14 +481,20 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito } func (s *Server) sendToFirebase(v *visitor, m *message) { - if err := s.firebase(m); err != nil { - log.Printf("[%s] FB - Unable to publish to Firebase: %v", v.ip, err.Error()) + log.Debug("%s Publishing to Firebase", logMessagePrefix(v, m)) + if err := s.firebaseClient.Send(v, m); err != nil { + if err == errFirebaseTemporarilyBanned { + log.Debug("%s Unable to publish to Firebase: %v", logMessagePrefix(v, m), err.Error()) + } else { + log.Warn("%s Unable to publish to Firebase: %v", logMessagePrefix(v, m), err.Error()) + } } } func (s *Server) sendEmail(v *visitor, m *message, email string) { - if err := s.mailer.Send(v.ip, email, m); err != nil { - log.Printf("[%s] MAIL - Unable to send email: %v", v.ip, err.Error()) + log.Debug("%s Sending email to %s", logMessagePrefix(v, m), email) + if err := s.smtpSender.Send(v, m, email); err != nil { + log.Warn("%s Unable to send email to %s: %v", logMessagePrefix(v, m), email, err.Error()) } } @@ -480,18 +502,22 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) { topicURL := fmt.Sprintf("%s/%s", s.config.BaseURL, m.Topic) topicHash := fmt.Sprintf("%x", sha256.Sum256([]byte(topicURL))) forwardURL := fmt.Sprintf("%s/%s", s.config.UpstreamBaseURL, topicHash) + log.Debug("%s Publishing poll request to %s", logMessagePrefix(v, m), forwardURL) req, err := http.NewRequest("POST", forwardURL, strings.NewReader("")) if err != nil { - log.Printf("[%s] FWD - Unable to forward poll request: %v", v.ip, err.Error()) + log.Warn("%s Unable to publish poll request: %v", logMessagePrefix(v, m), err.Error()) return } req.Header.Set("X-Poll-ID", m.ID) - response, err := http.DefaultClient.Do(req) + var httpClient = &http.Client{ + Timeout: time.Second * 10, + } + response, err := httpClient.Do(req) if err != nil { - log.Printf("[%s] FWD - Unable to forward poll request: %v", v.ip, err.Error()) + log.Warn("%s Unable to publish poll request: %v", logMessagePrefix(v, m), err.Error()) return } else if response.StatusCode != http.StatusOK { - log.Printf("[%s] FWD - Unable to forward poll request, unexpected status: %d", v.ip, response.StatusCode) + log.Warn("%s Unable to publish poll request, unexpected HTTP status: %d", logMessagePrefix(v, m), response.StatusCode) return } } @@ -533,7 +559,7 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca return false, false, "", false, errHTTPTooManyRequestsLimitEmails } } - if s.mailer == nil && email != "" { + if s.smtpSender == nil && email != "" { return false, false, "", false, errHTTPBadRequestEmailDisabled } messageStr := strings.ReplaceAll(readParam(r, "x-message", "message", "m"), "\\n", "\n") @@ -568,6 +594,7 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca return false, false, "", false, errHTTPBadRequestDelayTooLarge } m.Time = delay.Unix() + m.Sender = v.ip // Important for rate limiting } actionsStr := readParam(r, "x-actions", "actions", "action") if actionsStr != "" { @@ -606,7 +633,7 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca // 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 nil + return s.handleBodyDiscard(body) } else if unifiedpush { return s.handleBodyAsMessageAutoDetect(m, body) // Case 2 } else if m.Attachment != nil && m.Attachment.URL != "" { @@ -619,6 +646,12 @@ func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body return s.handleBodyAsAttachment(r, v, m, body) // Case 6 } +func (s *Server) handleBodyDiscard(body *util.PeekedReadCloser) error { + _, err := io.Copy(io.Discard, body) + _ = body.Close() + return err +} + func (s *Server) handleBodyAsMessageAutoDetect(m *message, body *util.PeekedReadCloser) error { if utf8.Valid(body.PeekedBytes) { m.Message = string(body.PeekedBytes) // Do not trim @@ -663,7 +696,7 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message, m.Attachment = &attachment{} } var ext string - m.Attachment.Owner = v.ip // Important for attachment rate limiting + m.Sender = v.ip // Important for attachment rate limiting m.Attachment.Expires = time.Now().Add(s.config.AttachmentExpiryDuration).Unix() m.Attachment.Type, ext = util.DetectContentType(body.PeekedBytes, m.Attachment.Name) m.Attachment.URL = fmt.Sprintf("%s/file/%s%s", s.config.BaseURL, m.ID, ext) @@ -718,6 +751,8 @@ func (s *Server) handleSubscribeRaw(w http.ResponseWriter, r *http.Request, v *v } func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v *visitor, contentType string, encoder messageEncoder) error { + log.Debug("%s HTTP stream connection opened", logHTTPPrefix(v, r)) + defer log.Debug("%s HTTP stream connection closed", logHTTPPrefix(v, r)) if err := v.SubscriptionAllowed(); err != nil { return errHTTPTooManyRequestsLimitSubscriptions } @@ -731,7 +766,7 @@ func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v * return err } var wlock sync.Mutex - sub := func(msg *message) error { + sub := func(v *visitor, msg *message) error { if !filters.Pass(msg) { return nil } @@ -752,7 +787,7 @@ func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v * w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests w.Header().Set("Content-Type", contentType+"; charset=utf-8") // Android/Volley client needs charset! if poll { - return s.sendOldMessages(topics, since, scheduled, sub) + return s.sendOldMessages(topics, since, scheduled, v, sub) } subscriberIDs := make([]int, 0) for _, t := range topics { @@ -763,10 +798,10 @@ func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v * topics[i].Unsubscribe(subscriberID) // Order! } }() - if err := sub(newOpenMessage(topicsStr)); err != nil { // Send out open message + if err := sub(v, newOpenMessage(topicsStr)); err != nil { // Send out open message return err } - if err := s.sendOldMessages(topics, since, scheduled, sub); err != nil { + if err := s.sendOldMessages(topics, since, scheduled, v, sub); err != nil { return err } for { @@ -774,8 +809,9 @@ func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v * case <-r.Context().Done(): return nil case <-time.After(s.config.KeepaliveInterval): + log.Trace("%s Sending keepalive message", logHTTPPrefix(v, r)) v.Keepalive() - if err := sub(newKeepaliveMessage(topicsStr)); err != nil { // Send keepalive message + if err := sub(v, newKeepaliveMessage(topicsStr)); err != nil { // Send keepalive message return err } } @@ -790,6 +826,8 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi return errHTTPTooManyRequestsLimitSubscriptions } defer v.RemoveSubscription() + log.Debug("%s WebSocket connection opened", logHTTPPrefix(v, r)) + defer log.Debug("%s WebSocket connection closed", logHTTPPrefix(v, r)) topics, topicsStr, err := s.topicsFromPath(r.URL.Path) if err != nil { return err @@ -819,6 +857,7 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi return err } conn.SetPongHandler(func(appData string) error { + log.Trace("%s Received WebSocket pong", logHTTPPrefix(v, r)) return conn.SetReadDeadline(time.Now().Add(pongWait)) }) for { @@ -835,6 +874,7 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi if err := conn.SetWriteDeadline(time.Now().Add(wsWriteWait)); err != nil { return err } + log.Trace("%s Sending WebSocket ping", logHTTPPrefix(v, r)) return conn.WriteMessage(websocket.PingMessage, nil) } for { @@ -849,7 +889,7 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi } } }) - sub := func(msg *message) error { + sub := func(v *visitor, msg *message) error { if !filters.Pass(msg) { return nil } @@ -862,7 +902,7 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi } w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests if poll { - return s.sendOldMessages(topics, since, scheduled, sub) + return s.sendOldMessages(topics, since, scheduled, v, sub) } subscriberIDs := make([]int, 0) for _, t := range topics { @@ -873,15 +913,16 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi topics[i].Unsubscribe(subscriberID) // Order! } }() - if err := sub(newOpenMessage(topicsStr)); err != nil { // Send out open message + if err := sub(v, newOpenMessage(topicsStr)); err != nil { // Send out open message return err } - if err := s.sendOldMessages(topics, since, scheduled, sub); err != nil { + if err := s.sendOldMessages(topics, since, scheduled, v, sub); err != nil { return err } err = g.Wait() - if err != nil && websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) { - return nil // Normal closures are not errors + if err != nil && websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { + log.Trace("%s WebSocket connection closed: %s", logHTTPPrefix(v, r), err.Error()) + return nil // Normal closures are not errors; note: "1006 (abnormal closure)" is treated as normal, because people disconnect a lot } return err } @@ -900,7 +941,7 @@ func parseSubscribeParams(r *http.Request) (poll bool, since sinceMarker, schedu return } -func (s *Server) sendOldMessages(topics []*topic, since sinceMarker, scheduled bool, sub subscriber) error { +func (s *Server) sendOldMessages(topics []*topic, since sinceMarker, scheduled bool, v *visitor, sub subscriber) error { if since.IsNone() { return nil } @@ -910,7 +951,7 @@ func (s *Server) sendOldMessages(topics []*topic, since sinceMarker, scheduled b return err } for _, m := range messages { - if err := sub(m); err != nil { + if err := sub(v, m); err != nil { return err } } @@ -1004,28 +1045,36 @@ func (s *Server) updateStatsAndPrune() { defer s.mu.Unlock() // Expire visitors from rate visitors map + staleVisitors := 0 for ip, v := range s.visitors { if v.Stale() { + log.Debug("Deleting stale visitor %s", v.ip) delete(s.visitors, ip) + staleVisitors++ } } + log.Debug("Manager: Deleted %d stale visitor(s)", staleVisitors) // Delete expired attachments if s.fileCache != nil { ids, err := s.messageCache.AttachmentsExpired() - if err == nil { + if err != nil { + log.Warn("Error retrieving expired attachments: %s", err.Error()) + } else if len(ids) > 0 { + log.Debug("Manager: Deleting expired attachments: %v", ids) if err := s.fileCache.Remove(ids...); err != nil { - log.Printf("error while deleting attachments: %s", err.Error()) + log.Warn("Error deleting attachments: %s", err.Error()) } } else { - log.Printf("error retrieving expired attachments: %s", err.Error()) + log.Debug("Manager: No expired attachments to delete") } } // Prune message cache olderThan := time.Now().Add(-1 * s.config.CacheDuration) + log.Debug("Manager: Pruning messages older than %s", olderThan.Format("2006-01-02 15:04:05")) if err := s.messageCache.Prune(olderThan); err != nil { - log.Printf("error pruning cache: %s", err.Error()) + log.Warn("Manager: Error pruning cache: %s", err.Error()) } // Prune old topics, remove subscriptions without subscribers @@ -1034,7 +1083,7 @@ func (s *Server) updateStatsAndPrune() { subs := t.Subscribers() msgs, err := s.messageCache.MessageCount(t.ID) if err != nil { - log.Printf("cannot get stats for topic %s: %s", t.ID, err.Error()) + log.Warn("Manager: Cannot get stats for topic %s: %s", t.ID, err.Error()) continue } if msgs == 0 && subs == 0 { @@ -1046,35 +1095,25 @@ func (s *Server) updateStatsAndPrune() { } // Mail stats - var mailSuccess, mailFailure int64 - if s.smtpBackend != nil { - mailSuccess, mailFailure = s.smtpBackend.Counts() + var receivedMailTotal, receivedMailSuccess, receivedMailFailure int64 + if s.smtpServerBackend != nil { + receivedMailTotal, receivedMailSuccess, receivedMailFailure = s.smtpServerBackend.Counts() + } + var sentMailTotal, sentMailSuccess, sentMailFailure int64 + if s.smtpSender != nil { + sentMailTotal, sentMailSuccess, sentMailFailure = s.smtpSender.Counts() } // Print stats - log.Printf("Stats: %d message(s) published, %d in cache, %d successful mails, %d failed, %d topic(s) active, %d subscriber(s), %d visitor(s)", - s.messages, messages, mailSuccess, mailFailure, len(s.topics), subscribers, len(s.visitors)) + log.Info("Stats: %d messages published, %d in cache, %d topic(s) active, %d subscriber(s), %d visitor(s), %d mails received (%d successful, %d failed), %d mails sent (%d successful, %d failed)", + s.messages, messages, len(s.topics), subscribers, len(s.visitors), + receivedMailTotal, receivedMailSuccess, receivedMailFailure, + sentMailTotal, sentMailSuccess, sentMailFailure) } func (s *Server) runSMTPServer() error { - sub := func(m *message) error { - url := fmt.Sprintf("%s/%s", s.config.BaseURL, m.Topic) - req, err := http.NewRequest("PUT", url, strings.NewReader(m.Message)) - if err != nil { - return err - } - if m.Title != "" { - req.Header.Set("Title", m.Title) - } - rr := httptest.NewRecorder() - s.handle(rr, req) - if rr.Code != http.StatusOK { - return errors.New("error: " + rr.Body.String()) - } - return nil - } - s.smtpBackend = newMailBackend(s.config, sub) - s.smtpServer = smtp.NewServer(s.smtpBackend) + s.smtpServerBackend = newMailBackend(s.config, s.handle) + s.smtpServer = smtp.NewServer(s.smtpServerBackend) s.smtpServer.Addr = s.config.SMTPServerListen s.smtpServer.Domain = s.config.SMTPServerDomain s.smtpServer.ReadTimeout = 10 * time.Second @@ -1096,32 +1135,29 @@ func (s *Server) runManager() { } } -func (s *Server) runAtSender() { +func (s *Server) runFirebaseKeepaliver() { + if s.firebaseClient == nil { + return + } + v := newVisitor(s.config, s.messageCache, "0.0.0.0") // Background process, not a real visitor for { select { - case <-time.After(s.config.AtSenderInterval): - if err := s.sendDelayedMessages(); err != nil { - log.Printf("error sending scheduled messages: %s", err.Error()) - } + case <-time.After(s.config.FirebaseKeepaliveInterval): + s.sendToFirebase(v, newKeepaliveMessage(firebaseControlTopic)) + case <-time.After(s.config.FirebasePollInterval): + s.sendToFirebase(v, newKeepaliveMessage(firebasePollTopic)) case <-s.closeChan: return } } } -func (s *Server) runFirebaseKeepaliver() { - if s.firebase == nil { - return - } +func (s *Server) runDelayedSender() { for { select { - case <-time.After(s.config.FirebaseKeepaliveInterval): - if err := s.firebase(newKeepaliveMessage(firebaseControlTopic)); err != nil { - log.Printf("error sending Firebase keepalive message to %s: %s", firebaseControlTopic, err.Error()) - } - case <-time.After(s.config.FirebasePollInterval): - if err := s.firebase(newKeepaliveMessage(firebasePollTopic)); err != nil { - log.Printf("error sending Firebase keepalive message to %s: %s", firebasePollTopic, err.Error()) + case <-time.After(s.config.DelayedSenderInterval): + if err := s.sendDelayedMessages(); err != nil { + log.Warn("Error sending delayed messages: %s", err.Error()) } case <-s.closeChan: return @@ -1130,27 +1166,40 @@ func (s *Server) runFirebaseKeepaliver() { } func (s *Server) sendDelayedMessages() error { - s.mu.Lock() - defer s.mu.Unlock() messages, err := s.messageCache.MessagesDue() if err != nil { return err } for _, m := range messages { - t, ok := s.topics[m.Topic] // If no subscribers, just mark message as published - if ok { - if err := t.Publish(m); err != nil { - log.Printf("unable to publish message %s to topic %s: %v", m.ID, m.Topic, err.Error()) + v := s.visitorFromIP(m.Sender) + if err := s.sendDelayedMessage(v, m); err != nil { + log.Warn("%s Error sending delayed message: %s", logMessagePrefix(v, m), err.Error()) + } + } + return nil +} + +func (s *Server) sendDelayedMessage(v *visitor, m *message) error { + s.mu.Lock() + defer s.mu.Unlock() + log.Debug("%s Sending delayed message", logMessagePrefix(v, m)) + t, ok := s.topics[m.Topic] // If no subscribers, just mark message as published + if ok { + go func() { + // We do not rate-limit messages here, since we've rate limited them in the PUT/POST handler + if err := t.Publish(v, m); err != nil { + log.Warn("%s Unable to publish message: %v", logMessagePrefix(v, m), err.Error()) } - } - if s.firebase != nil { // Firebase subscribers may not show up in topics map - if err := s.firebase(m); err != nil { - log.Printf("unable to publish to Firebase: %v", err.Error()) - } - } - if err := s.messageCache.MarkPublished(m); err != nil { - return err - } + }() + } + if s.firebaseClient != nil { // Firebase subscribers may not show up in topics map + go s.sendToFirebase(v, m) + } + if s.config.UpstreamBaseURL != "" { + go s.forwardPollRequest(v, m) + } + if err := s.messageCache.MarkPublished(m); err != nil { + return err } return nil } @@ -1252,13 +1301,13 @@ func (s *Server) withAuth(next handleFunc, perm auth.Permission) handleFunc { username, password, ok := extractUserPass(r) if ok { if user, err = s.auth.Authenticate(username, password); err != nil { - log.Printf("authentication failed: %s", err.Error()) + log.Info("authentication failed: %s", err.Error()) return errHTTPUnauthorized } } for _, t := range topics { if err := s.auth.Authorize(user, t.ID, perm); err != nil { - log.Printf("unauthorized: %s", err.Error()) + log.Info("unauthorized: %s", err.Error()) return errHTTPForbidden } } @@ -1290,8 +1339,6 @@ func extractUserPass(r *http.Request) (username string, password string, ok bool // visitor creates or retrieves a rate.Limiter for the given visitor. // This function was taken from https://www.alexedwards.net/blog/how-to-rate-limit-http-requests (MIT). func (s *Server) visitor(r *http.Request) *visitor { - s.mu.Lock() - defer s.mu.Unlock() remoteAddr := r.RemoteAddr ip, _, err := net.SplitHostPort(remoteAddr) if err != nil { @@ -1300,6 +1347,12 @@ func (s *Server) visitor(r *http.Request) *visitor { if s.config.BehindProxy && r.Header.Get("X-Forwarded-For") != "" { ip = r.Header.Get("X-Forwarded-For") } + return s.visitorFromIP(ip) +} + +func (s *Server) visitorFromIP(ip string) *visitor { + s.mu.Lock() + defer s.mu.Unlock() v, exists := s.visitors[ip] if !exists { s.visitors[ip] = newVisitor(s.config, s.messageCache, ip) diff --git a/server/server.yml b/server/server.yml index ce7b1c75..63c32209 100644 --- a/server/server.yml +++ b/server/server.yml @@ -178,3 +178,11 @@ # # visitor-attachment-total-size-limit: "100M" # visitor-attachment-daily-bandwidth-limit: "500M" + +# Log level, can be TRACE, DEBUG, INFO, WARN or ERROR +# This option can be hot-reloaded by calling "kill -HUP $pid" or "systemctl reload ntfy". +# +# Be aware that DEBUG (and particularly TRACE) can be VERY CHATTY. Only turn them on for +# debugging purposes, or your disk will fill up quickly. +# +# log-level: INFO diff --git a/server/server_firebase.go b/server/server_firebase.go index 40ca806a..99f0ba13 100644 --- a/server/server_firebase.go +++ b/server/server_firebase.go @@ -3,13 +3,15 @@ package server import ( "context" "encoding/json" + "errors" + firebase "firebase.google.com/go/v4" + "firebase.google.com/go/v4/messaging" "fmt" - "strings" - - firebase "firebase.google.com/go" - "firebase.google.com/go/messaging" "google.golang.org/api/option" "heckel.io/ntfy/auth" + "heckel.io/ntfy/log" + "heckel.io/ntfy/util" + "strings" ) const ( @@ -17,25 +19,79 @@ const ( fcmApnsBodyMessageLimit = 100 ) -func createFirebaseSubscriber(credentialsFile string, auther auth.Auther) (subscriber, error) { +var ( + errFirebaseQuotaExceeded = errors.New("quota exceeded for Firebase messages to topic") + errFirebaseTemporarilyBanned = errors.New("visitor temporarily banned from using Firebase") +) + +// firebaseClient is a generic client that formats and sends messages to Firebase. +// The actual Firebase implementation is implemented in firebaseSenderImpl, to make it testable. +type firebaseClient struct { + sender firebaseSender + auther auth.Auther +} + +func newFirebaseClient(sender firebaseSender, auther auth.Auther) *firebaseClient { + return &firebaseClient{ + sender: sender, + auther: auther, + } +} + +func (c *firebaseClient) Send(v *visitor, m *message) error { + if err := v.FirebaseAllowed(); err != nil { + return errFirebaseTemporarilyBanned + } + fbm, err := toFirebaseMessage(m, c.auther) + if err != nil { + return err + } + if log.IsTrace() { + log.Trace("%s Firebase message: %s", logMessagePrefix(v, m), util.MaybeMarshalJSON(fbm)) + } + err = c.sender.Send(fbm) + if err == errFirebaseQuotaExceeded { + log.Warn("%s Firebase quota exceeded (likely for topic), temporarily denying Firebase access to visitor", logMessagePrefix(v, m)) + v.FirebaseTemporarilyDeny() + } + return err +} + +// firebaseSender is an interface that represents a client that can send to Firebase Cloud Messaging. +// In tests, this can be implemented with a mock. +type firebaseSender interface { + // Send sends a message to Firebase, or returns an error. It returns errFirebaseQuotaExceeded + // if a rate limit has reached. + Send(m *messaging.Message) error +} + +// firebaseSenderImpl is a firebaseSender that actually talks to Firebase +type firebaseSenderImpl struct { + client *messaging.Client +} + +func newFirebaseSender(credentialsFile string) (*firebaseSenderImpl, error) { fb, err := firebase.NewApp(context.Background(), nil, option.WithCredentialsFile(credentialsFile)) if err != nil { return nil, err } - msg, err := fb.Messaging(context.Background()) + client, err := fb.Messaging(context.Background()) if err != nil { return nil, err } - return func(m *message) error { - fbm, err := toFirebaseMessage(m, auther) - if err != nil { - return err - } - _, err = msg.Send(context.Background(), fbm) - return err + return &firebaseSenderImpl{ + client: client, }, nil } +func (c *firebaseSenderImpl) Send(m *messaging.Message) error { + _, err := c.client.Send(context.Background(), m) + if err != nil && messaging.IsQuotaExceeded(err) { + return errFirebaseQuotaExceeded + } + return err +} + // toFirebaseMessage converts a message to a Firebase message. // // Normal messages ("message"): diff --git a/server/server_firebase_test.go b/server/server_firebase_test.go index c990b930..b004b0fa 100644 --- a/server/server_firebase_test.go +++ b/server/server_firebase_test.go @@ -3,11 +3,12 @@ package server import ( "encoding/json" "errors" - "firebase.google.com/go/messaging" + "firebase.google.com/go/v4/messaging" "fmt" "github.com/stretchr/testify/require" "heckel.io/ntfy/auth" "strings" + "sync" "testing" ) @@ -26,6 +27,35 @@ func (t testAuther) Authorize(_ *auth.User, _ string, _ auth.Permission) error { return errors.New("unauthorized") } +type testFirebaseSender struct { + allowed int + messages []*messaging.Message + mu sync.Mutex +} + +func newTestFirebaseSender(allowed int) *testFirebaseSender { + return &testFirebaseSender{ + allowed: allowed, + messages: make([]*messaging.Message, 0), + } +} + +func (s *testFirebaseSender) Send(m *messaging.Message) error { + s.mu.Lock() + defer s.mu.Unlock() + if len(s.messages)+1 > s.allowed { + return errFirebaseQuotaExceeded + } + s.messages = append(s.messages, m) + return nil +} + +func (s *testFirebaseSender) Messages() []*messaging.Message { + s.mu.Lock() + defer s.mu.Unlock() + return append(make([]*messaging.Message, 0), s.messages...) +} + func TestToFirebaseMessage_Keepalive(t *testing.T) { m := newKeepaliveMessage("mytopic") fbm, err := toFirebaseMessage(m, nil) @@ -119,7 +149,6 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) { Size: 12345, Expires: 98765543, URL: "https://example.com/file.jpg", - Owner: "some-owner", } fbm, err := toFirebaseMessage(m, &testAuther{Allow: true}) require.Nil(t, err) @@ -286,3 +315,22 @@ func TestMaybeTruncateFCMMessage_NotTooLong(t *testing.T) { require.Equal(t, len(serializedOrigFCMMessage), len(serializedNotTruncatedFCMMessage)) require.Equal(t, "", notTruncatedFCMMessage.Data["truncated"]) } + +func TestToFirebaseSender_Abuse(t *testing.T) { + sender := &testFirebaseSender{allowed: 2} + client := newFirebaseClient(sender, &testAuther{}) + visitor := newVisitor(newTestConfig(t), newMemTestCache(t), "1.2.3.4") + + require.Nil(t, client.Send(visitor, &message{Topic: "mytopic"})) + require.Equal(t, 1, len(sender.Messages())) + + require.Nil(t, client.Send(visitor, &message{Topic: "mytopic"})) + require.Equal(t, 2, len(sender.Messages())) + + require.Equal(t, errFirebaseQuotaExceeded, client.Send(visitor, &message{Topic: "mytopic"})) + require.Equal(t, 2, len(sender.Messages())) + + sender.messages = make([]*messaging.Message, 0) // Reset to test that time limit is working + require.Equal(t, errFirebaseTemporarilyBanned, client.Send(visitor, &message{Topic: "mytopic"})) + require.Equal(t, 0, len(sender.Messages())) +} diff --git a/server/server_test.go b/server/server_test.go index 06f3cd2d..8010643f 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -9,7 +9,6 @@ import ( "math/rand" "net/http" "net/http/httptest" - "os" "path/filepath" "strings" "sync" @@ -55,6 +54,21 @@ func TestServer_PublishAndPoll(t *testing.T) { require.Equal(t, "my second message", lines[1]) // \n -> " " } +func TestServer_PublishWithFirebase(t *testing.T) { + sender := newTestFirebaseSender(10) + s := newTestServer(t, newTestConfig(t)) + s.firebaseClient = newFirebaseClient(sender, &testAuther{Allow: true}) + + response := request(t, s, "PUT", "/mytopic", "my first message", nil) + msg1 := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg1.ID) + require.Equal(t, "my first message", msg1.Message) + require.Equal(t, 1, len(sender.Messages())) + require.Equal(t, "my first message", sender.Messages()[0].Data["message"]) + require.Equal(t, "my first message", sender.Messages()[0].APNS.Payload.Aps.Alert.Body) + require.Equal(t, "my first message", sender.Messages()[0].APNS.Payload.CustomData["message"]) +} + func TestServer_SubscribeOpenAndKeepalive(t *testing.T) { c := newTestConfig(t) c.KeepaliveInterval = time.Second @@ -264,7 +278,7 @@ func TestServer_PublishNoCache(t *testing.T) { func TestServer_PublishAt(t *testing.T) { c := newTestConfig(t) c.MinDelay = time.Second - c.AtSenderInterval = 100 * time.Millisecond + c.DelayedSenderInterval = 100 * time.Millisecond s := newTestServer(t, c) response := request(t, s, "PUT", "/mytopic", "a message", map[string]string{ @@ -283,6 +297,13 @@ func TestServer_PublishAt(t *testing.T) { messages = toMessages(t, response.Body.String()) require.Equal(t, 1, len(messages)) require.Equal(t, "a message", messages[0].Message) + require.Equal(t, "", messages[0].Sender) // Never return the sender! + + messages, err := s.messageCache.Messages("mytopic", sinceAllMessages, true) + require.Nil(t, err) + require.Equal(t, 1, len(messages)) + require.Equal(t, "a message", messages[0].Message) + require.Equal(t, "9.9.9.9", messages[0].Sender) // It's stored in the DB though! } func TestServer_PublishAtWithCacheError(t *testing.T) { @@ -454,29 +475,9 @@ func TestServer_PublishMessageInHeaderWithNewlines(t *testing.T) { require.Equal(t, "Line 1\nLine 2", msg.Message) // \\n -> \n ! } -func TestServer_PublishFirebase(t *testing.T) { - // This is unfortunately not much of a test, since it merely fires the messages towards Firebase, - // but cannot re-read them. There is no way from Go to read the messages back, or even get an error back. - // I tried everything. I already had written the test, and it increases the code coverage, so I'll leave it ... :shrug: ... - - c := newTestConfig(t) - c.FirebaseKeyFile = firebaseServiceAccountFile(t) // May skip the test! - s := newTestServer(t, c) - - // Normal message - response := request(t, s, "PUT", "/mytopic", "This is a message for firebase", nil) - msg := toMessage(t, response.Body.String()) - require.NotEmpty(t, msg.ID) - - // Keepalive message - require.Nil(t, s.firebase(newKeepaliveMessage(firebaseControlTopic))) - - time.Sleep(500 * time.Millisecond) // Time for sends -} - func TestServer_PublishInvalidTopic(t *testing.T) { s := newTestServer(t, newTestConfig(t)) - s.mailer = &testMailer{} + s.smtpSender = &testMailer{} response := request(t, s, "PUT", "/docs", "fail", nil) require.Equal(t, 40010, toHTTPError(t, response.Body.String()).Code) } @@ -742,13 +743,17 @@ type testMailer struct { mu sync.Mutex } -func (t *testMailer) Send(from, to string, m *message) error { +func (t *testMailer) Send(v *visitor, m *message, to string) error { t.mu.Lock() defer t.mu.Unlock() t.count++ return nil } +func (t *testMailer) Counts() (total int64, success int64, failure int64) { + return 0, 0, 0 +} + func (t *testMailer) Count() int { t.mu.Lock() defer t.mu.Unlock() @@ -794,7 +799,7 @@ func TestServer_PublishTooRequests_ShortReplenish(t *testing.T) { func TestServer_PublishTooManyEmails_Defaults(t *testing.T) { s := newTestServer(t, newTestConfig(t)) - s.mailer = &testMailer{} + s.smtpSender = &testMailer{} for i := 0; i < 16; i++ { response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), map[string]string{ "E-Mail": "test@example.com", @@ -811,7 +816,7 @@ func TestServer_PublishTooManyEmails_Replenish(t *testing.T) { c := newTestConfig(t) c.VisitorEmailLimitReplenish = 500 * time.Millisecond s := newTestServer(t, c) - s.mailer = &testMailer{} + s.smtpSender = &testMailer{} for i := 0; i < 16; i++ { response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), map[string]string{ "E-Mail": "test@example.com", @@ -837,7 +842,7 @@ func TestServer_PublishTooManyEmails_Replenish(t *testing.T) { func TestServer_PublishDelayedEmail_Fail(t *testing.T) { s := newTestServer(t, newTestConfig(t)) - s.mailer = &testMailer{} + s.smtpSender = &testMailer{} response := request(t, s, "PUT", "/mytopic", "fail", map[string]string{ "E-Mail": "test@example.com", "Delay": "20 min", @@ -955,7 +960,7 @@ func TestServer_PublishAsJSON(t *testing.T) { func TestServer_PublishAsJSON_WithEmail(t *testing.T) { mailer := &testMailer{} s := newTestServer(t, newTestConfig(t)) - s.mailer = mailer + s.smtpSender = mailer body := `{"topic":"mytopic","message":"A message","email":"phil@example.com"}` response := request(t, s, "PUT", "/", body, nil) require.Equal(t, 200, response.Code) @@ -1018,7 +1023,7 @@ func TestServer_PublishAttachment(t *testing.T) { require.Equal(t, int64(5000), msg.Attachment.Size) require.GreaterOrEqual(t, msg.Attachment.Expires, time.Now().Add(179*time.Minute).Unix()) // Almost 3 hours require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/") - require.Equal(t, "", msg.Attachment.Owner) // Should never be returned + require.Equal(t, "", msg.Sender) // Should never be returned require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, msg.ID)) path := strings.TrimPrefix(msg.Attachment.URL, "http://127.0.0.1:12345") @@ -1047,7 +1052,7 @@ func TestServer_PublishAttachmentShortWithFilename(t *testing.T) { require.Equal(t, int64(21), msg.Attachment.Size) require.GreaterOrEqual(t, msg.Attachment.Expires, time.Now().Add(3*time.Hour).Unix()) require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/") - require.Equal(t, "", msg.Attachment.Owner) // Should never be returned + require.Equal(t, "", msg.Sender) // Should never be returned require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, msg.ID)) path := strings.TrimPrefix(msg.Attachment.URL, "http://127.0.0.1:12345") @@ -1074,7 +1079,7 @@ func TestServer_PublishAttachmentExternalWithoutFilename(t *testing.T) { require.Equal(t, "", msg.Attachment.Type) require.Equal(t, int64(0), msg.Attachment.Size) require.Equal(t, int64(0), msg.Attachment.Expires) - require.Equal(t, "", msg.Attachment.Owner) + require.Equal(t, "", msg.Sender) // Slightly unrelated cross-test: make sure we don't add an owner for external attachments size, err := s.messageCache.AttachmentBytesUsed("127.0.0.1") @@ -1095,7 +1100,7 @@ func TestServer_PublishAttachmentExternalWithFilename(t *testing.T) { require.Equal(t, "", msg.Attachment.Type) require.Equal(t, int64(0), msg.Attachment.Size) require.Equal(t, int64(0), msg.Attachment.Expires) - require.Equal(t, "", msg.Attachment.Owner) + require.Equal(t, "", msg.Sender) } func TestServer_PublishAttachmentBadURL(t *testing.T) { @@ -1333,18 +1338,6 @@ func toHTTPError(t *testing.T, s string) *errHTTP { return &e } -func firebaseServiceAccountFile(t *testing.T) string { - if os.Getenv("NTFY_TEST_FIREBASE_SERVICE_ACCOUNT_FILE") != "" { - return os.Getenv("NTFY_TEST_FIREBASE_SERVICE_ACCOUNT_FILE") - } else if os.Getenv("NTFY_TEST_FIREBASE_SERVICE_ACCOUNT") != "" { - filename := filepath.Join(t.TempDir(), "firebase.json") - require.NotNil(t, os.WriteFile(filename, []byte(os.Getenv("NTFY_TEST_FIREBASE_SERVICE_ACCOUNT")), 0o600)) - return filename - } - t.SkipNow() - return "" -} - func basicAuth(s string) string { return fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(s))) } diff --git a/server/smtp_sender.go b/server/smtp_sender.go index 15f004c1..1ccaf084 100644 --- a/server/smtp_sender.go +++ b/server/smtp_sender.go @@ -4,33 +4,62 @@ import ( _ "embed" // required by go:embed "encoding/json" "fmt" + "heckel.io/ntfy/log" "heckel.io/ntfy/util" "mime" "net" "net/smtp" "strings" + "sync" "time" ) type mailer interface { - Send(from, to string, m *message) error + Send(v *visitor, m *message, to string) error + Counts() (total int64, success int64, failure int64) } type smtpSender struct { - config *Config + config *Config + success int64 + failure int64 + mu sync.Mutex } -func (s *smtpSender) Send(senderIP, to string, m *message) error { - host, _, err := net.SplitHostPort(s.config.SMTPSenderAddr) +func (s *smtpSender) Send(v *visitor, m *message, to string) error { + return s.withCount(v, m, func() error { + host, _, err := net.SplitHostPort(s.config.SMTPSenderAddr) + if err != nil { + return err + } + message, err := formatMail(s.config.BaseURL, v.ip, s.config.SMTPSenderFrom, to, m) + if err != nil { + return err + } + auth := smtp.PlainAuth("", s.config.SMTPSenderUser, s.config.SMTPSenderPass, host) + log.Debug("%s Sending mail: via=%s, user=%s, pass=***, to=%s", logMessagePrefix(v, m), s.config.SMTPSenderAddr, s.config.SMTPSenderUser, to) + log.Trace("%s Mail body: %s", logMessagePrefix(v, m), message) + return smtp.SendMail(s.config.SMTPSenderAddr, auth, s.config.SMTPSenderFrom, []string{to}, []byte(message)) + }) +} + +func (s *smtpSender) Counts() (total int64, success int64, failure int64) { + s.mu.Lock() + defer s.mu.Unlock() + return s.success + s.failure, s.success, s.failure +} + +func (s *smtpSender) withCount(v *visitor, m *message, fn func() error) error { + err := fn() + s.mu.Lock() + defer s.mu.Unlock() if err != nil { - return err + log.Debug("%s Sending mail failed: %s", logMessagePrefix(v, m), err.Error()) + s.failure++ + } else { + s.success++ } - message, err := formatMail(s.config.BaseURL, senderIP, s.config.SMTPSenderFrom, to, m) - if err != nil { - return err - } - auth := smtp.PlainAuth("", s.config.SMTPSenderUser, s.config.SMTPSenderPass, host) - return smtp.SendMail(s.config.SMTPSenderAddr, auth, s.config.SMTPSenderFrom, []string{to}, []byte(message)) + return err } func formatMail(baseURL, senderIP, from, to string, m *message) (string, error) { diff --git a/server/smtp_server.go b/server/smtp_server.go index c437d235..3f4b9b68 100644 --- a/server/smtp_server.go +++ b/server/smtp_server.go @@ -3,10 +3,15 @@ package server import ( "bytes" "errors" + "fmt" "github.com/emersion/go-smtp" + "heckel.io/ntfy/log" "io" "mime" "mime/multipart" + "net" + "net/http" + "net/http/httptest" "net/mail" "strings" "sync" @@ -23,49 +28,55 @@ var ( // smtpBackend implements SMTP server methods. type smtpBackend struct { config *Config - sub subscriber + handler func(http.ResponseWriter, *http.Request) success int64 failure int64 mu sync.Mutex } -func newMailBackend(conf *Config, sub subscriber) *smtpBackend { +func newMailBackend(conf *Config, handler func(http.ResponseWriter, *http.Request)) *smtpBackend { return &smtpBackend{ - config: conf, - sub: sub, + config: conf, + handler: handler, } } func (b *smtpBackend) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) { - return &smtpSession{backend: b}, nil + log.Debug("%s Incoming mail, login with user %s", logSMTPPrefix(state), username) + return &smtpSession{backend: b, state: state}, nil } func (b *smtpBackend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) { - return &smtpSession{backend: b}, nil + log.Debug("%s Incoming mail, anonymous login", logSMTPPrefix(state)) + return &smtpSession{backend: b, state: state}, nil } -func (b *smtpBackend) Counts() (success int64, failure int64) { +func (b *smtpBackend) Counts() (total int64, success int64, failure int64) { b.mu.Lock() defer b.mu.Unlock() - return b.success, b.failure + return b.success + b.failure, b.success, b.failure } // smtpSession is returned after EHLO. type smtpSession struct { backend *smtpBackend + state *smtp.ConnectionState topic string mu sync.Mutex } func (s *smtpSession) AuthPlain(username, password string) error { + log.Debug("%s AUTH PLAIN (with username %s)", logSMTPPrefix(s.state), username) return nil } func (s *smtpSession) Mail(from string, opts smtp.MailOptions) error { + log.Debug("%s MAIL FROM: %s (with options: %#v)", logSMTPPrefix(s.state), from, opts) return nil } func (s *smtpSession) Rcpt(to string) error { + log.Debug("%s RCPT TO: %s", logSMTPPrefix(s.state), to) return s.withFailCount(func() error { conf := s.backend.config addressList, err := mail.ParseAddressList(to) @@ -102,6 +113,11 @@ func (s *smtpSession) Data(r io.Reader) error { if err != nil { return err } + if log.IsTrace() { + log.Trace("%s DATA: %s", logSMTPPrefix(s.state), string(b)) + } else if log.IsDebug() { + log.Debug("%s DATA: %d byte(s)", logSMTPPrefix(s.state), len(b)) + } msg, err := mail.ReadMessage(bytes.NewReader(b)) if err != nil { return err @@ -128,7 +144,7 @@ func (s *smtpSession) Data(r io.Reader) error { m.Message = m.Title // Flip them, this makes more sense m.Title = "" } - if err := s.backend.sub(m); err != nil { + if err := s.publishMessage(m); err != nil { return err } s.backend.mu.Lock() @@ -138,6 +154,33 @@ func (s *smtpSession) Data(r io.Reader) error { }) } +func (s *smtpSession) publishMessage(m *message) error { + // Extract remote address (for rate limiting) + remoteAddr, _, err := net.SplitHostPort(s.state.RemoteAddr.String()) + if err != nil { + remoteAddr = s.state.RemoteAddr.String() + } + + // Call HTTP handler with fake HTTP request + url := fmt.Sprintf("%s/%s", s.backend.config.BaseURL, m.Topic) + req, err := http.NewRequest("POST", url, strings.NewReader(m.Message)) + req.RequestURI = "/" + m.Topic // just for the logs + req.RemoteAddr = remoteAddr // rate limiting!! + req.Header.Set("X-Forwarded-For", remoteAddr) + if err != nil { + return err + } + if m.Title != "" { + req.Header.Set("Title", m.Title) + } + rr := httptest.NewRecorder() + s.backend.handler(rr, req) + if rr.Code != http.StatusOK { + return errors.New("error: " + rr.Body.String()) + } + return nil +} + func (s *smtpSession) Reset() { s.mu.Lock() s.topic = "" @@ -153,6 +196,9 @@ func (s *smtpSession) withFailCount(fn func() error) error { s.backend.mu.Lock() defer s.backend.mu.Unlock() if err != nil { + // Almost all of these errors are parse errors, and user input errors. + // We do not want to spam the log with WARN messages. + log.Debug("%s Incoming mail error: %s", logSMTPPrefix(s.state), err.Error()) s.backend.failure++ } return err diff --git a/server/smtp_server_test.go b/server/smtp_server_test.go index d0e8bfd2..8e9d5892 100644 --- a/server/smtp_server_test.go +++ b/server/smtp_server_test.go @@ -3,6 +3,9 @@ package server import ( "github.com/emersion/go-smtp" "github.com/stretchr/testify/require" + "io" + "net" + "net/http" "strings" "testing" ) @@ -27,13 +30,12 @@ Content-Type: text/html; charset="UTF-8"
what's up

--000000000000f3320b05d42915c9--` - _, backend := newTestBackend(t, func(m *message) error { - require.Equal(t, "mytopic", m.Topic) - require.Equal(t, "and one more", m.Title) - require.Equal(t, "what's up", m.Message) - return nil + _, backend := newTestBackend(t, func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/mytopic", r.URL.Path) + require.Equal(t, "and one more", r.Header.Get("Title")) + require.Equal(t, "what's up", readAll(t, r.Body)) }) - session, _ := backend.AnonymousLogin(nil) + session, _ := backend.AnonymousLogin(fakeConnState(t, "1.2.3.4")) require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{})) require.Nil(t, session.Rcpt("ntfy-mytopic@ntfy.sh")) require.Nil(t, session.Data(strings.NewReader(email))) @@ -59,13 +61,12 @@ Content-Type: text/html; charset="UTF-8"

--000000000000bcf4a405d429f8d4--` - _, backend := newTestBackend(t, func(m *message) error { - require.Equal(t, "emailtest", m.Topic) - require.Equal(t, "", m.Title) // We flipped message and body - require.Equal(t, "This email has a subject but no body", m.Message) - return nil + _, backend := newTestBackend(t, func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/emailtest", r.URL.Path) + require.Equal(t, "", r.Header.Get("Title")) // We flipped message and body + require.Equal(t, "This email has a subject but no body", readAll(t, r.Body)) }) - session, _ := backend.AnonymousLogin(nil) + session, _ := backend.AnonymousLogin(fakeConnState(t, "1.2.3.4")) require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{})) require.Nil(t, session.Rcpt("ntfy-emailtest@ntfy.sh")) require.Nil(t, session.Data(strings.NewReader(email))) @@ -81,14 +82,13 @@ Content-Type: text/plain; charset="UTF-8" what's up ` - conf, backend := newTestBackend(t, func(m *message) error { - require.Equal(t, "mytopic", m.Topic) - require.Equal(t, "and one more", m.Title) - require.Equal(t, "what's up", m.Message) - return nil + conf, backend := newTestBackend(t, func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/mytopic", r.URL.Path) + require.Equal(t, "and one more", r.Header.Get("Title")) + require.Equal(t, "what's up", readAll(t, r.Body)) }) conf.SMTPServerAddrPrefix = "" - session, _ := backend.AnonymousLogin(nil) + session, _ := backend.AnonymousLogin(fakeConnState(t, "1.2.3.4")) require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{})) require.Nil(t, session.Rcpt("mytopic@ntfy.sh")) require.Nil(t, session.Data(strings.NewReader(email))) @@ -99,14 +99,13 @@ func TestSmtpBackend_Plaintext_No_ContentType(t *testing.T) { what's up ` - conf, backend := newTestBackend(t, func(m *message) error { - require.Equal(t, "mytopic", m.Topic) - require.Equal(t, "Very short mail", m.Title) - require.Equal(t, "what's up", m.Message) - return nil + conf, backend := newTestBackend(t, func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/mytopic", r.URL.Path) + require.Equal(t, "Very short mail", r.Header.Get("Title")) + require.Equal(t, "what's up", readAll(t, r.Body)) }) conf.SMTPServerAddrPrefix = "" - session, _ := backend.AnonymousLogin(nil) + session, _ := backend.AnonymousLogin(fakeConnState(t, "1.2.3.4")) require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{})) require.Nil(t, session.Rcpt("mytopic@ntfy.sh")) require.Nil(t, session.Data(strings.NewReader(email))) @@ -121,11 +120,10 @@ Content-Type: text/plain; charset="UTF-8" what's up ` - _, backend := newTestBackend(t, func(m *message) error { - require.Equal(t, "Three santas 🎅🎅🎅", m.Title) - return nil + _, backend := newTestBackend(t, func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "Three santas 🎅🎅🎅", r.Header.Get("Title")) }) - session, _ := backend.AnonymousLogin(nil) + session, _ := backend.AnonymousLogin(fakeConnState(t, "1.2.3.4")) require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{})) require.Nil(t, session.Rcpt("ntfy-mytopic@ntfy.sh")) require.Nil(t, session.Data(strings.NewReader(email))) @@ -140,7 +138,7 @@ To: mytopic@ntfy.sh Content-Type: text/plain; charset="UTF-8" you know this is a string. -it's a long string. +it's a long string. it's supposed to be longer than the max message length which is 4096 bytes, it used to be 512 bytes, but I increased that for the UnifiedPush support @@ -204,9 +202,9 @@ BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB that should do it ` - conf, backend := newTestBackend(t, func(m *message) error { + conf, backend := newTestBackend(t, func(w http.ResponseWriter, r *http.Request) { expected := `you know this is a string. -it's a long string. +it's a long string. it's supposed to be longer than the max message length which is 4096 bytes, it used to be 512 bytes, but I increased that for the UnifiedPush support @@ -266,13 +264,12 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ...................................................................... ...................................................................... and with BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB -BBBBBBBBBBBBBBBBBBBBBBBB` +BBBBBBBBBBBBBBBBBBBBBBBBB` require.Equal(t, 4096, len(expected)) // Sanity check - require.Equal(t, expected, m.Message) - return nil + require.Equal(t, expected, readAll(t, r.Body)) }) conf.SMTPServerAddrPrefix = "" - session, _ := backend.AnonymousLogin(nil) + session, _ := backend.AnonymousLogin(fakeConnState(t, "1.2.3.4")) require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{})) require.Nil(t, session.Rcpt("mytopic@ntfy.sh")) require.Nil(t, session.Data(strings.NewReader(email))) @@ -288,21 +285,41 @@ Content-Type: text/SOMETHINGELSE what's up ` - conf, backend := newTestBackend(t, func(m *message) error { - return nil + conf, backend := newTestBackend(t, func(http.ResponseWriter, *http.Request) { + // Nothing. }) conf.SMTPServerAddrPrefix = "" - session, _ := backend.Login(nil, "user", "pass") + session, _ := backend.Login(fakeConnState(t, "1.2.3.4"), "user", "pass") require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{})) require.Nil(t, session.Rcpt("mytopic@ntfy.sh")) require.Equal(t, errUnsupportedContentType, session.Data(strings.NewReader(email))) } -func newTestBackend(t *testing.T, sub subscriber) (*Config, *smtpBackend) { +func newTestBackend(t *testing.T, handler func(http.ResponseWriter, *http.Request)) (*Config, *smtpBackend) { conf := newTestConfig(t) conf.SMTPServerListen = ":25" conf.SMTPServerDomain = "ntfy.sh" conf.SMTPServerAddrPrefix = "ntfy-" - backend := newMailBackend(conf, sub) + backend := newMailBackend(conf, handler) return conf, backend } + +func readAll(t *testing.T, rc io.ReadCloser) string { + b, err := io.ReadAll(rc) + if err != nil { + t.Fatal(err) + } + return string(b) +} + +func fakeConnState(t *testing.T, remoteAddr string) *smtp.ConnectionState { + ip, err := net.ResolveIPAddr("ip", remoteAddr) + if err != nil { + t.Fatal(err) + } + return &smtp.ConnectionState{ + Hostname: "myhostname", + LocalAddr: ip, + RemoteAddr: ip, + } +} diff --git a/server/topic.go b/server/topic.go index 9badd7bd..889f1eb7 100644 --- a/server/topic.go +++ b/server/topic.go @@ -1,7 +1,7 @@ package server import ( - "log" + "heckel.io/ntfy/log" "math/rand" "sync" ) @@ -15,7 +15,7 @@ type topic struct { } // subscriber is a function that is called for every new message on a topic -type subscriber func(msg *message) error +type subscriber func(v *visitor, msg *message) error // newTopic creates a new topic func newTopic(id string) *topic { @@ -42,14 +42,19 @@ func (t *topic) Unsubscribe(id int) { } // Publish asynchronously publishes to all subscribers -func (t *topic) Publish(m *message) error { +func (t *topic) Publish(v *visitor, m *message) error { go func() { t.mu.Lock() defer t.mu.Unlock() - for _, s := range t.subscribers { - if err := s(m); err != nil { - log.Printf("error publishing message to subscriber") + if len(t.subscribers) > 0 { + log.Debug("%s Forwarding to %d subscriber(s)", logMessagePrefix(v, m), len(t.subscribers)) + for _, s := range t.subscribers { + if err := s(v, m); err != nil { + log.Warn("%s Error forwarding to subscriber", logMessagePrefix(v, m)) + } } + } else { + log.Trace("%s No stream or WebSocket subscribers, not forwarding", logMessagePrefix(v, m)) } }() return nil diff --git a/server/types.go b/server/types.go index 6a69338c..bb8e32a3 100644 --- a/server/types.go +++ b/server/types.go @@ -32,6 +32,7 @@ type message struct { Actions []*action `json:"actions,omitempty"` Attachment *attachment `json:"attachment,omitempty"` PollID string `json:"poll_id,omitempty"` + Sender string `json:"-"` // IP address of uploader, used for rate limiting Encoding string `json:"encoding,omitempty"` // empty for raw UTF-8, or "base64" for encoded bytes } @@ -41,7 +42,6 @@ type attachment struct { Size int64 `json:"size,omitempty"` Expires int64 `json:"expires,omitempty"` URL string `json:"url"` - Owner string `json:"-"` // IP address of uploader, used for rate limiting } type action struct { diff --git a/server/util.go b/server/util.go index 7c596344..d6b95a73 100644 --- a/server/util.go +++ b/server/util.go @@ -1,6 +1,8 @@ package server import ( + "fmt" + "github.com/emersion/go-smtp" "net/http" "strings" ) @@ -40,3 +42,19 @@ func readQueryParam(r *http.Request, names ...string) string { } return "" } + +func logMessagePrefix(v *visitor, m *message) string { + return fmt.Sprintf("%s/%s/%s", v.ip, m.Topic, m.ID) +} + +func logHTTPPrefix(v *visitor, r *http.Request) string { + requestURI := r.RequestURI + if requestURI == "" { + requestURI = r.URL.Path + } + return fmt.Sprintf("%s HTTP %s %s", v.ip, r.Method, requestURI) +} + +func logSMTPPrefix(state *smtp.ConnectionState) string { + return fmt.Sprintf("%s/%s SMTP", state.Hostname, state.RemoteAddr.String()) +} diff --git a/server/visitor.go b/server/visitor.go index 58cc28ab..5a8e186b 100644 --- a/server/visitor.go +++ b/server/visitor.go @@ -28,6 +28,7 @@ type visitor struct { emails *rate.Limiter subscriptions util.Limiter bandwidth util.Limiter + firebase time.Time // Next allowed Firebase message seen time.Time mu sync.Mutex } @@ -48,14 +49,11 @@ func newVisitor(conf *Config, messageCache *messageCache, ip string) *visitor { emails: rate.NewLimiter(rate.Every(conf.VisitorEmailLimitReplenish), conf.VisitorEmailLimitBurst), subscriptions: util.NewFixedLimiter(int64(conf.VisitorSubscriptionLimit)), bandwidth: util.NewBytesLimiter(conf.VisitorAttachmentDailyBandwidthLimit, 24*time.Hour), + firebase: time.Unix(0, 0), seen: time.Now(), } } -func (v *visitor) IP() string { - return v.ip -} - func (v *visitor) RequestAllowed() error { if !v.requests.Allow() { return errVisitorLimitReached @@ -63,6 +61,21 @@ func (v *visitor) RequestAllowed() error { return nil } +func (v *visitor) FirebaseAllowed() error { + v.mu.Lock() + defer v.mu.Unlock() + if time.Now().Before(v.firebase) { + return errVisitorLimitReached + } + return nil +} + +func (v *visitor) FirebaseTemporarilyDeny() { + v.mu.Lock() + defer v.mu.Unlock() + v.firebase = time.Now().Add(v.config.FirebaseQuotaExceededPenaltyDuration) +} + func (v *visitor) EmailAllowed() error { if !v.emails.Allow() { return errVisitorLimitReached diff --git a/tools/fbsend/main.go b/tools/fbsend/main.go index cd3a06d1..832aeb79 100644 --- a/tools/fbsend/main.go +++ b/tools/fbsend/main.go @@ -2,8 +2,8 @@ package main import ( "context" - firebase "firebase.google.com/go" - "firebase.google.com/go/messaging" + firebase "firebase.google.com/go/v4" + "firebase.google.com/go/v4/messaging" "flag" "fmt" "google.golang.org/api/option" diff --git a/util/util.go b/util/util.go index 7fc22d09..c8a42347 100644 --- a/util/util.go +++ b/util/util.go @@ -2,6 +2,7 @@ package util import ( "encoding/base64" + "encoding/json" "errors" "fmt" "github.com/gabriel-vasile/mimetype" @@ -264,3 +265,16 @@ func ReadPassword(in io.Reader) ([]byte, error) { func BasicAuth(user, pass string) string { return fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", user, pass)))) } + +// MaybeMarshalJSON returns a JSON string of the given object, or "" if serialization failed. +// This is useful for logging purposes where a failure doesn't matter that much. +func MaybeMarshalJSON(v interface{}) string { + jsonBytes, err := json.MarshalIndent(v, "", " ") + if err != nil { + return "" + } + if len(jsonBytes) > 5000 { + return string(jsonBytes)[:5000] + } + return string(jsonBytes) +} diff --git a/web/package-lock.json b/web/package-lock.json index c12cf8a1..505f2c63 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -471,9 +471,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.3.tgz", - "integrity": "sha512-rL50YcEuHbbauAFAysNsJA4/f89fGTOBRNs9P81sniKnKAr4xULe5AecolcsKbi88xu0ByWYDj/S1AJ3FSFuSQ==", + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.4.tgz", + "integrity": "sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow==", "bin": { "parser": "bin/babel-parser.js" }, @@ -1063,9 +1063,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.17.12", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.17.12.tgz", - "integrity": "sha512-jw8XW/B1i7Lqwqj2CbrViPcZijSxfguBWZP2aN59NHgxUyO/OcO1mfdCxH13QhN5LbWhPkX+f+brKGhZTiqtZQ==", + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.18.4.tgz", + "integrity": "sha512-+Hq10ye+jlvLEogSOtq4mKvtk7qwcUQ1f0Mrueai866C82f844Yom2cttfJdMdqRLTxWpsbfbkIkOIfovyUQXw==", "dependencies": { "@babel/helper-plugin-utils": "^7.17.12" }, @@ -1077,16 +1077,16 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.17.12", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.17.12.tgz", - "integrity": "sha512-cvO7lc7pZat6BsvH6l/EGaI8zpl8paICaoGk+7x7guvtfak/TbIf66nYmJOH13EuG0H+Xx3M+9LQDtSvZFKXKw==", + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.18.4.tgz", + "integrity": "sha512-e42NSG2mlKWgxKUAD9EJJSkZxR67+wZqzNxLSpc51T8tRU5SLFHsPmgYR5yr7sdgX4u+iHA1C5VafJ6AyImV3A==", "dependencies": { "@babel/helper-annotate-as-pure": "^7.16.7", - "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-environment-visitor": "^7.18.2", "@babel/helper-function-name": "^7.17.9", "@babel/helper-optimise-call-expression": "^7.16.7", "@babel/helper-plugin-utils": "^7.17.12", - "@babel/helper-replace-supers": "^7.16.7", + "@babel/helper-replace-supers": "^7.18.2", "@babel/helper-split-export-declaration": "^7.16.7", "globals": "^11.1.0" }, @@ -1276,9 +1276,9 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.18.0.tgz", - "integrity": "sha512-vwKpxdHnlM5tIrRt/eA0bzfbi7gUBLN08vLu38np1nZevlPySRe6yvuATJB5F/WPJ+ur4OXwpVYq9+BsxqAQuQ==", + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.18.4.tgz", + "integrity": "sha512-lH2UaQaHVOAeYrUUuZ8i38o76J/FnO8vu21OE+tD1MyP9lxdZoSfz+pDbWkq46GogUrdrMz3tiz/FYGB+bVThg==", "dependencies": { "@babel/helper-hoist-variables": "^7.16.7", "@babel/helper-module-transforms": "^7.18.0", @@ -1575,9 +1575,9 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.18.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.18.1.tgz", - "integrity": "sha512-F+RJmL479HJmC0KeqqwEGZMg1P7kWArLGbAKfEi9yPthJyMNjF+DjxFF/halfQvq1Q9GFM4TUbYDNV8xe4Ctqg==", + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.18.4.tgz", + "integrity": "sha512-l4vHuSLUajptpHNEOUDEGsnpl9pfRLsN1XUoDQDD/YBuXTM+v37SHGS+c6n4jdcZy96QtuUuSvZYMLSSsjH8Mw==", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.18.0", "@babel/helper-plugin-utils": "^7.17.12", @@ -1814,9 +1814,9 @@ } }, "node_modules/@babel/types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.2.tgz", - "integrity": "sha512-0On6B8A4/+mFUto5WERt3EEuG1NznDirvwca1O8UwXQHVY8g3R7OzYgxXdOfMwLO08UrpUD/2+3Bclyq+/C94Q==", + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.4.tgz", + "integrity": "sha512-ThN1mBcMq5pG/Vm2IcBmPPfyPXbd8S02rS+OBIDENdufvqC7Z/jHPCv9IcP01277aKtDI8g/2XysBN4hA8niiw==", "dependencies": { "@babel/helper-validator-identifier": "^7.16.7", "to-fast-properties": "^2.0.0" @@ -2004,6 +2004,24 @@ "postcss": "^8.3" } }, + "node_modules/@csstools/postcss-trigonometric-functions": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-1.0.1.tgz", + "integrity": "sha512-G78CY/+GePc6dDCTUbwI6TTFQ5fs3N9POHhI6v0QzteGpf6ylARiJUNz9HrRKi4eVYBNXjae1W2766iUEFxHlw==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, "node_modules/@csstools/postcss-unset-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.1.tgz", @@ -2947,6 +2965,28 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.1.tgz", + "integrity": "sha512-GcHwniMlA2z+WFPWuY8lp3fsza0I8xPFMWL5+n8LYyP6PSvPrXf4+n8stDHZY2DM0zy9sVkRDy1jDI4XGzYVqg==", + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.13", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz", @@ -2967,9 +3007,9 @@ "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==" }, "node_modules/@mui/base": { - "version": "5.0.0-alpha.82", - "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.82.tgz", - "integrity": "sha512-WUVDjCGnLXzmGxrmfW31blhucg0sRX4YddK2Falq7FlVzwdJaPgWn/xzPZmdLL0+WXon0gQVnDrq2qvggE/GMg==", + "version": "5.0.0-alpha.83", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.83.tgz", + "integrity": "sha512-/bFcjiI36R2Epf2Y3BkZOIdxrz5uMLqOU4cRai4igJ8DHTRMZDeKbOff0SdvwJNwg8r6oPUyoeOpsWkaOOX9/g==", "dependencies": { "@babel/runtime": "^7.17.2", "@emotion/is-prop-valid": "^1.1.2", @@ -2999,9 +3039,9 @@ } }, "node_modules/@mui/icons-material": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.8.0.tgz", - "integrity": "sha512-ScwLxa0q5VYV70Jfc60V/9VD0b9SvIeZ0Jddx2Dt2pBUFFO9vKdrbt9LYiT+4p21Au5NdYIb2XSHj46CLN1v3g==", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.8.2.tgz", + "integrity": "sha512-fP6KUCCZZjc2rdbMSmkNmBHDskLkmP0uCox57cbVXvomU6BOPrCxnr5YXsSsQrZB8fchx7hfH0bkAgvMZ5KM0Q==", "dependencies": { "@babel/runtime": "^7.17.2" }, @@ -3024,18 +3064,18 @@ } }, "node_modules/@mui/material": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.8.1.tgz", - "integrity": "sha512-Vl3BHFzOcAT5TJfvzoQUyuo/Xckn+/NSRyJ8upM4Hbz6Y1egW6P8f1RCa4FdkEfPSd5wSSYdmPfAiEh8eI4rPg==", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.8.2.tgz", + "integrity": "sha512-w/A1KG9Czf42uTyJOiRU5U1VullOz1R3xcsBvv3BtKCCWdVP+D6v/Yb8v0tJpIixMEbjeWzWGjotQBU0nd+yNA==", "dependencies": { "@babel/runtime": "^7.17.2", - "@mui/base": "5.0.0-alpha.82", - "@mui/system": "^5.8.1", + "@mui/base": "5.0.0-alpha.83", + "@mui/system": "^5.8.2", "@mui/types": "^7.1.3", "@mui/utils": "^5.8.0", "@types/react-transition-group": "^4.4.4", "clsx": "^1.1.1", - "csstype": "^3.0.11", + "csstype": "^3.1.0", "hoist-non-react-statics": "^3.3.2", "prop-types": "^15.8.1", "react-is": "^17.0.2", @@ -3124,9 +3164,9 @@ } }, "node_modules/@mui/system": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.8.1.tgz", - "integrity": "sha512-kWJMEN62+HJb4LMRNEAZQYc++FPYsqPsU9dCL7ByLgmz/ZzRrZ8FjDi2r4j0ZeE4kaVvqBXh+RA7tLzmCKqV9w==", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.8.2.tgz", + "integrity": "sha512-N74gDNKM+MnWvKTMmCPvCVLH4f0ZzakP1bcMDaPctrHwcyxNcEmtTGNpIiVk0Iu7vtThZAFL3DjHpINPGF7+cg==", "dependencies": { "@babel/runtime": "^7.17.2", "@mui/private-theming": "^5.8.0", @@ -3134,7 +3174,7 @@ "@mui/types": "^7.1.3", "@mui/utils": "^5.8.0", "clsx": "^1.1.1", - "csstype": "^3.0.11", + "csstype": "^3.1.0", "prop-types": "^15.8.1" }, "engines": { @@ -3832,9 +3872,9 @@ "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" }, "node_modules/@types/node": { - "version": "17.0.36", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.36.tgz", - "integrity": "sha512-V3orv+ggDsWVHP99K3JlwtH20R7J4IhI1Kksgc+64q5VxgfRkQG8Ws3MFm/FZOKDYGy9feGFlZ70/HpCNe9QaA==" + "version": "17.0.38", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.38.tgz", + "integrity": "sha512-5jY9RhV7c0Z4Jy09G+NIDTsCZ5G0L5n+Z+p+Y7t5VJHM30bgwzSjVtlcBxqAj+6L/swIlvtOSzr8rBk/aNyV2g==" }, "node_modules/@types/parse-json": { "version": "4.0.0", @@ -3867,9 +3907,9 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" }, "node_modules/@types/react": { - "version": "18.0.9", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.9.tgz", - "integrity": "sha512-9bjbg1hJHUm4De19L1cHiW0Jvx3geel6Qczhjd0qY5VKVE2X5+x77YxAepuCwVh4vrgZJdgEJw48zrhRIeF4Nw==", + "version": "18.0.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.10.tgz", + "integrity": "sha512-dIugadZuIPrRzvIEevIu7A1smqOAjkSMv8qOfwPt9Ve6i6JT/FQcCHyk2qIAxwsQNKZt5/oGR0T4z9h2dXRAkg==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -3967,13 +4007,13 @@ "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.26.0.tgz", - "integrity": "sha512-oGCmo0PqnRZZndr+KwvvAUvD3kNE4AfyoGCwOZpoCncSh4MVD06JTE8XQa2u9u+NX5CsyZMBTEc2C72zx38eYA==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.27.0.tgz", + "integrity": "sha512-DDrIA7GXtmHXr1VCcx9HivA39eprYBIFxbQEHI6NyraRDxCGpxAFiYQAT/1Y0vh1C+o2vfBiy4IuPoXxtTZCAQ==", "dependencies": { - "@typescript-eslint/scope-manager": "5.26.0", - "@typescript-eslint/type-utils": "5.26.0", - "@typescript-eslint/utils": "5.26.0", + "@typescript-eslint/scope-manager": "5.27.0", + "@typescript-eslint/type-utils": "5.27.0", + "@typescript-eslint/utils": "5.27.0", "debug": "^4.3.4", "functional-red-black-tree": "^1.0.1", "ignore": "^5.2.0", @@ -4013,11 +4053,11 @@ } }, "node_modules/@typescript-eslint/experimental-utils": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.26.0.tgz", - "integrity": "sha512-OgUGXC/teXD8PYOkn33RSwBJPVwL0I2ipm5OHr9g9cfAhVrPC2DxQiWqaq88MNO5mbr/ZWnav3EVBpuwDreS5Q==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.27.0.tgz", + "integrity": "sha512-ZOn342bYh19IYvkiorrqnzNoRAr91h3GiFSSfa4tlHV+R9GgR8SxCwAi8PKMyT8+pfwMxfQdNbwKsMurbF9hzg==", "dependencies": { - "@typescript-eslint/utils": "5.26.0" + "@typescript-eslint/utils": "5.27.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -4031,13 +4071,13 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.26.0.tgz", - "integrity": "sha512-n/IzU87ttzIdnAH5vQ4BBDnLPly7rC5VnjN3m0xBG82HK6rhRxnCb3w/GyWbNDghPd+NktJqB/wl6+YkzZ5T5Q==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.27.0.tgz", + "integrity": "sha512-8oGjQF46c52l7fMiPPvX4It3u3V3JipssqDfHQ2hcR0AeR8Zge+OYyKUCm5b70X72N1qXt0qgHenwN6Gc2SXZA==", "dependencies": { - "@typescript-eslint/scope-manager": "5.26.0", - "@typescript-eslint/types": "5.26.0", - "@typescript-eslint/typescript-estree": "5.26.0", + "@typescript-eslint/scope-manager": "5.27.0", + "@typescript-eslint/types": "5.27.0", + "@typescript-eslint/typescript-estree": "5.27.0", "debug": "^4.3.4" }, "engines": { @@ -4057,12 +4097,12 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.26.0.tgz", - "integrity": "sha512-gVzTJUESuTwiju/7NiTb4c5oqod8xt5GhMbExKsCTp6adU3mya6AGJ4Pl9xC7x2DX9UYFsjImC0mA62BCY22Iw==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.27.0.tgz", + "integrity": "sha512-VnykheBQ/sHd1Vt0LJ1JLrMH1GzHO+SzX6VTXuStISIsvRiurue/eRkTqSrG0CexHQgKG8shyJfR4o5VYioB9g==", "dependencies": { - "@typescript-eslint/types": "5.26.0", - "@typescript-eslint/visitor-keys": "5.26.0" + "@typescript-eslint/types": "5.27.0", + "@typescript-eslint/visitor-keys": "5.27.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -4073,11 +4113,11 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.26.0.tgz", - "integrity": "sha512-7ccbUVWGLmcRDSA1+ADkDBl5fP87EJt0fnijsMFTVHXKGduYMgienC/i3QwoVhDADUAPoytgjbZbCOMj4TY55A==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.27.0.tgz", + "integrity": "sha512-vpTvRRchaf628Hb/Xzfek+85o//zEUotr1SmexKvTfs7czXfYjXVT/a5yDbpzLBX1rhbqxjDdr1Gyo0x1Fc64g==", "dependencies": { - "@typescript-eslint/utils": "5.26.0", + "@typescript-eslint/utils": "5.27.0", "debug": "^4.3.4", "tsutils": "^3.21.0" }, @@ -4098,9 +4138,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.26.0.tgz", - "integrity": "sha512-8794JZFE1RN4XaExLWLI2oSXsVImNkl79PzTOOWt9h0UHROwJedNOD2IJyfL0NbddFllcktGIO2aOu10avQQyA==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.27.0.tgz", + "integrity": "sha512-lY6C7oGm9a/GWhmUDOs3xAVRz4ty/XKlQ2fOLr8GAIryGn0+UBOoJDWyHer3UgrHkenorwvBnphhP+zPmzmw0A==", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -4110,12 +4150,12 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.26.0.tgz", - "integrity": "sha512-EyGpw6eQDsfD6jIqmXP3rU5oHScZ51tL/cZgFbFBvWuCwrIptl+oueUZzSmLtxFuSOQ9vDcJIs+279gnJkfd1w==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.27.0.tgz", + "integrity": "sha512-QywPMFvgZ+MHSLRofLI7BDL+UczFFHyj0vF5ibeChDAJgdTV8k4xgEwF0geFhVlPc1p8r70eYewzpo6ps+9LJQ==", "dependencies": { - "@typescript-eslint/types": "5.26.0", - "@typescript-eslint/visitor-keys": "5.26.0", + "@typescript-eslint/types": "5.27.0", + "@typescript-eslint/visitor-keys": "5.27.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -4150,14 +4190,14 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.26.0.tgz", - "integrity": "sha512-PJFwcTq2Pt4AMOKfe3zQOdez6InIDOjUJJD3v3LyEtxHGVVRK3Vo7Dd923t/4M9hSH2q2CLvcTdxlLPjcIk3eg==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.27.0.tgz", + "integrity": "sha512-nZvCrkIJppym7cIbP3pOwIkAefXOmfGPnCM0LQfzNaKxJHI6VjI8NC662uoiPlaf5f6ymkTy9C3NQXev2mdXmA==", "dependencies": { "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.26.0", - "@typescript-eslint/types": "5.26.0", - "@typescript-eslint/typescript-estree": "5.26.0", + "@typescript-eslint/scope-manager": "5.27.0", + "@typescript-eslint/types": "5.27.0", + "@typescript-eslint/typescript-estree": "5.27.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0" }, @@ -4193,11 +4233,11 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.26.0.tgz", - "integrity": "sha512-wei+ffqHanYDOQgg/fS6Hcar6wAWv0CUPQ3TZzOWd2BLfgP539rb49bwua8WRAs7R6kOSLn82rfEu2ro6Llt8Q==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.27.0.tgz", + "integrity": "sha512-46cYrteA2MrIAjv9ai44OQDUoCZyHeGIc4lsjCUX2WT6r4C+kidz1bNiR4017wHOPUythYeH+Sc7/cFP97KEAA==", "dependencies": { - "@typescript-eslint/types": "5.26.0", + "@typescript-eslint/types": "5.27.0", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -5172,7 +5212,7 @@ "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/bonjour-service": { "version": "1.0.12", @@ -5335,9 +5375,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001344", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001344.tgz", - "integrity": "sha512-0ZFjnlCaXNOAYcV7i+TtdKBp0L/3XEU2MF/x6Du1lrh+SRX4IfzIVL4HNJg5pB2PmFb8rszIGyOvsZnqqRoc2g==", + "version": "1.0.30001346", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001346.tgz", + "integrity": "sha512-q6ibZUO2t88QCIPayP/euuDREq+aMAxFE5S70PkrLh0iTDj/zEhgvJRKC2+CvXY6EWc6oQwUR48lL5vCW6jiXQ==", "funding": [ { "type": "opencollective", @@ -5617,7 +5657,7 @@ "node_modules/compression/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/concat-map": { "version": "0.0.1", @@ -5697,9 +5737,9 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, "node_modules/core-js": { - "version": "3.22.7", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.22.7.tgz", - "integrity": "sha512-Jt8SReuDKVNZnZEzyEQT5eK6T2RRCXkfTq7Lo09kpm+fHjgGewSbNjV+Wt4yZMhPDdzz2x1ulI5z/w4nxpBseg==", + "version": "3.22.8", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.22.8.tgz", + "integrity": "sha512-UoGQ/cfzGYIuiq6Z7vWL1HfkE9U9IZ4Ub+0XSiJTCzvbZzgPA69oDF2f+lgJ6dFFLEdjW5O6svvoKzXX23xFkA==", "hasInstallScript": true, "funding": { "type": "opencollective", @@ -5707,9 +5747,9 @@ } }, "node_modules/core-js-compat": { - "version": "3.22.7", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.22.7.tgz", - "integrity": "sha512-uI9DAQKKiiE/mclIC5g4AjRpio27g+VMRhe6rQoz+q4Wm4L6A/fJhiLtBw+sfOpDG9wZ3O0pxIw7GbfOlBgjOA==", + "version": "3.22.8", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.22.8.tgz", + "integrity": "sha512-pQnwg4xtuvc2Bs/5zYQPaEYYSuTxsF7LBWF0SvnVhthZo/Qe+rJpcEekrdNK5DWwDJ0gv0oI9NNX5Mppdy0ctg==", "dependencies": { "browserslist": "^4.20.3", "semver": "7.0.0" @@ -5728,9 +5768,9 @@ } }, "node_modules/core-js-pure": { - "version": "3.22.7", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.22.7.tgz", - "integrity": "sha512-wTriFxiZI+C8msGeh7fJcbC/a0V8fdInN1oS2eK79DMBGs8iIJiXhtFJCiT3rBa8w6zroHWW3p8ArlujZ/Mz+w==", + "version": "3.22.8", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.22.8.tgz", + "integrity": "sha512-bOxbZIy9S5n4OVH63XaLVXZ49QKicjowDx/UELyJ68vxfCRpYsbyh/WNZNfEfAk+ekA8vSjt+gCDpvh672bc3w==", "hasInstallScript": true, "funding": { "type": "opencollective", @@ -6050,11 +6090,11 @@ } }, "node_modules/cssnano": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.9.tgz", - "integrity": "sha512-hctQHIIeDrfMjq0bQhoVmRVaSeNNOGxkvkKVOcKpJzLr09wlRrZWH4GaYudp0aszpW8wJeaO5/yBmID9n7DNCg==", + "version": "5.1.10", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.10.tgz", + "integrity": "sha512-ACpnRgDg4m6CZD/+8SgnLcGCgy6DDGdkMbOawwdvVxNietTNLe/MtWcenp6qT0PRt5wzhGl6/cjMWCdhKXC9QA==", "dependencies": { - "cssnano-preset-default": "^5.2.9", + "cssnano-preset-default": "^5.2.10", "lilconfig": "^2.0.3", "yaml": "^1.10.2" }, @@ -6070,25 +6110,25 @@ } }, "node_modules/cssnano-preset-default": { - "version": "5.2.9", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.9.tgz", - "integrity": "sha512-/4qcQcAfFEg+gnXE5NxKmYJ9JcT+8S5SDuJCLYMDN8sM/ymZ+lgLXq5+ohx/7V2brUCkgW2OaoCzOdAN0zvhGw==", + "version": "5.2.10", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.10.tgz", + "integrity": "sha512-H8TJRhTjBKVOPltp9vr9El9I+IfYsOMhmXdK0LwdvwJcxYX9oWkY7ctacWusgPWAgQq1vt/WO8v+uqpfLnM7QA==", "dependencies": { "css-declaration-sorter": "^6.2.2", "cssnano-utils": "^3.1.0", "postcss-calc": "^8.2.3", "postcss-colormin": "^5.3.0", - "postcss-convert-values": "^5.1.1", - "postcss-discard-comments": "^5.1.1", + "postcss-convert-values": "^5.1.2", + "postcss-discard-comments": "^5.1.2", "postcss-discard-duplicates": "^5.1.0", "postcss-discard-empty": "^5.1.1", "postcss-discard-overridden": "^5.1.0", "postcss-merge-longhand": "^5.1.5", - "postcss-merge-rules": "^5.1.1", + "postcss-merge-rules": "^5.1.2", "postcss-minify-font-values": "^5.1.0", "postcss-minify-gradients": "^5.1.1", "postcss-minify-params": "^5.1.3", - "postcss-minify-selectors": "^5.2.0", + "postcss-minify-selectors": "^5.2.1", "postcss-normalize-charset": "^5.1.0", "postcss-normalize-display-values": "^5.1.0", "postcss-normalize-positions": "^5.1.0", @@ -6369,7 +6409,7 @@ "node_modules/detect-port-alt/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/detective": { "version": "5.2.1", @@ -6595,9 +6635,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.141", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.141.tgz", - "integrity": "sha512-mfBcbqc0qc6RlxrsIgLG2wCqkiPAjEezHxGTu7p3dHHFOurH4EjS9rFZndX5axC8264rI1Pcbw8uQP39oZckeA==" + "version": "1.4.144", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.144.tgz", + "integrity": "sha512-R3RV3rU1xWwFJlSClVWDvARaOk6VUO/FubHLodIASDB3Mc2dzuWvNdfOgH9bwHUTqT79u92qw60NWfwUdzAqdg==" }, "node_modules/emittery": { "version": "0.8.1", @@ -6785,7 +6825,7 @@ "node_modules/escodegen/node_modules/levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", "dependencies": { "prelude-ls": "~1.1.2", "type-check": "~0.3.2" @@ -6813,7 +6853,7 @@ "node_modules/escodegen/node_modules/prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", "engines": { "node": ">= 0.8.0" } @@ -6967,7 +7007,7 @@ "node_modules/eslint-module-utils/node_modules/locate-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", "dependencies": { "p-locate": "^2.0.0", "path-exists": "^3.0.0" @@ -6990,7 +7030,7 @@ "node_modules/eslint-module-utils/node_modules/p-locate": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", "dependencies": { "p-limit": "^1.1.0" }, @@ -7001,7 +7041,7 @@ "node_modules/eslint-module-utils/node_modules/p-try": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", "engines": { "node": ">=4" } @@ -7009,7 +7049,7 @@ "node_modules/eslint-module-utils/node_modules/path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", "engines": { "node": ">=4" } @@ -7079,7 +7119,7 @@ "node_modules/eslint-plugin-import/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/eslint-plugin-jest": { "version": "25.7.0", @@ -7568,7 +7608,7 @@ "node_modules/express/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/express/node_modules/safe-buffer": { "version": "5.2.1", @@ -7761,7 +7801,7 @@ "node_modules/finalhandler/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/find-cache-dir": { "version": "3.3.2", @@ -8318,7 +8358,7 @@ "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "engines": { "node": ">=4" } @@ -8399,7 +8439,7 @@ "node_modules/hpack.js": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", "dependencies": { "inherits": "^2.0.1", "obuf": "^1.0.0", @@ -8521,7 +8561,7 @@ "node_modules/http-deceiver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=" + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==" }, "node_modules/http-errors": { "version": "2.0.0", @@ -8613,9 +8653,9 @@ } }, "node_modules/i18next": { - "version": "21.8.4", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.8.4.tgz", - "integrity": "sha512-b3LQ5n9V1juu8UItb5x1QTI4OTvNqsNs/wetwQlBvfijEqks+N5HKMKSoevf8w0/RGUrDQ7g4cvVzF8WBp9pUw==", + "version": "21.8.7", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.8.7.tgz", + "integrity": "sha512-fYbEmaCIMSDrAzXGnisZyBd0h3bcO43jVJa5fulk4ambTIOgs9tqgNsiyr1sy6Xi4iZpYMHp2ZBjU3IcCLGxPA==", "funding": [ { "type": "individual", @@ -8680,7 +8720,7 @@ "node_modules/identity-obj-proxy": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", - "integrity": "sha1-lNK9qWCERT7zb7xarsN+D3nx/BQ=", + "integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==", "dependencies": { "harmony-reflect": "^1.4.6" }, @@ -8741,7 +8781,7 @@ "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "engines": { "node": ">=0.8.19" } @@ -8749,7 +8789,7 @@ "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -8789,7 +8829,7 @@ "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, "node_modules/is-bigint": { "version": "1.0.4", @@ -8881,7 +8921,7 @@ "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "engines": { "node": ">=0.10.0" } @@ -8916,7 +8956,7 @@ "node_modules/is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=" + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" }, "node_modules/is-negative-zero": { "version": "2.0.2", @@ -8954,7 +8994,7 @@ "node_modules/is-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", "engines": { "node": ">=0.10.0" } @@ -8993,7 +9033,7 @@ "node_modules/is-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", - "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", "engines": { "node": ">=0.10.0" } @@ -9059,7 +9099,7 @@ "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" }, "node_modules/is-weakref": { "version": "1.0.2", @@ -9086,12 +9126,12 @@ "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", @@ -11273,7 +11313,7 @@ "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=" + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" }, "node_modules/json5": { "version": "2.2.1", @@ -11349,7 +11389,7 @@ "node_modules/language-tags": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", - "integrity": "sha1-0yHbxNowuovzAk4ED6XBRmH5GTo=", + "integrity": "sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==", "dependencies": { "language-subtag-registry": "~0.3.2" } @@ -11430,12 +11470,12 @@ "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=" + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -11445,12 +11485,12 @@ "node_modules/lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==" }, "node_modules/lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" }, "node_modules/loose-envify": { "version": "1.4.0", @@ -11520,7 +11560,7 @@ "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "engines": { "node": ">= 0.6" } @@ -11539,7 +11579,7 @@ "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" }, "node_modules/merge-stream": { "version": "2.0.0", @@ -11557,7 +11597,7 @@ "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "engines": { "node": ">= 0.6" } @@ -11742,7 +11782,7 @@ "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=" + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" }, "node_modules/negotiator": { "version": "0.6.3", @@ -11796,7 +11836,7 @@ "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=" + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==" }, "node_modules/node-releases": { "version": "2.0.5", @@ -11814,7 +11854,7 @@ "node_modules/normalize-range": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", "engines": { "node": ">=0.10.0" } @@ -11860,7 +11900,7 @@ "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "engines": { "node": ">=0.10.0" } @@ -12007,7 +12047,7 @@ "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dependencies": { "wrappy": "1" } @@ -12176,7 +12216,7 @@ "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "engines": { "node": ">=0.10.0" } @@ -12197,7 +12237,7 @@ "node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, "node_modules/path-type": { "version": "4.0.0", @@ -12210,7 +12250,7 @@ "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" }, "node_modules/picocolors": { "version": "1.0.0", @@ -12357,7 +12397,7 @@ "node_modules/pkg-up/node_modules/path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", "engines": { "node": ">=4" } @@ -12498,9 +12538,9 @@ } }, "node_modules/postcss-convert-values": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.1.tgz", - "integrity": "sha512-UjcYfl3wJJdcabGKk8lgetPvhi1Et7VDc3sYr9EyhNBeB00YD4vHgPBp+oMVoG/dDWCc6ASbmzPNV6jADTwh8Q==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.2.tgz", + "integrity": "sha512-c6Hzc4GAv95B7suy4udszX9Zy4ETyMCgFPUDtWjdFTKH1SE9eFY/jEpHSwTH1QPuwxHpWslhckUQWbNRM4ho5g==", "dependencies": { "browserslist": "^4.20.3", "postcss-value-parser": "^4.2.0" @@ -12570,9 +12610,9 @@ } }, "node_modules/postcss-discard-comments": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.1.tgz", - "integrity": "sha512-5JscyFmvkUxz/5/+TB3QTTT9Gi9jHkcn8dcmmuN68JQcv3aQg4y88yEHHhwFB52l/NkaJ43O0dbksGMAo49nfQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", + "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", "engines": { "node": "^10 || ^12 || >=14.0" }, @@ -12872,9 +12912,9 @@ } }, "node_modules/postcss-merge-rules": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.1.tgz", - "integrity": "sha512-8wv8q2cXjEuCcgpIB1Xx1pIy8/rhMPIQqYKNzEdyx37m6gpq83mQQdCxgIkFgliyEnKvdwJf/C61vN4tQDq4Ww==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.2.tgz", + "integrity": "sha512-zKMUlnw+zYCWoPN6yhPjtcEdlJaMUZ0WyVcxTAmw3lkkN/NDMRkOkiuctQEoWAOvH7twaxUUdvBWl0d4+hifRQ==", "dependencies": { "browserslist": "^4.16.6", "caniuse-api": "^3.0.0", @@ -12935,9 +12975,9 @@ } }, "node_modules/postcss-minify-selectors": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.0.tgz", - "integrity": "sha512-vYxvHkW+iULstA+ctVNx0VoRAR4THQQRkG77o0oa4/mBS0OzGvvzLIvHDv/nNEM0crzN2WIyFU5X7wZhaUK3RA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz", + "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==", "dependencies": { "postcss-selector-parser": "^6.0.5" }, @@ -13249,11 +13289,11 @@ } }, "node_modules/postcss-preset-env": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.6.0.tgz", - "integrity": "sha512-5cnzpSFZnQJOlBu85xn4Nnluy/WjIST/ugn+gOVcKnmFJ+GLtkfRhmJPo/TW9UDpG7oyA467kvDOO8mtcpOL4g==", + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.7.0.tgz", + "integrity": "sha512-2Q9YARQju+j2BVgAyDnW1pIWIMlaHZqbaGISPMmalznNlWcNFIZFQsJfRLXS+WHmHJDCmV7wIWpVf9JNKR4Elw==", "dependencies": { - "@csstools/postcss-cascade-layers": "^1.0.1", + "@csstools/postcss-cascade-layers": "^1.0.2", "@csstools/postcss-color-function": "^1.1.0", "@csstools/postcss-font-format-keywords": "^1.0.0", "@csstools/postcss-hwb-function": "^1.0.1", @@ -13263,16 +13303,17 @@ "@csstools/postcss-oklab-function": "^1.1.0", "@csstools/postcss-progressive-custom-properties": "^1.3.0", "@csstools/postcss-stepped-value-functions": "^1.0.0", + "@csstools/postcss-trigonometric-functions": "^1.0.0", "@csstools/postcss-unset-value": "^1.0.1", "autoprefixer": "^10.4.7", "browserslist": "^4.20.3", "css-blank-pseudo": "^3.0.3", "css-has-pseudo": "^3.0.4", "css-prefers-color-scheme": "^6.0.3", - "cssdb": "^6.6.1", + "cssdb": "^6.6.2", "postcss-attribute-case-insensitive": "^5.0.0", "postcss-clamp": "^4.1.0", - "postcss-color-functional-notation": "^4.2.2", + "postcss-color-functional-notation": "^4.2.3", "postcss-color-hex-alpha": "^8.0.3", "postcss-color-rebeccapurple": "^7.0.2", "postcss-custom-media": "^8.0.0", @@ -13290,7 +13331,7 @@ "postcss-lab-function": "^4.2.0", "postcss-logical": "^5.0.4", "postcss-media-minmax": "^5.0.0", - "postcss-nesting": "^10.1.6", + "postcss-nesting": "^10.1.7", "postcss-opacity-percentage": "^1.1.2", "postcss-overflow-shorthand": "^3.0.3", "postcss-page-break": "^3.0.4", @@ -13604,7 +13645,7 @@ "node_modules/q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", "engines": { "node": ">=0.6.0", "teleport": ">=0.2.0" @@ -14174,7 +14215,7 @@ "node_modules/regjsparser/node_modules/jsesc": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", "bin": { "jsesc": "bin/jsesc" } @@ -14359,9 +14400,9 @@ } }, "node_modules/rollup": { - "version": "2.75.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.75.1.tgz", - "integrity": "sha512-zD73rq3Fanr/spmiybMqmGEvOpryj/heLqOb+lubxiXlo8azeJ/z306T2dJYuzfWZPQBS0OT++GXG6Lbd4ToKw==", + "version": "2.75.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.75.5.tgz", + "integrity": "sha512-JzNlJZDison3o2mOxVmb44Oz7t74EfSd1SQrplQk0wSaXV7uLQXtVdHbxlcT3w+8tZ1TL4r/eLfc7nAbz38BBA==", "bin": { "rollup": "dist/bin/rollup" }, @@ -14599,7 +14640,7 @@ "node_modules/send/node_modules/debug/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/send/node_modules/ms": { "version": "2.1.3", @@ -14650,7 +14691,7 @@ "node_modules/serve-index/node_modules/http-errors": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", "dependencies": { "depd": "~1.1.2", "inherits": "2.0.3", @@ -14664,12 +14705,12 @@ "node_modules/serve-index/node_modules/inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" }, "node_modules/serve-index/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/serve-index/node_modules/setprototypeof": { "version": "1.1.0", @@ -15382,13 +15423,13 @@ } }, "node_modules/terser": { - "version": "5.13.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.13.1.tgz", - "integrity": "sha512-hn4WKOfwnwbYfe48NgrQjqNOH9jzLqRcIfbYytOXCOv46LBfWr9bDS17MQqOi+BWGD0sJK3Sj5NC/gJjiojaoA==", + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.0.tgz", + "integrity": "sha512-JC6qfIEkPBd9j1SMO3Pfn+A6w2kQV54tv+ABQLgZr7dA3k/DL/OBoYSWxzVpZev3J+bUHXfr55L8Mox7AaNo6g==", "dependencies": { + "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", "commander": "^2.20.0", - "source-map": "~0.8.0-beta.0", "source-map-support": "~0.5.20" }, "bin": { @@ -15399,14 +15440,14 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1.tgz", - "integrity": "sha512-GvlZdT6wPQKbDNW/GDQzZFg/j4vKU96yl2q6mcUkzKOgW4gwf1Z8cZToUCrz31XHlPWH8MVb1r2tFtdDtTGJ7g==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.3.tgz", + "integrity": "sha512-Fx60G5HNYknNTNQnzQ1VePRuu89ZVYWfjRAeT5rITuCY/1b08s49e5kSQwHDirKZWuoKOBRFS98EUUoZ9kLEwQ==", "dependencies": { + "@jridgewell/trace-mapping": "^0.3.7", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.0", - "source-map": "^0.6.1", "terser": "^5.7.2" }, "engines": { @@ -15431,53 +15472,11 @@ } } }, - "node_modules/terser-webpack-plugin/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, - "node_modules/terser/node_modules/source-map": { - "version": "0.8.0-beta.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", - "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", - "dependencies": { - "whatwg-url": "^7.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/terser/node_modules/tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/terser/node_modules/webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" - }, - "node_modules/terser/node_modules/whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "dependencies": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -15903,9 +15902,9 @@ } }, "node_modules/watchpack": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz", - "integrity": "sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -15931,9 +15930,9 @@ } }, "node_modules/webpack": { - "version": "5.72.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.72.1.tgz", - "integrity": "sha512-dXG5zXCLspQR4krZVR6QgajnZOjW2K/djHvdcRaDQvsjV9z9vaW6+ja5dZOYbqBBjF6kGXka/2ZyxNdc+8Jung==", + "version": "5.73.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.73.0.tgz", + "integrity": "sha512-svjudQRPPa0YiOYa2lM/Gacw0r6PvxptHj4FuEKQ2kX05ZLkjbVc5MnPs6its5j7IZljnIqSVo/OsY2X0IpHGA==", "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^0.0.51", @@ -16048,9 +16047,9 @@ } }, "node_modules/webpack-dev-server": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.9.0.tgz", - "integrity": "sha512-+Nlb39iQSOSsFv0lWUuUTim3jDQO8nhK3E68f//J2r5rIcp4lULHXz2oZ0UVdEeWXEh5lSzYUlzarZhDAeAVQw==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.9.1.tgz", + "integrity": "sha512-CTMfu2UMdR/4OOZVHRpdy84pNopOuigVIsRbGX3LVDMWNP8EUgC5mUBMErbwBlHTEX99ejZJpVqrir6EXAEajA==", "dependencies": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", @@ -16076,7 +16075,7 @@ "schema-utils": "^4.0.0", "selfsigned": "^2.0.1", "serve-index": "^1.9.1", - "sockjs": "^0.3.21", + "sockjs": "^0.3.24", "spdy": "^4.0.2", "webpack-dev-middleware": "^5.3.1", "ws": "^8.4.2" @@ -17107,9 +17106,9 @@ } }, "@babel/parser": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.3.tgz", - "integrity": "sha512-rL50YcEuHbbauAFAysNsJA4/f89fGTOBRNs9P81sniKnKAr4xULe5AecolcsKbi88xu0ByWYDj/S1AJ3FSFuSQ==" + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.4.tgz", + "integrity": "sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow==" }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.17.12", @@ -17480,24 +17479,24 @@ } }, "@babel/plugin-transform-block-scoping": { - "version": "7.17.12", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.17.12.tgz", - "integrity": "sha512-jw8XW/B1i7Lqwqj2CbrViPcZijSxfguBWZP2aN59NHgxUyO/OcO1mfdCxH13QhN5LbWhPkX+f+brKGhZTiqtZQ==", + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.18.4.tgz", + "integrity": "sha512-+Hq10ye+jlvLEogSOtq4mKvtk7qwcUQ1f0Mrueai866C82f844Yom2cttfJdMdqRLTxWpsbfbkIkOIfovyUQXw==", "requires": { "@babel/helper-plugin-utils": "^7.17.12" } }, "@babel/plugin-transform-classes": { - "version": "7.17.12", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.17.12.tgz", - "integrity": "sha512-cvO7lc7pZat6BsvH6l/EGaI8zpl8paICaoGk+7x7guvtfak/TbIf66nYmJOH13EuG0H+Xx3M+9LQDtSvZFKXKw==", + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.18.4.tgz", + "integrity": "sha512-e42NSG2mlKWgxKUAD9EJJSkZxR67+wZqzNxLSpc51T8tRU5SLFHsPmgYR5yr7sdgX4u+iHA1C5VafJ6AyImV3A==", "requires": { "@babel/helper-annotate-as-pure": "^7.16.7", - "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-environment-visitor": "^7.18.2", "@babel/helper-function-name": "^7.17.9", "@babel/helper-optimise-call-expression": "^7.16.7", "@babel/helper-plugin-utils": "^7.17.12", - "@babel/helper-replace-supers": "^7.16.7", + "@babel/helper-replace-supers": "^7.18.2", "@babel/helper-split-export-declaration": "^7.16.7", "globals": "^11.1.0" } @@ -17609,9 +17608,9 @@ } }, "@babel/plugin-transform-modules-systemjs": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.18.0.tgz", - "integrity": "sha512-vwKpxdHnlM5tIrRt/eA0bzfbi7gUBLN08vLu38np1nZevlPySRe6yvuATJB5F/WPJ+ur4OXwpVYq9+BsxqAQuQ==", + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.18.4.tgz", + "integrity": "sha512-lH2UaQaHVOAeYrUUuZ8i38o76J/FnO8vu21OE+tD1MyP9lxdZoSfz+pDbWkq46GogUrdrMz3tiz/FYGB+bVThg==", "requires": { "@babel/helper-hoist-variables": "^7.16.7", "@babel/helper-module-transforms": "^7.18.0", @@ -17788,9 +17787,9 @@ } }, "@babel/plugin-transform-typescript": { - "version": "7.18.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.18.1.tgz", - "integrity": "sha512-F+RJmL479HJmC0KeqqwEGZMg1P7kWArLGbAKfEi9yPthJyMNjF+DjxFF/halfQvq1Q9GFM4TUbYDNV8xe4Ctqg==", + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.18.4.tgz", + "integrity": "sha512-l4vHuSLUajptpHNEOUDEGsnpl9pfRLsN1XUoDQDD/YBuXTM+v37SHGS+c6n4jdcZy96QtuUuSvZYMLSSsjH8Mw==", "requires": { "@babel/helper-create-class-features-plugin": "^7.18.0", "@babel/helper-plugin-utils": "^7.17.12", @@ -17976,9 +17975,9 @@ } }, "@babel/types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.2.tgz", - "integrity": "sha512-0On6B8A4/+mFUto5WERt3EEuG1NznDirvwca1O8UwXQHVY8g3R7OzYgxXdOfMwLO08UrpUD/2+3Bclyq+/C94Q==", + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.4.tgz", + "integrity": "sha512-ThN1mBcMq5pG/Vm2IcBmPPfyPXbd8S02rS+OBIDENdufvqC7Z/jHPCv9IcP01277aKtDI8g/2XysBN4hA8niiw==", "requires": { "@babel/helper-validator-identifier": "^7.16.7", "to-fast-properties": "^2.0.0" @@ -18079,6 +18078,14 @@ "postcss-value-parser": "^4.2.0" } }, + "@csstools/postcss-trigonometric-functions": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-1.0.1.tgz", + "integrity": "sha512-G78CY/+GePc6dDCTUbwI6TTFQ5fs3N9POHhI6v0QzteGpf6ylARiJUNz9HrRKi4eVYBNXjae1W2766iUEFxHlw==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, "@csstools/postcss-unset-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.1.tgz", @@ -18768,6 +18775,27 @@ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.1.tgz", "integrity": "sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ==" }, + "@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "dependencies": { + "@jridgewell/gen-mapping": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.1.tgz", + "integrity": "sha512-GcHwniMlA2z+WFPWuY8lp3fsza0I8xPFMWL5+n8LYyP6PSvPrXf4+n8stDHZY2DM0zy9sVkRDy1jDI4XGzYVqg==", + "requires": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + } + } + }, "@jridgewell/sourcemap-codec": { "version": "1.4.13", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz", @@ -18788,9 +18816,9 @@ "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==" }, "@mui/base": { - "version": "5.0.0-alpha.82", - "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.82.tgz", - "integrity": "sha512-WUVDjCGnLXzmGxrmfW31blhucg0sRX4YddK2Falq7FlVzwdJaPgWn/xzPZmdLL0+WXon0gQVnDrq2qvggE/GMg==", + "version": "5.0.0-alpha.83", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.83.tgz", + "integrity": "sha512-/bFcjiI36R2Epf2Y3BkZOIdxrz5uMLqOU4cRai4igJ8DHTRMZDeKbOff0SdvwJNwg8r6oPUyoeOpsWkaOOX9/g==", "requires": { "@babel/runtime": "^7.17.2", "@emotion/is-prop-valid": "^1.1.2", @@ -18803,26 +18831,26 @@ } }, "@mui/icons-material": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.8.0.tgz", - "integrity": "sha512-ScwLxa0q5VYV70Jfc60V/9VD0b9SvIeZ0Jddx2Dt2pBUFFO9vKdrbt9LYiT+4p21Au5NdYIb2XSHj46CLN1v3g==", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.8.2.tgz", + "integrity": "sha512-fP6KUCCZZjc2rdbMSmkNmBHDskLkmP0uCox57cbVXvomU6BOPrCxnr5YXsSsQrZB8fchx7hfH0bkAgvMZ5KM0Q==", "requires": { "@babel/runtime": "^7.17.2" } }, "@mui/material": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.8.1.tgz", - "integrity": "sha512-Vl3BHFzOcAT5TJfvzoQUyuo/Xckn+/NSRyJ8upM4Hbz6Y1egW6P8f1RCa4FdkEfPSd5wSSYdmPfAiEh8eI4rPg==", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.8.2.tgz", + "integrity": "sha512-w/A1KG9Czf42uTyJOiRU5U1VullOz1R3xcsBvv3BtKCCWdVP+D6v/Yb8v0tJpIixMEbjeWzWGjotQBU0nd+yNA==", "requires": { "@babel/runtime": "^7.17.2", - "@mui/base": "5.0.0-alpha.82", - "@mui/system": "^5.8.1", + "@mui/base": "5.0.0-alpha.83", + "@mui/system": "^5.8.2", "@mui/types": "^7.1.3", "@mui/utils": "^5.8.0", "@types/react-transition-group": "^4.4.4", "clsx": "^1.1.1", - "csstype": "^3.0.11", + "csstype": "^3.1.0", "hoist-non-react-statics": "^3.3.2", "prop-types": "^15.8.1", "react-is": "^17.0.2", @@ -18850,9 +18878,9 @@ } }, "@mui/system": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.8.1.tgz", - "integrity": "sha512-kWJMEN62+HJb4LMRNEAZQYc++FPYsqPsU9dCL7ByLgmz/ZzRrZ8FjDi2r4j0ZeE4kaVvqBXh+RA7tLzmCKqV9w==", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.8.2.tgz", + "integrity": "sha512-N74gDNKM+MnWvKTMmCPvCVLH4f0ZzakP1bcMDaPctrHwcyxNcEmtTGNpIiVk0Iu7vtThZAFL3DjHpINPGF7+cg==", "requires": { "@babel/runtime": "^7.17.2", "@mui/private-theming": "^5.8.0", @@ -18860,7 +18888,7 @@ "@mui/types": "^7.1.3", "@mui/utils": "^5.8.0", "clsx": "^1.1.1", - "csstype": "^3.0.11", + "csstype": "^3.1.0", "prop-types": "^15.8.1" } }, @@ -19338,9 +19366,9 @@ "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" }, "@types/node": { - "version": "17.0.36", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.36.tgz", - "integrity": "sha512-V3orv+ggDsWVHP99K3JlwtH20R7J4IhI1Kksgc+64q5VxgfRkQG8Ws3MFm/FZOKDYGy9feGFlZ70/HpCNe9QaA==" + "version": "17.0.38", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.38.tgz", + "integrity": "sha512-5jY9RhV7c0Z4Jy09G+NIDTsCZ5G0L5n+Z+p+Y7t5VJHM30bgwzSjVtlcBxqAj+6L/swIlvtOSzr8rBk/aNyV2g==" }, "@types/parse-json": { "version": "4.0.0", @@ -19373,9 +19401,9 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" }, "@types/react": { - "version": "18.0.9", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.9.tgz", - "integrity": "sha512-9bjbg1hJHUm4De19L1cHiW0Jvx3geel6Qczhjd0qY5VKVE2X5+x77YxAepuCwVh4vrgZJdgEJw48zrhRIeF4Nw==", + "version": "18.0.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.10.tgz", + "integrity": "sha512-dIugadZuIPrRzvIEevIu7A1smqOAjkSMv8qOfwPt9Ve6i6JT/FQcCHyk2qIAxwsQNKZt5/oGR0T4z9h2dXRAkg==", "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -19473,13 +19501,13 @@ "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==" }, "@typescript-eslint/eslint-plugin": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.26.0.tgz", - "integrity": "sha512-oGCmo0PqnRZZndr+KwvvAUvD3kNE4AfyoGCwOZpoCncSh4MVD06JTE8XQa2u9u+NX5CsyZMBTEc2C72zx38eYA==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.27.0.tgz", + "integrity": "sha512-DDrIA7GXtmHXr1VCcx9HivA39eprYBIFxbQEHI6NyraRDxCGpxAFiYQAT/1Y0vh1C+o2vfBiy4IuPoXxtTZCAQ==", "requires": { - "@typescript-eslint/scope-manager": "5.26.0", - "@typescript-eslint/type-utils": "5.26.0", - "@typescript-eslint/utils": "5.26.0", + "@typescript-eslint/scope-manager": "5.27.0", + "@typescript-eslint/type-utils": "5.27.0", + "@typescript-eslint/utils": "5.27.0", "debug": "^4.3.4", "functional-red-black-tree": "^1.0.1", "ignore": "^5.2.0", @@ -19499,55 +19527,55 @@ } }, "@typescript-eslint/experimental-utils": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.26.0.tgz", - "integrity": "sha512-OgUGXC/teXD8PYOkn33RSwBJPVwL0I2ipm5OHr9g9cfAhVrPC2DxQiWqaq88MNO5mbr/ZWnav3EVBpuwDreS5Q==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.27.0.tgz", + "integrity": "sha512-ZOn342bYh19IYvkiorrqnzNoRAr91h3GiFSSfa4tlHV+R9GgR8SxCwAi8PKMyT8+pfwMxfQdNbwKsMurbF9hzg==", "requires": { - "@typescript-eslint/utils": "5.26.0" + "@typescript-eslint/utils": "5.27.0" } }, "@typescript-eslint/parser": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.26.0.tgz", - "integrity": "sha512-n/IzU87ttzIdnAH5vQ4BBDnLPly7rC5VnjN3m0xBG82HK6rhRxnCb3w/GyWbNDghPd+NktJqB/wl6+YkzZ5T5Q==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.27.0.tgz", + "integrity": "sha512-8oGjQF46c52l7fMiPPvX4It3u3V3JipssqDfHQ2hcR0AeR8Zge+OYyKUCm5b70X72N1qXt0qgHenwN6Gc2SXZA==", "requires": { - "@typescript-eslint/scope-manager": "5.26.0", - "@typescript-eslint/types": "5.26.0", - "@typescript-eslint/typescript-estree": "5.26.0", + "@typescript-eslint/scope-manager": "5.27.0", + "@typescript-eslint/types": "5.27.0", + "@typescript-eslint/typescript-estree": "5.27.0", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.26.0.tgz", - "integrity": "sha512-gVzTJUESuTwiju/7NiTb4c5oqod8xt5GhMbExKsCTp6adU3mya6AGJ4Pl9xC7x2DX9UYFsjImC0mA62BCY22Iw==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.27.0.tgz", + "integrity": "sha512-VnykheBQ/sHd1Vt0LJ1JLrMH1GzHO+SzX6VTXuStISIsvRiurue/eRkTqSrG0CexHQgKG8shyJfR4o5VYioB9g==", "requires": { - "@typescript-eslint/types": "5.26.0", - "@typescript-eslint/visitor-keys": "5.26.0" + "@typescript-eslint/types": "5.27.0", + "@typescript-eslint/visitor-keys": "5.27.0" } }, "@typescript-eslint/type-utils": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.26.0.tgz", - "integrity": "sha512-7ccbUVWGLmcRDSA1+ADkDBl5fP87EJt0fnijsMFTVHXKGduYMgienC/i3QwoVhDADUAPoytgjbZbCOMj4TY55A==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.27.0.tgz", + "integrity": "sha512-vpTvRRchaf628Hb/Xzfek+85o//zEUotr1SmexKvTfs7czXfYjXVT/a5yDbpzLBX1rhbqxjDdr1Gyo0x1Fc64g==", "requires": { - "@typescript-eslint/utils": "5.26.0", + "@typescript-eslint/utils": "5.27.0", "debug": "^4.3.4", "tsutils": "^3.21.0" } }, "@typescript-eslint/types": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.26.0.tgz", - "integrity": "sha512-8794JZFE1RN4XaExLWLI2oSXsVImNkl79PzTOOWt9h0UHROwJedNOD2IJyfL0NbddFllcktGIO2aOu10avQQyA==" + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.27.0.tgz", + "integrity": "sha512-lY6C7oGm9a/GWhmUDOs3xAVRz4ty/XKlQ2fOLr8GAIryGn0+UBOoJDWyHer3UgrHkenorwvBnphhP+zPmzmw0A==" }, "@typescript-eslint/typescript-estree": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.26.0.tgz", - "integrity": "sha512-EyGpw6eQDsfD6jIqmXP3rU5oHScZ51tL/cZgFbFBvWuCwrIptl+oueUZzSmLtxFuSOQ9vDcJIs+279gnJkfd1w==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.27.0.tgz", + "integrity": "sha512-QywPMFvgZ+MHSLRofLI7BDL+UczFFHyj0vF5ibeChDAJgdTV8k4xgEwF0geFhVlPc1p8r70eYewzpo6ps+9LJQ==", "requires": { - "@typescript-eslint/types": "5.26.0", - "@typescript-eslint/visitor-keys": "5.26.0", + "@typescript-eslint/types": "5.27.0", + "@typescript-eslint/visitor-keys": "5.27.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -19566,14 +19594,14 @@ } }, "@typescript-eslint/utils": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.26.0.tgz", - "integrity": "sha512-PJFwcTq2Pt4AMOKfe3zQOdez6InIDOjUJJD3v3LyEtxHGVVRK3Vo7Dd923t/4M9hSH2q2CLvcTdxlLPjcIk3eg==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.27.0.tgz", + "integrity": "sha512-nZvCrkIJppym7cIbP3pOwIkAefXOmfGPnCM0LQfzNaKxJHI6VjI8NC662uoiPlaf5f6ymkTy9C3NQXev2mdXmA==", "requires": { "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.26.0", - "@typescript-eslint/types": "5.26.0", - "@typescript-eslint/typescript-estree": "5.26.0", + "@typescript-eslint/scope-manager": "5.27.0", + "@typescript-eslint/types": "5.27.0", + "@typescript-eslint/typescript-estree": "5.27.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0" }, @@ -19595,11 +19623,11 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.26.0.tgz", - "integrity": "sha512-wei+ffqHanYDOQgg/fS6Hcar6wAWv0CUPQ3TZzOWd2BLfgP539rb49bwua8WRAs7R6kOSLn82rfEu2ro6Llt8Q==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.27.0.tgz", + "integrity": "sha512-46cYrteA2MrIAjv9ai44OQDUoCZyHeGIc4lsjCUX2WT6r4C+kidz1bNiR4017wHOPUythYeH+Sc7/cFP97KEAA==", "requires": { - "@typescript-eslint/types": "5.26.0", + "@typescript-eslint/types": "5.27.0", "eslint-visitor-keys": "^3.3.0" } }, @@ -20355,7 +20383,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" } } }, @@ -20477,9 +20505,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001344", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001344.tgz", - "integrity": "sha512-0ZFjnlCaXNOAYcV7i+TtdKBp0L/3XEU2MF/x6Du1lrh+SRX4IfzIVL4HNJg5pB2PmFb8rszIGyOvsZnqqRoc2g==" + "version": "1.0.30001346", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001346.tgz", + "integrity": "sha512-q6ibZUO2t88QCIPayP/euuDREq+aMAxFE5S70PkrLh0iTDj/zEhgvJRKC2+CvXY6EWc6oQwUR48lL5vCW6jiXQ==" }, "case-sensitive-paths-webpack-plugin": { "version": "2.4.0", @@ -20692,7 +20720,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" } } }, @@ -20750,14 +20778,14 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, "core-js": { - "version": "3.22.7", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.22.7.tgz", - "integrity": "sha512-Jt8SReuDKVNZnZEzyEQT5eK6T2RRCXkfTq7Lo09kpm+fHjgGewSbNjV+Wt4yZMhPDdzz2x1ulI5z/w4nxpBseg==" + "version": "3.22.8", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.22.8.tgz", + "integrity": "sha512-UoGQ/cfzGYIuiq6Z7vWL1HfkE9U9IZ4Ub+0XSiJTCzvbZzgPA69oDF2f+lgJ6dFFLEdjW5O6svvoKzXX23xFkA==" }, "core-js-compat": { - "version": "3.22.7", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.22.7.tgz", - "integrity": "sha512-uI9DAQKKiiE/mclIC5g4AjRpio27g+VMRhe6rQoz+q4Wm4L6A/fJhiLtBw+sfOpDG9wZ3O0pxIw7GbfOlBgjOA==", + "version": "3.22.8", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.22.8.tgz", + "integrity": "sha512-pQnwg4xtuvc2Bs/5zYQPaEYYSuTxsF7LBWF0SvnVhthZo/Qe+rJpcEekrdNK5DWwDJ0gv0oI9NNX5Mppdy0ctg==", "requires": { "browserslist": "^4.20.3", "semver": "7.0.0" @@ -20771,9 +20799,9 @@ } }, "core-js-pure": { - "version": "3.22.7", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.22.7.tgz", - "integrity": "sha512-wTriFxiZI+C8msGeh7fJcbC/a0V8fdInN1oS2eK79DMBGs8iIJiXhtFJCiT3rBa8w6zroHWW3p8ArlujZ/Mz+w==" + "version": "3.22.8", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.22.8.tgz", + "integrity": "sha512-bOxbZIy9S5n4OVH63XaLVXZ49QKicjowDx/UELyJ68vxfCRpYsbyh/WNZNfEfAk+ekA8vSjt+gCDpvh672bc3w==" }, "core-util-is": { "version": "1.0.3", @@ -20972,35 +21000,35 @@ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" }, "cssnano": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.9.tgz", - "integrity": "sha512-hctQHIIeDrfMjq0bQhoVmRVaSeNNOGxkvkKVOcKpJzLr09wlRrZWH4GaYudp0aszpW8wJeaO5/yBmID9n7DNCg==", + "version": "5.1.10", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.10.tgz", + "integrity": "sha512-ACpnRgDg4m6CZD/+8SgnLcGCgy6DDGdkMbOawwdvVxNietTNLe/MtWcenp6qT0PRt5wzhGl6/cjMWCdhKXC9QA==", "requires": { - "cssnano-preset-default": "^5.2.9", + "cssnano-preset-default": "^5.2.10", "lilconfig": "^2.0.3", "yaml": "^1.10.2" } }, "cssnano-preset-default": { - "version": "5.2.9", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.9.tgz", - "integrity": "sha512-/4qcQcAfFEg+gnXE5NxKmYJ9JcT+8S5SDuJCLYMDN8sM/ymZ+lgLXq5+ohx/7V2brUCkgW2OaoCzOdAN0zvhGw==", + "version": "5.2.10", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.10.tgz", + "integrity": "sha512-H8TJRhTjBKVOPltp9vr9El9I+IfYsOMhmXdK0LwdvwJcxYX9oWkY7ctacWusgPWAgQq1vt/WO8v+uqpfLnM7QA==", "requires": { "css-declaration-sorter": "^6.2.2", "cssnano-utils": "^3.1.0", "postcss-calc": "^8.2.3", "postcss-colormin": "^5.3.0", - "postcss-convert-values": "^5.1.1", - "postcss-discard-comments": "^5.1.1", + "postcss-convert-values": "^5.1.2", + "postcss-discard-comments": "^5.1.2", "postcss-discard-duplicates": "^5.1.0", "postcss-discard-empty": "^5.1.1", "postcss-discard-overridden": "^5.1.0", "postcss-merge-longhand": "^5.1.5", - "postcss-merge-rules": "^5.1.1", + "postcss-merge-rules": "^5.1.2", "postcss-minify-font-values": "^5.1.0", "postcss-minify-gradients": "^5.1.1", "postcss-minify-params": "^5.1.3", - "postcss-minify-selectors": "^5.2.0", + "postcss-minify-selectors": "^5.2.1", "postcss-normalize-charset": "^5.1.0", "postcss-normalize-display-values": "^5.1.0", "postcss-normalize-positions": "^5.1.0", @@ -21212,7 +21240,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" } } }, @@ -21384,9 +21412,9 @@ } }, "electron-to-chromium": { - "version": "1.4.141", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.141.tgz", - "integrity": "sha512-mfBcbqc0qc6RlxrsIgLG2wCqkiPAjEezHxGTu7p3dHHFOurH4EjS9rFZndX5axC8264rI1Pcbw8uQP39oZckeA==" + "version": "1.4.144", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.144.tgz", + "integrity": "sha512-R3RV3rU1xWwFJlSClVWDvARaOk6VUO/FubHLodIASDB3Mc2dzuWvNdfOgH9bwHUTqT79u92qw60NWfwUdzAqdg==" }, "emittery": { "version": "0.8.1", @@ -21526,7 +21554,7 @@ "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", "requires": { "prelude-ls": "~1.1.2", "type-check": "~0.3.2" @@ -21548,7 +21576,7 @@ "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==" }, "source-map": { "version": "0.6.1", @@ -21747,7 +21775,7 @@ "locate-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", "requires": { "p-locate": "^2.0.0", "path-exists": "^3.0.0" @@ -21764,7 +21792,7 @@ "p-locate": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", "requires": { "p-limit": "^1.1.0" } @@ -21772,12 +21800,12 @@ "p-try": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==" }, "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==" } } }, @@ -21829,7 +21857,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" } } }, @@ -22102,7 +22130,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "safe-buffer": { "version": "5.2.1", @@ -22253,7 +22281,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" } } }, @@ -22631,7 +22659,7 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" }, "has-property-descriptors": { "version": "1.0.0", @@ -22690,7 +22718,7 @@ "hpack.js": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", "requires": { "inherits": "^2.0.1", "obuf": "^1.0.0", @@ -22788,7 +22816,7 @@ "http-deceiver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=" + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==" }, "http-errors": { "version": "2.0.0", @@ -22854,9 +22882,9 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==" }, "i18next": { - "version": "21.8.4", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.8.4.tgz", - "integrity": "sha512-b3LQ5n9V1juu8UItb5x1QTI4OTvNqsNs/wetwQlBvfijEqks+N5HKMKSoevf8w0/RGUrDQ7g4cvVzF8WBp9pUw==", + "version": "21.8.7", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.8.7.tgz", + "integrity": "sha512-fYbEmaCIMSDrAzXGnisZyBd0h3bcO43jVJa5fulk4ambTIOgs9tqgNsiyr1sy6Xi4iZpYMHp2ZBjU3IcCLGxPA==", "requires": { "@babel/runtime": "^7.17.2" } @@ -22899,7 +22927,7 @@ "identity-obj-proxy": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", - "integrity": "sha1-lNK9qWCERT7zb7xarsN+D3nx/BQ=", + "integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==", "requires": { "harmony-reflect": "^1.4.6" } @@ -22935,12 +22963,12 @@ "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "requires": { "once": "^1.3.0", "wrappy": "1" @@ -22974,7 +23002,7 @@ "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, "is-bigint": { "version": "1.0.4", @@ -23030,7 +23058,7 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" }, "is-fullwidth-code-point": { "version": "3.0.0", @@ -23053,7 +23081,7 @@ "is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=" + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" }, "is-negative-zero": { "version": "2.0.2", @@ -23076,7 +23104,7 @@ "is-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==" }, "is-plain-obj": { "version": "3.0.0", @@ -23100,7 +23128,7 @@ "is-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", - "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=" + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==" }, "is-root": { "version": "2.1.0", @@ -23139,7 +23167,7 @@ "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" }, "is-weakref": { "version": "1.0.2", @@ -23160,12 +23188,12 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "istanbul-lib-coverage": { "version": "3.2.0", @@ -24766,7 +24794,7 @@ "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=" + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" }, "json5": { "version": "2.2.1", @@ -24819,7 +24847,7 @@ "language-tags": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", - "integrity": "sha1-0yHbxNowuovzAk4ED6XBRmH5GTo=", + "integrity": "sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==", "requires": { "language-subtag-registry": "~0.3.2" } @@ -24879,12 +24907,12 @@ "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=" + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==" }, "lodash.merge": { "version": "4.6.2", @@ -24894,12 +24922,12 @@ "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==" }, "lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" }, "loose-envify": { "version": "1.4.0", @@ -24957,7 +24985,7 @@ "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" }, "memfs": { "version": "3.4.4", @@ -24970,7 +24998,7 @@ "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" }, "merge-stream": { "version": "2.0.0", @@ -24985,7 +25013,7 @@ "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" }, "micromatch": { "version": "4.0.5", @@ -25112,7 +25140,7 @@ "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=" + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" }, "negotiator": { "version": "0.6.3", @@ -25149,7 +25177,7 @@ "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=" + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==" }, "node-releases": { "version": "2.0.5", @@ -25164,7 +25192,7 @@ "normalize-range": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=" + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==" }, "normalize-url": { "version": "6.1.0", @@ -25195,7 +25223,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, "object-hash": { "version": "3.0.0", @@ -25294,7 +25322,7 @@ "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "requires": { "wrappy": "1" } @@ -25415,7 +25443,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" }, "path-key": { "version": "3.1.1", @@ -25430,7 +25458,7 @@ "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, "path-type": { "version": "4.0.0", @@ -25440,7 +25468,7 @@ "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" }, "picocolors": { "version": "1.0.0", @@ -25544,7 +25572,7 @@ "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==" } } }, @@ -25625,9 +25653,9 @@ } }, "postcss-convert-values": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.1.tgz", - "integrity": "sha512-UjcYfl3wJJdcabGKk8lgetPvhi1Et7VDc3sYr9EyhNBeB00YD4vHgPBp+oMVoG/dDWCc6ASbmzPNV6jADTwh8Q==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.2.tgz", + "integrity": "sha512-c6Hzc4GAv95B7suy4udszX9Zy4ETyMCgFPUDtWjdFTKH1SE9eFY/jEpHSwTH1QPuwxHpWslhckUQWbNRM4ho5g==", "requires": { "browserslist": "^4.20.3", "postcss-value-parser": "^4.2.0" @@ -25664,9 +25692,9 @@ } }, "postcss-discard-comments": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.1.tgz", - "integrity": "sha512-5JscyFmvkUxz/5/+TB3QTTT9Gi9jHkcn8dcmmuN68JQcv3aQg4y88yEHHhwFB52l/NkaJ43O0dbksGMAo49nfQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", + "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", "requires": {} }, "postcss-discard-duplicates": { @@ -25832,9 +25860,9 @@ } }, "postcss-merge-rules": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.1.tgz", - "integrity": "sha512-8wv8q2cXjEuCcgpIB1Xx1pIy8/rhMPIQqYKNzEdyx37m6gpq83mQQdCxgIkFgliyEnKvdwJf/C61vN4tQDq4Ww==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.2.tgz", + "integrity": "sha512-zKMUlnw+zYCWoPN6yhPjtcEdlJaMUZ0WyVcxTAmw3lkkN/NDMRkOkiuctQEoWAOvH7twaxUUdvBWl0d4+hifRQ==", "requires": { "browserslist": "^4.16.6", "caniuse-api": "^3.0.0", @@ -25871,9 +25899,9 @@ } }, "postcss-minify-selectors": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.0.tgz", - "integrity": "sha512-vYxvHkW+iULstA+ctVNx0VoRAR4THQQRkG77o0oa4/mBS0OzGvvzLIvHDv/nNEM0crzN2WIyFU5X7wZhaUK3RA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz", + "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==", "requires": { "postcss-selector-parser": "^6.0.5" } @@ -26044,11 +26072,11 @@ } }, "postcss-preset-env": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.6.0.tgz", - "integrity": "sha512-5cnzpSFZnQJOlBu85xn4Nnluy/WjIST/ugn+gOVcKnmFJ+GLtkfRhmJPo/TW9UDpG7oyA467kvDOO8mtcpOL4g==", + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.7.0.tgz", + "integrity": "sha512-2Q9YARQju+j2BVgAyDnW1pIWIMlaHZqbaGISPMmalznNlWcNFIZFQsJfRLXS+WHmHJDCmV7wIWpVf9JNKR4Elw==", "requires": { - "@csstools/postcss-cascade-layers": "^1.0.1", + "@csstools/postcss-cascade-layers": "^1.0.2", "@csstools/postcss-color-function": "^1.1.0", "@csstools/postcss-font-format-keywords": "^1.0.0", "@csstools/postcss-hwb-function": "^1.0.1", @@ -26058,16 +26086,17 @@ "@csstools/postcss-oklab-function": "^1.1.0", "@csstools/postcss-progressive-custom-properties": "^1.3.0", "@csstools/postcss-stepped-value-functions": "^1.0.0", + "@csstools/postcss-trigonometric-functions": "^1.0.0", "@csstools/postcss-unset-value": "^1.0.1", "autoprefixer": "^10.4.7", "browserslist": "^4.20.3", "css-blank-pseudo": "^3.0.3", "css-has-pseudo": "^3.0.4", "css-prefers-color-scheme": "^6.0.3", - "cssdb": "^6.6.1", + "cssdb": "^6.6.2", "postcss-attribute-case-insensitive": "^5.0.0", "postcss-clamp": "^4.1.0", - "postcss-color-functional-notation": "^4.2.2", + "postcss-color-functional-notation": "^4.2.3", "postcss-color-hex-alpha": "^8.0.3", "postcss-color-rebeccapurple": "^7.0.2", "postcss-custom-media": "^8.0.0", @@ -26085,7 +26114,7 @@ "postcss-lab-function": "^4.2.0", "postcss-logical": "^5.0.4", "postcss-media-minmax": "^5.0.0", - "postcss-nesting": "^10.1.6", + "postcss-nesting": "^10.1.7", "postcss-opacity-percentage": "^1.1.2", "postcss-overflow-shorthand": "^3.0.3", "postcss-page-break": "^3.0.4", @@ -26310,7 +26339,7 @@ "q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==" }, "qs": { "version": "6.10.3", @@ -26725,7 +26754,7 @@ "jsesc": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==" } } }, @@ -26848,9 +26877,9 @@ } }, "rollup": { - "version": "2.75.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.75.1.tgz", - "integrity": "sha512-zD73rq3Fanr/spmiybMqmGEvOpryj/heLqOb+lubxiXlo8azeJ/z306T2dJYuzfWZPQBS0OT++GXG6Lbd4ToKw==", + "version": "2.75.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.75.5.tgz", + "integrity": "sha512-JzNlJZDison3o2mOxVmb44Oz7t74EfSd1SQrplQk0wSaXV7uLQXtVdHbxlcT3w+8tZ1TL4r/eLfc7nAbz38BBA==", "requires": { "fsevents": "~2.3.2" } @@ -27011,7 +27040,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" } } }, @@ -27060,7 +27089,7 @@ "http-errors": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", "requires": { "depd": "~1.1.2", "inherits": "2.0.3", @@ -27071,12 +27100,12 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "setprototypeof": { "version": "1.1.0", @@ -27629,13 +27658,13 @@ } }, "terser": { - "version": "5.13.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.13.1.tgz", - "integrity": "sha512-hn4WKOfwnwbYfe48NgrQjqNOH9jzLqRcIfbYytOXCOv46LBfWr9bDS17MQqOi+BWGD0sJK3Sj5NC/gJjiojaoA==", + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.0.tgz", + "integrity": "sha512-JC6qfIEkPBd9j1SMO3Pfn+A6w2kQV54tv+ABQLgZr7dA3k/DL/OBoYSWxzVpZev3J+bUHXfr55L8Mox7AaNo6g==", "requires": { + "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", "commander": "^2.20.0", - "source-map": "~0.8.0-beta.0", "source-map-support": "~0.5.20" }, "dependencies": { @@ -27643,57 +27672,19 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "source-map": { - "version": "0.8.0-beta.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", - "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", - "requires": { - "whatwg-url": "^7.0.0" - } - }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "requires": { - "punycode": "^2.1.0" - } - }, - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" - }, - "whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } } } }, "terser-webpack-plugin": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1.tgz", - "integrity": "sha512-GvlZdT6wPQKbDNW/GDQzZFg/j4vKU96yl2q6mcUkzKOgW4gwf1Z8cZToUCrz31XHlPWH8MVb1r2tFtdDtTGJ7g==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.3.tgz", + "integrity": "sha512-Fx60G5HNYknNTNQnzQ1VePRuu89ZVYWfjRAeT5rITuCY/1b08s49e5kSQwHDirKZWuoKOBRFS98EUUoZ9kLEwQ==", "requires": { + "@jridgewell/trace-mapping": "^0.3.7", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.0", - "source-map": "^0.6.1", "terser": "^5.7.2" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } } }, "test-exclude": { @@ -28022,9 +28013,9 @@ } }, "watchpack": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz", - "integrity": "sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", "requires": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -28044,9 +28035,9 @@ "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==" }, "webpack": { - "version": "5.72.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.72.1.tgz", - "integrity": "sha512-dXG5zXCLspQR4krZVR6QgajnZOjW2K/djHvdcRaDQvsjV9z9vaW6+ja5dZOYbqBBjF6kGXka/2ZyxNdc+8Jung==", + "version": "5.73.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.73.0.tgz", + "integrity": "sha512-svjudQRPPa0YiOYa2lM/Gacw0r6PvxptHj4FuEKQ2kX05ZLkjbVc5MnPs6its5j7IZljnIqSVo/OsY2X0IpHGA==", "requires": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^0.0.51", @@ -28140,9 +28131,9 @@ } }, "webpack-dev-server": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.9.0.tgz", - "integrity": "sha512-+Nlb39iQSOSsFv0lWUuUTim3jDQO8nhK3E68f//J2r5rIcp4lULHXz2oZ0UVdEeWXEh5lSzYUlzarZhDAeAVQw==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.9.1.tgz", + "integrity": "sha512-CTMfu2UMdR/4OOZVHRpdy84pNopOuigVIsRbGX3LVDMWNP8EUgC5mUBMErbwBlHTEX99ejZJpVqrir6EXAEajA==", "requires": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", @@ -28168,7 +28159,7 @@ "schema-utils": "^4.0.0", "selfsigned": "^2.0.1", "serve-index": "^1.9.1", - "sockjs": "^0.3.21", + "sockjs": "^0.3.24", "spdy": "^4.0.2", "webpack-dev-middleware": "^5.3.1", "ws": "^8.4.2" diff --git a/web/public/home.html b/web/public/home.html index 3e3a95d8..43007ca3 100644 --- a/web/public/home.html +++ b/web/public/home.html @@ -110,7 +110,7 @@

- +

Here's a video showing the app in action: diff --git a/web/public/static/langs/nl.json b/web/public/static/langs/nl.json index 56fdf389..11954852 100644 --- a/web/public/static/langs/nl.json +++ b/web/public/static/langs/nl.json @@ -1,7 +1,191 @@ { "action_bar_settings": "Instellingen", - "action_bar_send_test_notification": "Stuur testmelding", - "action_bar_clear_notifications": "Alle meldingen wissen", + "action_bar_send_test_notification": "Stuur test notificatie", + "action_bar_clear_notifications": "Wis alle notificaties", "message_bar_type_message": "Typ hier een bericht", - "action_bar_unsubscribe": "Afmelden" + "action_bar_unsubscribe": "Afmelden", + "message_bar_error_publishing": "Fout bij publiceren notificatie", + "nav_topics_title": "Geabonneerde onderwerpen", + "nav_button_settings": "Instellingen", + "alert_not_supported_description": "Notificaties worden niet ondersteund in je browser.", + "notifications_none_for_any_title": "Je hebt nog geen notificaties ontvangen.", + "publish_dialog_tags_label": "Tags", + "publish_dialog_chip_attach_file_label": "Lokaal bestand bijvoegen", + "prefs_users_dialog_title_edit": "Gebruiker bewerken", + "error_boundary_title": "Oh nee, ntfy is vastgelopen", + "error_boundary_description": "Dit hoort natuurlijk niet te gebeuren. Onze excuses.
Wanneer het mogelijk is, meld deze fout op GitHub, of laat het ons weten via Discord of Matrix.", + "error_boundary_button_copy_stack_trace": "Stack trace kopiëren", + "error_boundary_stack_trace": "Stacktrace", + "error_boundary_gathering_info": "Meer informatie verzamelen …", + "prefs_users_delete_button": "Gebruiker verwijderen", + "prefs_notifications_delete_after_one_week": "Na één week", + "prefs_notifications_delete_after_one_month": "Na één maand", + "prefs_users_dialog_title_add": "Gebruiker toevoegen", + "prefs_users_dialog_password_label": "Wachtwoord", + "error_boundary_unsupported_indexeddb_description": "De ntfy web applicatie heeft IndexedDB nodig om correct te kunnen functioneren, helaas ondersteund jouw browser IndexedDB niet in privé / incognito modus.

Dit is jammer maar het is ook onlogisch om de ntfy web applicatie in privé / incognito modus te gebruiken want alle gegevens worden bewaard in de browser zijn lokale opslag. Je kan hier meer over lezen in deze GitHub issue, of praat met ons op Discord of Matrix.", + "action_bar_show_menu": "Toon menu", + "action_bar_logo_alt": "ntfy logo", + "action_bar_toggle_mute": "Notificaties dempen/opheffen", + "action_bar_toggle_action_menu": "Actie menu openen/sluiten", + "message_bar_show_dialog": "Toon publicatie venster", + "message_bar_publish": "Bericht publiceren", + "nav_button_all_notifications": "Alle notificaties", + "nav_button_documentation": "Documentatie", + "nav_button_publish_message": "Notificatie publiceren", + "nav_button_subscribe": "Onderwerp abonneren", + "nav_button_muted": "Notificaties gedempt", + "nav_button_connecting": "verbinden", + "alert_grant_title": "Notificaties zijn uitgeschakeld", + "alert_grant_description": "Geef je browser toestemming om meldingen weer te geven.", + "alert_grant_button": "Nu toestaan", + "alert_not_supported_title": "Notificaties zijn niet ondersteund", + "notifications_list": "Notificaties lijst", + "notifications_list_item": "Notificatie", + "notifications_mark_read": "Markeer als gelezen", + "notifications_delete": "Verwijder", + "notifications_copied_to_clipboard": "Gekopieerd naar klembord", + "notifications_tags": "Tags", + "notifications_priority_x": "Prioriteit {{priority}}", + "notifications_new_indicator": "Nieuwe notificatie", + "notifications_attachment_image": "Afbeelding bijlage", + "notifications_attachment_copy_url_title": "Kopieer URL van bijlage naar klembord", + "notifications_attachment_copy_url_button": "URL kopiëren", + "notifications_attachment_open_title": "Ga naar {{url}}", + "notifications_attachment_open_button": "Bijlage openen", + "notifications_attachment_link_expires": "link vervalt op {{date}}", + "notifications_attachment_link_expired": "download link is verlopen", + "notifications_attachment_file_image": "afbeeldingsbestand", + "notifications_attachment_file_video": "videobestand", + "notifications_attachment_file_audio": "audiobestand", + "notifications_attachment_file_app": "Android app bestand", + "notifications_attachment_file_document": "overig document", + "notifications_click_copy_url_title": "URL naar klembord kopiëren", + "notifications_click_copy_url_button": "Link kopiëren", + "notifications_click_open_button": "Link openen", + "notifications_none_for_topic_description": "Om notificaties naar dit onderwerp te sturen, doe een PUT of POST naar het onderwerp URL.", + "notifications_none_for_any_description": "Om notificaties naar dit onderwerp te sturen, doe een PUT of POST naar het onderwerp URL. Hier is een voorbeeld met één van je onderwerpen.", + "notifications_no_subscriptions_title": "Het lijkt erop dat je nog op geen onderwerpen geabonneerd bent.", + "notifications_no_subscriptions_description": "Klik op de \"{{linktext}}\" link om een onderwerp te maken of erop te abonneren. Daarna kan je berichten sturen via PUT of POST and ontvang je hier notificaties.", + "notifications_example": "Voorbeeld", + "notifications_more_details": "Voor meer informatie, bezoek de website of documentatie.", + "notifications_loading": "Notificaties laden …", + "publish_dialog_title_topic": "Publiceren naar {{topic}}", + "publish_dialog_title_no_topic": "Notificatie publiceren", + "publish_dialog_progress_uploading": "Uploaden …", + "notifications_actions_open_url_title": "Ga naar {{url}}", + "notifications_actions_not_supported": "Deze actie is niet ondersteund in de web applicatie", + "notifications_actions_http_request_title": "Stuur HTTP {{method}} naar {{url}}", + "notifications_none_for_topic_title": "Je hebt nog geen notificaties ontvangen voor dit onderwerp.", + "publish_dialog_priority_low": "Lage prioriteit", + "publish_dialog_progress_uploading_detail": "Uploaden {{loaded}}/{{total}} ({{percent}}%) …", + "publish_dialog_message_published": "Notificatie gepubliceerd", + "publish_dialog_attachment_limits_file_and_quota_reached": "overschrijd {{fileSizeLimit}} bestandslimiet en quotum, {{remainingBytes}} resterend", + "publish_dialog_attachment_limits_file_reached": "overschrijd {{fileSizeLimit}} bestandslimiet", + "publish_dialog_priority_default": "Standaard prioriteit", + "publish_dialog_attachment_limits_quota_reached": "overschrijd quotum, {{remainingBytes}} resterend", + "publish_dialog_emoji_picker_show": "Kies een emoji", + "publish_dialog_priority_high": "Hoge prioriteit", + "publish_dialog_priority_max": "Maximale prioriteit", + "publish_dialog_priority_min": "Minimale prioriteit", + "publish_dialog_base_url_label": "Service URL", + "publish_dialog_base_url_placeholder": "Service URL, bijvoorbeeld: https://voorbeeld.com", + "publish_dialog_topic_label": "Onderwerp", + "publish_dialog_topic_placeholder": "Onderwerp, bijv. phil_alerts", + "publish_dialog_topic_reset": "Onderwerp resetten", + "publish_dialog_title_label": "Titel", + "publish_dialog_title_placeholder": "Notificatie titel , bijv. Schijfruimte alarm", + "publish_dialog_message_label": "Bericht", + "publish_dialog_message_placeholder": "Typ hier een bericht", + "publish_dialog_tags_placeholder": "Komma gescheiden lijst met tags, bijv. waarschuwing, srv1-backup", + "publish_dialog_priority_label": "Prioriteit", + "publish_dialog_click_label": "Klik URL", + "publish_dialog_click_reset": "Verwijder klik URL", + "publish_dialog_email_label": "Email", + "publish_dialog_email_placeholder": "Adres om de notificatie naar door te sturen, bijv. phil@voorbeeld.com", + "publish_dialog_email_reset": "Email doorsturen verwijderen", + "publish_dialog_attach_label": "URL van bijlage", + "publish_dialog_click_placeholder": "URL die geopend zal worden wanneer op de notificatie geklikt wordt", + "publish_dialog_attach_placeholder": "Bestand bijvoegen via URL, bijv. https://f-droid.org/F-Droid.apk", + "publish_dialog_attach_reset": "Bijlage URL verwijderen", + "publish_dialog_filename_label": "Bestandsnaam", + "publish_dialog_filename_placeholder": "Bestandsnaam van bijlage", + "publish_dialog_delay_label": "Uitstellen", + "publish_dialog_delay_placeholder": "Bezorging uitstellen, bijv. {{unixTimestamp}}, {{relativeTime}}, of \"{{naturalLanguage}}\" (alleen Engels)", + "publish_dialog_delay_reset": "Verwijder uitgestelde bezorging", + "publish_dialog_other_features": "Andere functionaliteiten:", + "publish_dialog_chip_click_label": "Klik URL", + "publish_dialog_chip_email_label": "Doorsturen naar email", + "publish_dialog_chip_attach_url_label": "Bestand bijvoegen via URL", + "publish_dialog_chip_delay_label": "Uitgestelde bezorging", + "publish_dialog_chip_topic_label": "Onderwerp veranderen", + "publish_dialog_details_examples_description": "Voor meer voorbeelden en gedetailleerde beschrijvingen van alle functionaliteiten, bekijk de documentatie.", + "publish_dialog_button_cancel_sending": "Versturen annuleren", + "publish_dialog_button_cancel": "Annuleer", + "publish_dialog_button_send": "Verstuur", + "publish_dialog_checkbox_publish_another": "Nog een bericht versturen", + "publish_dialog_attached_file_title": "Bijgevoegd bestand:", + "publish_dialog_attached_file_filename_placeholder": "Bijlage bestandsnaam", + "publish_dialog_attached_file_remove": "Verwijder bijgevoegd bestand", + "publish_dialog_drop_file_here": "Bestand hier slepen", + "emoji_picker_search_placeholder": "Emoji zoeken", + "emoji_picker_search_clear": "Zoeken leegmaken", + "subscribe_dialog_subscribe_topic_placeholder": "Onderwerp naam, bijv. phils_waarschuwingen", + "subscribe_dialog_subscribe_use_another_label": "Gebruik een andere server", + "subscribe_dialog_subscribe_base_url_label": "Service URL", + "subscribe_dialog_subscribe_button_cancel": "Annuleren", + "subscribe_dialog_subscribe_button_subscribe": "Abonneren", + "subscribe_dialog_login_title": "Aanmelding vereist", + "subscribe_dialog_login_description": "Dit onderwerp is beveiligd met een wachtwoord. Geef een gebruikersnaam en wachtwoord op om te abonneren.", + "subscribe_dialog_login_username_label": "Gebruikersnaam, bijv. phil", + "subscribe_dialog_subscribe_title": "Onderwerp abonneren", + "subscribe_dialog_subscribe_description": "Onderwerpen zijn mogelijk niet beschermd met een wachtwoord, kies daarom een moeilijk te raden naam. Na abonneren kun je notificaties via PUT/POST sturen.", + "subscribe_dialog_login_password_label": "Wachtwoord", + "subscribe_dialog_login_button_back": "Terug", + "subscribe_dialog_login_button_login": "Aanmelden", + "subscribe_dialog_error_user_not_authorized": "Gebruiker {{username}} heeft geen toegang", + "subscribe_dialog_error_user_anonymous": "anoniem", + "prefs_notifications_title": "Notificaties", + "prefs_notifications_sound_title": "Meldingsgeluid", + "prefs_notifications_sound_description_none": "Notificaties zullen geen geluid geven", + "prefs_notifications_sound_play": "Geselecteerd geluid afspelen", + "prefs_notifications_sound_description_some": "Inkomende notificaties zullen het {{sound}} afspelen", + "prefs_notifications_sound_no_sound": "Geen geluid", + "prefs_notifications_min_priority_title": "Minimale prioriteit", + "prefs_notifications_min_priority_description_any": "Toon alle notificaties, ongeacht prioriteit", + "prefs_notifications_min_priority_description_x_or_higher": "Toon notificaties als prioriteit is {{number}} ({{name}}) of hoger", + "prefs_notifications_min_priority_description_max": "Toon notificaties als prioriteit is 5 (maximaal)", + "prefs_notifications_min_priority_any": "Elke prioriteit", + "prefs_notifications_min_priority_low_and_higher": "Lage prioriteit en hoger", + "prefs_notifications_min_priority_default_and_higher": "Standaard prioriteit en hoger", + "prefs_notifications_min_priority_high_and_higher": "Hoge prioriteit en hoger", + "prefs_notifications_min_priority_max_only": "Alleen maximale prioriteit", + "prefs_notifications_delete_after_title": "Notificaties verwijderen", + "prefs_notifications_delete_after_never": "Nooit", + "prefs_notifications_delete_after_three_hours": "Na drie uur", + "prefs_notifications_delete_after_one_day": "Na één dag", + "prefs_notifications_delete_after_never_description": "Notificaties worden nooit automatisch verwijderd", + "prefs_notifications_delete_after_three_hours_description": "Notificaties worden na drie uur automatisch verwijderd", + "prefs_notifications_delete_after_one_day_description": "Notificaties worden na één dag automatisch verwijderd", + "prefs_notifications_delete_after_one_week_description": "Notificaties worden na één week automatisch verwijderd", + "prefs_notifications_delete_after_one_month_description": "Notificaties worden na één maand automatisch verwijderd", + "prefs_users_title": "Gebruikers beheren", + "prefs_users_description": "Gebruikers voor beveiligde onderwerpen kunnen hier toegevoegd of verwijderd worden. Let op: gebruikersnaam en wachtwoord worden opgeslagen in lokale browser opslag.", + "prefs_users_table": "Gebruikerstabel", + "prefs_users_add_button": "Gebruiker toevoegen", + "prefs_users_edit_button": "Gebruiker bewerken", + "prefs_users_table_user_header": "Gebruiker", + "prefs_users_table_base_url_header": "Service URL", + "prefs_users_dialog_base_url_label": "Service URL, bijv. https://ntfy.sh", + "prefs_users_dialog_username_label": "Gebruikersnaam, bijv. phil", + "prefs_users_dialog_button_cancel": "Annuleren", + "prefs_users_dialog_button_add": "Toevoegen", + "prefs_users_dialog_button_save": "Bewaren", + "prefs_appearance_title": "Weergave", + "prefs_appearance_language_title": "Taal", + "priority_min": "min", + "priority_low": "laag", + "priority_default": "standaard", + "priority_high": "hoog", + "priority_max": "max", + "error_boundary_unsupported_indexeddb_title": "Privé / incognito browservensters worden niet ondersteund" } diff --git a/web/public/static/langs/zh_Hans.json b/web/public/static/langs/zh_Hans.json new file mode 100644 index 00000000..e084bcf2 --- /dev/null +++ b/web/public/static/langs/zh_Hans.json @@ -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_action_menu": "开启或关闭操作菜单", + "message_bar_type_message": "在此处输入消息", + "message_bar_show_dialog": "显示发布对话框", + "message_bar_publish": "发布消息", + "nav_topics_title": "订阅主题", + "nav_button_all_notifications": "全部通知", + "nav_button_documentation": "文档", + "nav_button_publish_message": "发布通知", + "nav_button_subscribe": "订阅主题", + "nav_button_connecting": "正在连接", + "alert_grant_title": "已禁用通知", + "alert_grant_description": "授予浏览器显示桌面通知的权限。", + "alert_grant_button": "现在授予", + "alert_not_supported_title": "不支持通知", + "alert_not_supported_description": "您的浏览器不支持通知。", + "notifications_list": "通知列表", + "notifications_list_item": "通知", + "notifications_mark_read": "标记为已读", + "notifications_copied_to_clipboard": "复制到剪贴板", + "notifications_tags": "标记", + "notifications_priority_x": "优先级 {{priority}}", + "notifications_new_indicator": "新通知", + "notifications_attachment_open_button": "打开附件", + "notifications_attachment_link_expires": "链接过期 {{date}}", + "notifications_attachment_link_expired": "下载链接已过期", + "notifications_attachment_file_image": "图片文件", + "notifications_attachment_image": "附件图片", + "notifications_attachment_file_video": "视频文件", + "notifications_attachment_file_audio": "音频文件", + "notifications_attachment_file_app": "安卓应用文件", + "notifications_attachment_file_document": "其他文件", + "notifications_click_copy_url_title": "复制链接地址到剪贴板", + "notifications_click_copy_url_button": "复制链接", + "notifications_click_open_button": "打开链接", + "action_bar_toggle_mute": "暂停或恢复通知", + "nav_button_muted": "已暂停通知", + "notifications_actions_not_supported": "网页应用程序不支持操作", + "notifications_none_for_topic_title": "您尚未收到有关此主题的任何通知。", + "notifications_none_for_any_title": "您尚未收到任何通知。", + "notifications_none_for_any_description": "要向此主题发送通知,只需使用 PUT 或 POST 到主题链接即可。以下是使用您的主题的示例。", + "notifications_no_subscriptions_title": "看起来你还没有任何订阅。", + "notifications_example": "示例", + "notifications_more_details": "有关更多信息,请查看网站文档。", + "notifications_loading": "正在加载通知……", + "publish_dialog_title_topic": "发布到 {{topic}}", + "publish_dialog_title_no_topic": "发布通知", + "publish_dialog_progress_uploading": "正在上传……", + "publish_dialog_progress_uploading_detail": "正在上传 {{loaded}}/{{total}} ({{percent}}%) ……", + "publish_dialog_message_published": "已发布通知", + "publish_dialog_attachment_limits_file_and_quota_reached": "超过 {{fileSizeLimit}} 文件限制和配额,剩余 {{remainingBytes}}", + "publish_dialog_emoji_picker_show": "选择表情符号", + "publish_dialog_priority_min": "最低优先级", + "publish_dialog_priority_low": "低优先级", + "publish_dialog_priority_default": "默认优先级", + "publish_dialog_priority_high": "高优先级", + "publish_dialog_priority_max": "最高优先级", + "publish_dialog_topic_label": "主题名称", + "publish_dialog_topic_placeholder": "主题名称,例如 phil_alerts", + "publish_dialog_topic_reset": "重置主题", + "publish_dialog_title_label": "主题", + "publish_dialog_message_label": "消息", + "publish_dialog_message_placeholder": "在此输入消息", + "publish_dialog_tags_label": "标记", + "publish_dialog_priority_label": "优先级", + "publish_dialog_base_url_label": "服务链接地址", + "publish_dialog_base_url_placeholder": "服务链接地址,例如 https://example.com", + "publish_dialog_click_label": "点击链接地址", + "publish_dialog_click_placeholder": "点击通知时打开链接地址", + "publish_dialog_email_placeholder": "将通知转发到的地址,例如 phil@example.com", + "publish_dialog_email_reset": "移除电子邮件转发", + "publish_dialog_filename_label": "文件名", + "publish_dialog_filename_placeholder": "附件文件名", + "publish_dialog_delay_label": "延期", + "publish_dialog_other_features": "其它功能:", + "publish_dialog_attach_placeholder": "使用链接地址附加文件,例如 https://f-droid.org/F-Droid.apk", + "publish_dialog_delay_reset": "删除延迟交付", + "publish_dialog_attach_reset": "移除附件链接地址", + "publish_dialog_chip_click_label": "点击链接地址", + "publish_dialog_chip_email_label": "转发邮件", + "publish_dialog_chip_attach_file_label": "本地文件附件", + "publish_dialog_chip_topic_label": "变更主题", + "publish_dialog_button_cancel_sending": "取消发送", + "publish_dialog_checkbox_publish_another": "发布另一个", + "publish_dialog_attached_file_title": "附件文件:", + "publish_dialog_attached_file_filename_placeholder": "附件文件名", + "publish_dialog_attached_file_remove": "删除附件文件", + "publish_dialog_drop_file_here": "将文件拖拽至此", + "emoji_picker_search_placeholder": "查找表情符号", + "emoji_picker_search_clear": "清除搜索", + "subscribe_dialog_subscribe_title": "订阅主题", + "publish_dialog_chip_delay_label": "延迟交付", + "publish_dialog_chip_attach_url_label": "链接附件地址", + "subscribe_dialog_subscribe_use_another_label": "使用其他服务器", + "subscribe_dialog_subscribe_button_subscribe": "订阅", + "subscribe_dialog_login_title": "请登录", + "subscribe_dialog_login_description": "本主题受密码保护,请输入用户名和密码进行订阅。", + "subscribe_dialog_login_username_label": "用户名,例如 phil", + "subscribe_dialog_login_password_label": "密码", + "subscribe_dialog_login_button_back": "返回", + "subscribe_dialog_login_button_login": "登录", + "subscribe_dialog_error_user_not_authorized": "未授权 {{username}} 用户", + "subscribe_dialog_error_user_anonymous": "匿名", + "prefs_notifications_title": "通知", + "prefs_notifications_sound_title": "通知提示音", + "prefs_notifications_sound_description_none": "收到通知时不播放任何声音", + "prefs_notifications_sound_description_some": "收到通知时播放 {{sound}} 声音", + "prefs_notifications_sound_no_sound": "静音", + "prefs_notifications_sound_play": "播放选中声音", + "prefs_notifications_min_priority_title": "最低优先级", + "prefs_notifications_min_priority_description_x_or_higher": "仅显示优先级为{{number}}({{name}})或以上的通知", + "prefs_notifications_min_priority_description_max": "仅显示最高优先级的通知", + "prefs_notifications_min_priority_any": "任意优先级", + "prefs_notifications_min_priority_low_and_higher": "低优先级和更高优先级", + "prefs_notifications_min_priority_default_and_higher": "默认优先级或更高优先级", + "prefs_notifications_min_priority_high_and_higher": "高优先级或更高优先级", + "prefs_notifications_min_priority_max_only": "仅最高优先级", + "prefs_notifications_delete_after_never": "从不", + "prefs_notifications_delete_after_one_month": "一月后", + "prefs_notifications_delete_after_one_week": "一周后", + "prefs_notifications_delete_after_never_description": "永不自动删除通知", + "prefs_notifications_delete_after_three_hours_description": "三小时后自动删除通知", + "prefs_notifications_delete_after_one_day_description": "一天后自动删除通知", + "prefs_notifications_delete_after_one_week_description": "一周后自动删除通知", + "prefs_notifications_delete_after_one_month_description": "一月后后自动删除通知", + "prefs_users_title": "管理用户", + "prefs_users_description": "在此处添加/删除受保护主题的用户。请注意,用户名和密码存储在浏览器的本地存储中。", + "prefs_users_add_button": "添加用户", + "prefs_users_dialog_title_add": "添加用户", + "prefs_users_dialog_title_edit": "编辑用户", + "prefs_users_dialog_username_label": "用户名,例如 phil", + "prefs_users_dialog_password_label": "密码", + "prefs_users_dialog_button_cancel": "取消", + "prefs_users_dialog_button_save": "保存", + "prefs_appearance_title": "外观", + "prefs_appearance_language_title": "语言", + "priority_min": "最低", + "priority_low": "低", + "priority_default": "默认", + "priority_high": "高", + "priority_max": "最高", + "error_boundary_title": "天啊,ntfy 崩溃了", + "prefs_users_table_base_url_header": "服务链接地址", + "prefs_users_dialog_base_url_label": "服务链接地址,例如 https://ntfy.sh", + "error_boundary_button_copy_stack_trace": "复制堆栈跟踪", + "error_boundary_stack_trace": "堆栈跟踪", + "error_boundary_gathering_info": "收集更多信息……", + "error_boundary_unsupported_indexeddb_title": "不支持隐私浏览", + "error_boundary_unsupported_indexeddb_description": "Ntfy Web应用程序需要IndexedDB才能运行,并且您的浏览器在私隐私浏览模式下不支持IndexedDB。

虽然这很不幸,但在隐私浏览模式下使用ntfy Web应用程序也没有多大意义,因为所有东西都存储在浏览器存储中。您可以在本GitHub问题中阅读有关它的更多信息,或者在DiscordMatrix上与我们交谈。", + "message_bar_error_publishing": "发布通知时出错", + "nav_button_settings": "设置", + "notifications_delete": "删除", + "notifications_attachment_copy_url_title": "将附件中链接地址复制到剪贴板", + "notifications_attachment_copy_url_button": "复制链接地址", + "notifications_attachment_open_title": "转到 {{url}}", + "notifications_actions_http_request_title": "发送 HTTP {{method}} 到 {{url}}", + "notifications_actions_open_url_title": "转到 {{url}}", + "notifications_none_for_topic_description": "要向此主题发送通知,只需使用 PUT 或 POST 到主题链接即可。", + "subscribe_dialog_subscribe_topic_placeholder": "主题名,例如 phil_alerts", + "notifications_no_subscriptions_description": "单击 \"{{linktext}}\" 链接以创建或订阅主题。之后,您可以使用 PUT 或 POST 发送消息,您将在这里收到通知。", + "publish_dialog_attachment_limits_file_reached": "超过 {{fileSizeLimit}} 文件限制", + "publish_dialog_title_placeholder": "主题标题,例如 磁盘空间告警", + "publish_dialog_email_label": "电子邮件", + "publish_dialog_button_send": "发送", + "publish_dialog_attachment_limits_quota_reached": "超过配额,剩余 {{remainingBytes}}", + "publish_dialog_attach_label": "附件链接地址", + "publish_dialog_click_reset": "移除点击连接地址", + "publish_dialog_button_cancel": "取消", + "subscribe_dialog_subscribe_button_cancel": "取消", + "subscribe_dialog_subscribe_base_url_label": "服务地址地址", + "prefs_notifications_min_priority_description_any": "显示所有通知,无论优先级如何", + "prefs_notifications_delete_after_title": "删除通知", + "prefs_notifications_delete_after_three_hours": "三小时后", + "prefs_users_delete_button": "删除用户", + "prefs_users_table_user_header": "用户", + "prefs_users_dialog_button_add": "添加", + "prefs_notifications_delete_after_one_day": "一天后", + "error_boundary_description": "这显然不应该发生。对此非常抱歉。
如果您有时间,请在GitHub上报告,或通过DiscordMatrix告诉我们。", + "prefs_users_table": "用户表", + "prefs_users_edit_button": "编辑用户", + "publish_dialog_tags_placeholder": "英文逗号分隔标记列表,例如 warning, srv1-backup", + "publish_dialog_details_examples_description": "有关所有发送功能的示例和详细说明,请参阅文档。", + "subscribe_dialog_subscribe_description": "主题可能不受密码保护,因此请选择一个不容易猜测的名字。订阅后,您可以使用 PUT/POST 通知。", + "publish_dialog_delay_placeholder": "延迟交付,例如{{unixTimestamp}}、{{relativeTime}}或“{{naturalLanguage}}”(仅限英语)" +} diff --git a/web/src/components/Preferences.js b/web/src/components/Preferences.js index b93702fa..468cc4c3 100644 --- a/web/src/components/Preferences.js +++ b/web/src/components/Preferences.js @@ -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"; @@ -452,12 +452,14 @@ const Language = () => { Bahasa Indonesia Български Čeština + 中文 Deutsch Español Français Italiano Magyar 日本語 + Nederlands Norsk bokmål Português (Brasil) Русский