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 <img src="https://hosted.weblate.org/widgets/ntfy/-/multi-blue.svg" alt="Translation status" /> </a> -## 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: `<number>(smh)`, e.g. 30s, 20m or 1h. The format for a *size* is: `<number>(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 <a href="/example.html">live example</a> or just look the source of this page. +web application. Check out the <a href="/example.html">live example</a>. ## 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 iOS app v1.1 (UNRELEASED) +## ntfy Android app v1.14.0 (UNRELEASED) + +**Additional translations:** + +* Italian (thanks to [@Genio2003](https://hosted.weblate.org/user/Genio2003/)) + +## ntfy server v1.26.0 (UNRELEASED) + +**Features:** + +* Windows CLI is now available via [Scoop](https://scoop.sh) ([ScoopInstaller#3594](https://github.com/ScoopInstaller/Main/pull/3594), [#311](https://github.com/binwiederhier/ntfy/pull/311), [#269](https://github.com/binwiederhier/ntfy/issues/269), thanks to [@kzshantonu](https://github.com/kzshantonu)) + +--> + +## 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" <div dir="ltr">what's up<br clear="all"><div><br></div></div> --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" <div dir="ltr"><br></div> --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 "<cannot serialize>" 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 "<cannot serialize>" + } + 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 @@ <p> <a href="https://play.google.com/store/apps/details?id=io.heckel.ntfy"><img src="static/img/badge-googleplay.png"></a> <a href="https://f-droid.org/en/packages/io.heckel.ntfy/"><img src="static/img/badge-fdroid.png"></a> - <a href="https://github.com/binwiederhier/ntfy/issues/4"><img src="static/img/badge-appstore.png"></a> + <a href="https://apps.apple.com/us/app/ntfy/id1625396347"><img src="static/img/badge-appstore.png"></a> </p> <p> 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.<br/>Wanneer het mogelijk is, <githubLink>meld deze fout op GitHub</githubLink>, of laat het ons weten via <discordLink>Discord</discordLink> of <matrixLink>Matrix</matrixLink>.", + "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.<br/><br/>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 <githubLink>in deze GitHub issue</githubLink>, of praat met ons op <discordLink>Discord</discordLink> of <matrixLink>Matrix</matrixLink>.", + "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 <websiteLink>website</websiteLink> of <docsLink>documentatie</docsLink>.", + "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 <docsLink>documentatie</docsLink>.", + "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": "有关更多信息,请查看<websiteLink>网站</websiteLink>或<docsLink>文档</docsLink>。", + "notifications_loading": "正在加载通知……", + "publish_dialog_title_topic": "发布到 {{topic}}", + "publish_dialog_title_no_topic": "发布通知", + "publish_dialog_progress_uploading": "正在上传……", + "publish_dialog_progress_uploading_detail": "正在上传 {{loaded}}/{{total}} ({{percent}}%) ……", + "publish_dialog_message_published": "已发布通知", + "publish_dialog_attachment_limits_file_and_quota_reached": "超过 {{fileSizeLimit}} 文件限制和配额,剩余 {{remainingBytes}}", + "publish_dialog_emoji_picker_show": "选择表情符号", + "publish_dialog_priority_min": "最低优先级", + "publish_dialog_priority_low": "低优先级", + "publish_dialog_priority_default": "默认优先级", + "publish_dialog_priority_high": "高优先级", + "publish_dialog_priority_max": "最高优先级", + "publish_dialog_topic_label": "主题名称", + "publish_dialog_topic_placeholder": "主题名称,例如 phil_alerts", + "publish_dialog_topic_reset": "重置主题", + "publish_dialog_title_label": "主题", + "publish_dialog_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。<br/><br/>虽然这很不幸,但在隐私浏览模式下使用ntfy Web应用程序也没有多大意义,因为所有东西都存储在浏览器存储中。您可以在<githubLink>本GitHub问题</githubLink>中阅读有关它的更多信息,或者在<discordLink>Discord</discordLink>或<matrixLink>Matrix</matrixLink>上与我们交谈。", + "message_bar_error_publishing": "发布通知时出错", + "nav_button_settings": "设置", + "notifications_delete": "删除", + "notifications_attachment_copy_url_title": "将附件中链接地址复制到剪贴板", + "notifications_attachment_copy_url_button": "复制链接地址", + "notifications_attachment_open_title": "转到 {{url}}", + "notifications_actions_http_request_title": "发送 HTTP {{method}} 到 {{url}}", + "notifications_actions_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": "这显然不应该发生。对此非常抱歉。<br/>如果您有时间,请<githubLink>在GitHub</githubLink>上报告,或通过<discordLink>Discord</discordLink>或<matrixLink>Matrix</matrixLink>告诉我们。", + "prefs_users_table": "用户表", + "prefs_users_edit_button": "编辑用户", + "publish_dialog_tags_placeholder": "英文逗号分隔标记列表,例如 warning, srv1-backup", + "publish_dialog_details_examples_description": "有关所有发送功能的示例和详细说明,请参阅<docsLink>文档</docsLink>。", + "subscribe_dialog_subscribe_description": "主题可能不受密码保护,因此请选择一个不容易猜测的名字。订阅后,您可以使用 PUT/POST 通知。", + "publish_dialog_delay_placeholder": "延迟交付,例如{{unixTimestamp}}、{{relativeTime}}或“{{naturalLanguage}}”(仅限英语)" +} 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 = () => { <MenuItem value="id">Bahasa Indonesia</MenuItem> <MenuItem value="bg">Български</MenuItem> <MenuItem value="cs">Čeština</MenuItem> + <MenuItem value="zh_Hans">中文</MenuItem> <MenuItem value="de">Deutsch</MenuItem> <MenuItem value="es">Español</MenuItem> <MenuItem value="fr">Français</MenuItem> <MenuItem value="it">Italiano</MenuItem> <MenuItem value="hu">Magyar</MenuItem> <MenuItem value="ja">日本語</MenuItem> + <MenuItem value="nl">Nederlands</MenuItem> <MenuItem value="nb_NO">Norsk bokmål</MenuItem> <MenuItem value="pt_BR">Português (Brasil)</MenuItem> <MenuItem value="ru">Русский</MenuItem>