diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..3bf2a126 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +dist +*/node_modules +Dockerfile* diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..23002306 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,11 @@ +# https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view + +# Run prettier (https://github.com/binwiederhier/ntfy/pull/746) +6f6a2d1f693070bf72e89d86748080e4825c9164 +c87549e71a10bc789eac8036078228f06e515a8e +ca5d736a7169eb6b4b0d849e061d5bf9565dcc53 +2e27f58963feb9e4d1c573d4745d07770777fa7d + +# Run eslint (https://github.com/binwiederhier/ntfy/pull/748) +f558b4dbe9bb5b9e0e87fada1215de2558353173 +8319f1cf26113167fb29fe12edaff5db74caf35f diff --git a/.github/images/logo.png b/.github/images/logo.png new file mode 100644 index 00000000..351db4d7 Binary files /dev/null and b/.github/images/logo.png differ diff --git a/web/public/static/img/screenshot-curl.png b/.github/images/screenshot-curl.png similarity index 100% rename from web/public/static/img/screenshot-curl.png rename to .github/images/screenshot-curl.png diff --git a/web/public/static/img/screenshot-phone-detail.jpg b/.github/images/screenshot-phone-detail.jpg similarity index 100% rename from web/public/static/img/screenshot-phone-detail.jpg rename to .github/images/screenshot-phone-detail.jpg diff --git a/web/public/static/img/screenshot-phone-main.jpg b/.github/images/screenshot-phone-main.jpg similarity index 100% rename from web/public/static/img/screenshot-phone-main.jpg rename to .github/images/screenshot-phone-main.jpg diff --git a/web/public/static/img/screenshot-phone-notification.jpg b/.github/images/screenshot-phone-notification.jpg similarity index 100% rename from web/public/static/img/screenshot-phone-notification.jpg rename to .github/images/screenshot-phone-notification.jpg diff --git a/web/public/static/img/screenshot-web-detail.png b/.github/images/screenshot-web-detail.png similarity index 100% rename from web/public/static/img/screenshot-web-detail.png rename to .github/images/screenshot-web-detail.png diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index e5e989b2..cb310934 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -4,30 +4,21 @@ jobs: build: runs-on: ubuntu-latest steps: + - + name: Checkout code + uses: actions/checkout@v3 - name: Install Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: - go-version: '1.19.x' + go-version: '1.21.x' - name: Install node - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: - node-version: '18' - - - 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- + node-version: '20' + cache: 'npm' + cache-dependency-path: './web/package-lock.json' - name: Install dependencies run: make build-deps-ubuntu diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 2ba9b9c6..6991dea6 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -30,7 +30,7 @@ jobs: run: | cd build/ntfy-docs.github.io git config user.name "GitHub Actions Bot" - git config user.email "<>" + git config user.email "" git add docs/ git commit -m "Updated docs" git push origin main diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7341addd..c630a2d3 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -7,30 +7,21 @@ jobs: release: runs-on: ubuntu-latest steps: + - + name: Checkout code + uses: actions/checkout@v3 - name: Install Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: - go-version: '1.19.x' + go-version: '1.21.x' - name: Install node - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: - node-version: '18' - - - 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- + node-version: '20' + cache: 'npm' + cache-dependency-path: './web/package-lock.json' - name: Docker login uses: docker/login-action@v2 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 372a87ce..53eb1d67 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -4,30 +4,21 @@ jobs: test: runs-on: ubuntu-latest steps: + - + name: Checkout code + uses: actions/checkout@v3 - name: Install Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: - go-version: '1.19.x' + go-version: '1.21.x' - name: Install node - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: - node-version: '18' - - - 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- + node-version: '20' + cache: 'npm' + cache-dependency-path: './web/package-lock.json' - name: Install dependencies run: make build-deps-ubuntu diff --git a/.gitignore b/.gitignore index b0c2d330..7cbb52ac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ dist/ +dev-dist/ build/ .idea/ .vscode/ @@ -12,3 +13,5 @@ secrets/ node_modules/ .DS_Store __pycache__ +web/dev-dist/ +venv/ diff --git a/.goreleaser.yml b/.goreleaser.yml index 9ba8bb49..062cce1f 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -97,7 +97,7 @@ nfpms: - dst: /var/lib/ntfy type: dir - dst: /usr/share/ntfy/logo.png - src: web/public/static/img/ntfy.png + src: web/public/static/images/ntfy.png scripts: preinstall: "scripts/preinst.sh" postinstall: "scripts/postinst.sh" @@ -119,8 +119,6 @@ archives: - server/ntfy.service - client/client.yml - client/ntfy-client.service - replacements: - amd64: x86_64 - id: ntfy_windows builds: @@ -131,8 +129,6 @@ archives: - LICENSE - README.md - client/client.yml - replacements: - amd64: x86_64 - id: ntfy_darwin builds: @@ -142,8 +138,6 @@ archives: - LICENSE - README.md - client/client.yml - replacements: - darwin: macOS universal_binaries: - id: ntfy_darwin_all @@ -170,14 +164,14 @@ dockers: - image_templates: - &arm64v8_image "binwiederhier/ntfy:{{ .Tag }}-arm64v8" use: buildx - dockerfile: Dockerfile + dockerfile: Dockerfile-arm goarch: arm64 build_flag_templates: - "--platform=linux/arm64/v8" - image_templates: - &armv7_image "binwiederhier/ntfy:{{ .Tag }}-armv7" use: buildx - dockerfile: Dockerfile + dockerfile: Dockerfile-arm goarch: arm goarm: 7 build_flag_templates: @@ -185,7 +179,7 @@ dockers: - image_templates: - &armv6_image "binwiederhier/ntfy:{{ .Tag }}-armv6" use: buildx - dockerfile: Dockerfile + dockerfile: Dockerfile-arm goarch: arm goarm: 6 build_flag_templates: diff --git a/Dockerfile b/Dockerfile index 7c2052ef..45dad05d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,7 @@ LABEL org.opencontainers.image.licenses="Apache-2.0, GPL-2.0" LABEL org.opencontainers.image.title="ntfy" LABEL org.opencontainers.image.description="Send push notifications to your phone or desktop using PUT/POST" +RUN apk add --no-cache tzdata COPY ntfy /usr/bin EXPOSE 80/tcp diff --git a/Dockerfile-arm b/Dockerfile-arm new file mode 100644 index 00000000..755092fd --- /dev/null +++ b/Dockerfile-arm @@ -0,0 +1,18 @@ +FROM alpine + +LABEL org.opencontainers.image.authors="philipp.heckel@gmail.com" +LABEL org.opencontainers.image.url="https://ntfy.sh/" +LABEL org.opencontainers.image.documentation="https://docs.ntfy.sh/" +LABEL org.opencontainers.image.source="https://github.com/binwiederhier/ntfy" +LABEL org.opencontainers.image.vendor="Philipp C. Heckel" +LABEL org.opencontainers.image.licenses="Apache-2.0, GPL-2.0" +LABEL org.opencontainers.image.title="ntfy" +LABEL org.opencontainers.image.description="Send push notifications to your phone or desktop using PUT/POST" + +# Alpine does not support adding "tzdata" on ARM anymore, see +# https://github.com/binwiederhier/ntfy/issues/894 + +COPY ntfy /usr/bin + +EXPOSE 80/tcp +ENTRYPOINT ["ntfy"] diff --git a/Dockerfile-build b/Dockerfile-build new file mode 100644 index 00000000..a0608b12 --- /dev/null +++ b/Dockerfile-build @@ -0,0 +1,59 @@ +FROM golang:1.20-bullseye as builder + +ARG VERSION=dev +ARG COMMIT=unknown +ARG NODE_MAJOR=18 + +RUN apt-get update && apt-get install -y \ + build-essential ca-certificates curl gnupg \ + && mkdir -p /etc/apt/keyrings \ + && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ + && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" >> /etc/apt/sources.list.d/nodesource.list \ + && apt-get update \ + && apt-get install -y \ + python3-pip \ + python3-venv \ + nodejs \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +ADD Makefile . + +# docs +ADD ./requirements.txt . +RUN make docs-deps +ADD ./mkdocs.yml . +ADD ./docs ./docs +RUN make docs-build + +# web +ADD ./web/package.json ./web/package-lock.json ./web/ +RUN make web-deps +ADD ./web ./web +RUN make web-build + +# cli & server +ADD go.mod go.sum main.go ./ +ADD ./client ./client +ADD ./cmd ./cmd +ADD ./log ./log +ADD ./server ./server +ADD ./user ./user +ADD ./util ./util +RUN make VERSION=$VERSION COMMIT=$COMMIT cli-linux-server + +FROM alpine + +LABEL org.opencontainers.image.authors="philipp.heckel@gmail.com" +LABEL org.opencontainers.image.url="https://ntfy.sh/" +LABEL org.opencontainers.image.documentation="https://docs.ntfy.sh/" +LABEL org.opencontainers.image.source="https://github.com/binwiederhier/ntfy" +LABEL org.opencontainers.image.vendor="Philipp C. Heckel" +LABEL org.opencontainers.image.licenses="Apache-2.0, GPL-2.0" +LABEL org.opencontainers.image.title="ntfy" +LABEL org.opencontainers.image.description="Send push notifications to your phone or desktop using PUT/POST" + +COPY --from=builder /app/dist/ntfy_linux_server/ntfy /usr/bin/ntfy + +EXPOSE 80/tcp +ENTRYPOINT ["ntfy"] diff --git a/Makefile b/Makefile index 76f46a84..a29fb4f5 100644 --- a/Makefile +++ b/Makefile @@ -31,10 +31,16 @@ help: @echo " make cli-darwin-server - Build client & server (no GoReleaser, current arch, macOS)" @echo " make cli-client - Build client only (no GoReleaser, current arch, Linux/macOS/Windows)" @echo + @echo "Build dev Docker:" + @echo " make docker-dev - Build client & server for current architecture using Docker only" + @echo @echo "Build web app:" @echo " make web - Build the web app" @echo " make web-deps - Install web app dependencies (npm install the universe)" @echo " make web-build - Actually build the web app" + @echo " make web-lint - Run eslint on the web app" + @echo " make web-fmt - Run prettier on the web app" + @echo " make web-fmt-check - Run prettier on the web app, but don't change anything" @echo @echo "Build documentation:" @echo " make docs - Build the documentation" @@ -80,40 +86,45 @@ build: web docs cli update: web-deps-update cli-deps-update docs-deps-update docker pull alpine +docker-dev: + docker build \ + --file ./Dockerfile-build \ + --tag binwiederhier/ntfy:$(VERSION) \ + --tag binwiederhier/ntfy:dev \ + --build-arg VERSION=$(VERSION) \ + --build-arg COMMIT=$(COMMIT) \ + ./ + + # Ubuntu-specific build-deps-ubuntu: - sudo apt update - sudo apt install -y \ + sudo apt-get update + sudo apt-get install -y \ curl \ gcc-aarch64-linux-gnu \ gcc-arm-linux-gnueabi \ + python3 \ + python3-venv \ jq - which pip3 || sudo apt install -y python3-pip + which pip3 || sudo apt-get install -y python3-pip + # Documentation docs: docs-deps docs-build -docs-build: .PHONY - @if ! /bin/echo -e "import sys\nif sys.version_info < (3,8):\n exit(1)" | python3; then \ - if which python3.8; then \ - echo "python3.8 $(shell which mkdocs) build"; \ - python3.8 $(shell which mkdocs) build; \ - else \ - echo "ERROR: Python version too low. mkdocs-material needs >= 3.8"; \ - exit 1; \ - fi; \ - else \ - echo "mkdocs build"; \ - mkdocs build; \ - fi +docs-venv: .PHONY + python3 -m venv ./venv -docs-deps: .PHONY - pip3 install -r requirements.txt +docs-build: docs-venv + (. venv/bin/activate && mkdocs build) + +docs-deps: docs-venv + (. venv/bin/activate && pip3 install -r requirements.txt) docs-deps-update: .PHONY - pip3 install -r requirements.txt --upgrade + (. venv/bin/activate && pip3 install -r requirements.txt --upgrade) # Web app @@ -127,8 +138,7 @@ web-build: && rm -rf ../server/site \ && mv build ../server/site \ && rm \ - ../server/site/config.js \ - ../server/site/asset-manifest.json + ../server/site/config.js web-deps: cd web && npm install @@ -137,6 +147,14 @@ web-deps: web-deps-update: cd web && npm update +web-fmt: + cd web && npm run format + +web-fmt-check: + cd web && npm run format:check + +web-lint: + cd web && npm run lint # Main server/client build @@ -226,7 +244,7 @@ cli-build-results: # Test/check targets -check: test fmt-check vet lint staticcheck +check: test web-fmt-check fmt-check vet web-lint lint staticcheck test: .PHONY go test $(shell go list ./... | grep -vE 'ntfy/(test|examples|tools)') @@ -253,7 +271,7 @@ coverage-upload: # Lint/formatting targets -fmt: +fmt: web-fmt gofmt -s -w . fmt-check: diff --git a/README.md b/README.md index 7cf41fe7..90d9285f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![ntfy](web/public/static/img/ntfy.png) +![ntfy](web/public/static/images/ntfy.png) # ntfy.sh | Send push notifications to your phone or desktop via PUT/POST [![Release](https://img.shields.io/github/release/binwiederhier/ntfy.svg?color=success&style=flat-square)](https://github.com/binwiederhier/ntfy/releases/latest) @@ -9,7 +9,7 @@ [![Discord](https://img.shields.io/discord/874398661709295626?label=Discord)](https://discord.gg/cT7ECsZj9w) [![Matrix](https://img.shields.io/matrix/ntfy:matrix.org?label=Matrix)](https://matrix.to/#/#ntfy:matrix.org) [![Matrix space](https://img.shields.io/matrix/ntfy-space:matrix.org?label=Matrix+space)](https://matrix.to/#/#ntfy-space:matrix.org) -[![Reddit](https://img.shields.io/reddit/subreddit-subscribers/ntfy?color=%23317f6f&label=-%20r%2Fntfy&style=social)](https://www.reddit.com/r/ntfy/) +[![Lemmy](https://img.shields.io/badge/Lemmy-discuss-green)](https://discuss.ntfy.sh/c/ntfy) [![Healthcheck](https://healthchecks.io/badge/68b65976-b3b0-4102-aec9-980921/kcoEgrLY.svg)](https://ntfy.statuspage.io/) [![Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod)](https://gitpod.io/#https://github.com/binwiederhier/ntfy) @@ -18,18 +18,24 @@ notification service. With ntfy, you can **send notifications to your phone or d **without having to sign up or pay any fees**. If you'd like to run your own instance of the service, you can easily do so since ntfy is open source. -You can access the free version of ntfy at **[ntfy.sh](https://ntfy.sh)**. There is also an [open source Android app](https://github.com/binwiederhier/ntfy-android) +You can access the free version of ntfy at **[ntfy.sh](https://ntfy.sh)**. There is also an [open-source Android app](https://github.com/binwiederhier/ntfy-android) available on [Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy) or [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/), as well as an [open source iOS app](https://github.com/binwiederhier/ntfy-ios) available on the [App Store](https://apps.apple.com/us/app/ntfy/id1625396347).

- - - - - + + + + +

+## [ntfy Pro](https://ntfy.sh/app) 💸 🎉 +I now offer paid plans for [ntfy.sh](https://ntfy.sh/) if you don't want to self-host, or you want to support the development of +ntfy (→ [Purchase via web app](https://ntfy.sh/app)). You can **buy a plan for as low as $5/month**. +You can also donate via [GitHub Sponsors](https://github.com/sponsors/binwiederhier), and [Liberapay](https://liberapay.com/ntfy). +I would be very humbled by your sponsorship. ❤️ + ## **[Documentation](https://ntfy.sh/docs/)** [Getting started](https://ntfy.sh/docs/) | @@ -38,23 +44,22 @@ as well as an [open source iOS app](https://github.com/binwiederhier/ntfy-ios) a [Install / Self-hosting](https://ntfy.sh/docs/install/) | [Building](https://ntfy.sh/docs/develop/) -## Chat / forum +## Chat/forum There are a few ways to get in touch with me and/or the rest of the community. Feel free to use any of these methods. Whatever works best for you: * [Discord server](https://discord.gg/cT7ECsZj9w) - direct chat with the community * [Matrix room #ntfy](https://matrix.to/#/#ntfy:matrix.org) (+ [Matrix space](https://matrix.to/#/#ntfy-space:matrix.org)) - same chat, bridged from Discord -* [Reddit r/ntfy](https://www.reddit.com/r/ntfy/) - asynchronous forum (_new as of October 2022_) +* [Lemmy discussion board](https://discuss.ntfy.sh/c/ntfy) - asynchronous forum (_new as of June 2023_) * [GitHub issues](https://github.com/binwiederhier/ntfy/issues) - questions, features, bugs -* [Email](https://heckel.io/about) - reach me directly (_I usually prefer the other methods_) -## Announcements / beta testers +## Announcements/beta testers For announcements of new releases and cutting-edge beta versions, please subscribe to the [ntfy.sh/announcements](https://ntfy.sh/announcements) topic. If you'd like to test the iOS app, join [TestFlight](https://testflight.apple.com/join/P1fFnAm9). For Android betas, join Discord/Matrix (I'll eventually make a testing channel in Google Play). ## Contributing -I welcome any and all contributions. Just create a PR or an issue. For larger features/ideas, please reach out +I welcome any contributions. Just create a PR or an issue. For larger features/ideas, please reach out on Discord/Matrix first to see if I'd accept them. To contribute code, check out the [build instructions](https://ntfy.sh/docs/develop/) for the server and the Android app. Or, if you'd like to help translate 🇩🇪 🇺🇸 🇧🇬, you can start immediately in [Hosted Weblate](https://hosted.weblate.org/projects/ntfy/). @@ -130,8 +135,38 @@ account costs. Even small donations are very much appreciated. A big fat **Thank + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -I'd also like to thank JetBrains for providing their awesome [IntelliJ IDEA](https://www.jetbrains.com/idea/) to me for free, +I'd also like to thank JetBrains for their awesome [IntelliJ IDEA](https://www.jetbrains.com/idea/), and [DigitalOcean](https://m.do.co/c/442b929528db) (*referral link*) for supporting the project: @@ -147,7 +182,7 @@ _Please be sure to read the complete [Code of Conduct](CODE_OF_CONDUCT.md)._ Made with ❤️ by [Philipp C. Heckel](https://heckel.io). The project is dual licensed under the [Apache License 2.0](LICENSE) and the [GPLv2 License](LICENSE.GPLv2). -Third party libraries and resources: +Third-party libraries and resources: * [github.com/urfave/cli](https://github.com/urfave/cli) (MIT) is used to drive the CLI * [Mixkit sounds](https://mixkit.co/free-sound-effects/notification/) (Mixkit Free License) are used as notification sounds * [Sounds from notificationsounds.com](https://notificationsounds.com) (Creative Commons Attribution) are used as notification sounds @@ -167,3 +202,4 @@ Third party libraries and resources: * [Regex for auto-linking](https://github.com/bryanwoods/autolink-js) (MIT) is used to highlight links (the library is not used) * [Statically linking go-sqlite3](https://www.arp242.net/static-go.html) * [Linked tabs in mkdocs](https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/#linked-tabs) +* [webpush-go](https://github.com/SherClockHolmes/webpush-go) (MIT) is used to send web push notifications diff --git a/client/client.go b/client/client.go index b744fa11..93cf7da5 100644 --- a/client/client.go +++ b/client/client.go @@ -11,23 +11,25 @@ import ( "heckel.io/ntfy/util" "io" "net/http" + "regexp" "strings" "sync" "time" ) -// Event type constants const ( - MessageEvent = "message" - KeepaliveEvent = "keepalive" - OpenEvent = "open" - PollRequestEvent = "poll_request" + // MessageEvent identifies a message event + MessageEvent = "message" ) const ( maxResponseBytes = 4096 ) +var ( + topicRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`) // Same as in server/server.go +) + // Client is the ntfy client that can be used to publish and subscribe to ntfy topics type Client struct { Messages chan *Message @@ -96,8 +98,14 @@ func (c *Client) Publish(topic, message string, options ...PublishOption) (*Mess // To pass title, priority and tags, check out WithTitle, WithPriority, WithTagsList, WithDelay, WithNoCache, // WithNoFirebase, and the generic WithHeader. func (c *Client) PublishReader(topic string, body io.Reader, options ...PublishOption) (*Message, error) { - topicURL := c.expandTopicURL(topic) - req, _ := http.NewRequest("POST", topicURL, body) + topicURL, err := c.expandTopicURL(topic) + if err != nil { + return nil, err + } + req, err := http.NewRequest("POST", topicURL, body) + if err != nil { + return nil, err + } for _, option := range options { if err := option(req); err != nil { return nil, err @@ -133,11 +141,14 @@ func (c *Client) PublishReader(topic string, body io.Reader, options ...PublishO // By default, all messages will be returned, but you can change this behavior using a SubscribeOption. // See WithSince, WithSinceAll, WithSinceUnixTime, WithScheduled, and the generic WithQueryParam. func (c *Client) Poll(topic string, options ...SubscribeOption) ([]*Message, error) { + topicURL, err := c.expandTopicURL(topic) + if err != nil { + return nil, err + } ctx := context.Background() messages := make([]*Message, 0) 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() { @@ -166,15 +177,18 @@ func (c *Client) Poll(topic string, options ...SubscribeOption) ([]*Message, err // Example: // // c := client.New(client.NewConfig()) -// subscriptionID := c.Subscribe("mytopic") +// subscriptionID, _ := c.Subscribe("mytopic") // for m := range c.Messages { // fmt.Printf("New message: %s", m.Message) // } -func (c *Client) Subscribe(topic string, options ...SubscribeOption) string { +func (c *Client) Subscribe(topic string, options ...SubscribeOption) (string, error) { + topicURL, err := c.expandTopicURL(topic) + if err != nil { + return "", err + } c.mu.Lock() 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{ @@ -183,7 +197,7 @@ func (c *Client) Subscribe(topic string, options ...SubscribeOption) string { cancel: cancel, } go handleSubscribeConnLoop(ctx, c.Messages, topicURL, subscriptionID, options...) - return subscriptionID + return subscriptionID, nil } // Unsubscribe unsubscribes from a topic that has been previously subscribed to using the unique @@ -199,31 +213,16 @@ func (c *Client) Unsubscribe(subscriptionID string) { sub.cancel() } -// UnsubscribeAll unsubscribes from a topic that has been previously subscribed with Subscribe. -// If there are multiple subscriptions matching the topic, all of them are unsubscribed from. -// -// A topic can be either a full URL (e.g. https://myhost.lan/mytopic), a short URL which is then prepended https:// -// (e.g. myhost.lan -> https://myhost.lan), or a short name which is expanded using the default host in the -// config (e.g. mytopic -> https://ntfy.sh/mytopic). -func (c *Client) UnsubscribeAll(topic string) { - c.mu.Lock() - defer c.mu.Unlock() - topicURL := c.expandTopicURL(topic) - for _, sub := range c.subscriptions { - if sub.topicURL == topicURL { - delete(c.subscriptions, sub.ID) - sub.cancel() - } - } -} - -func (c *Client) expandTopicURL(topic string) string { +func (c *Client) expandTopicURL(topic string) (string, error) { if strings.HasPrefix(topic, "http://") || strings.HasPrefix(topic, "https://") { - return topic + return topic, nil } else if strings.Contains(topic, "/") { - return fmt.Sprintf("https://%s", topic) + return fmt.Sprintf("https://%s", topic), nil } - return fmt.Sprintf("%s/%s", c.config.DefaultHost, topic) + if !topicRegex.MatchString(topic) { + return "", fmt.Errorf("invalid topic name: %s", topic) + } + return fmt.Sprintf("%s/%s", c.config.DefaultHost, topic), nil } func handleSubscribeConnLoop(ctx context.Context, msgChan chan *Message, topicURL, subcriptionID string, options ...SubscribeOption) { diff --git a/client/client.yml b/client/client.yml index 1b81b80d..ebf4c281 100644 --- a/client/client.yml +++ b/client/client.yml @@ -7,7 +7,10 @@ # Default credentials will be used with "ntfy publish" and "ntfy subscribe" if no other credentials are provided. # You can set a default token to use or a default user:password combination, but not both. For an empty password, -# use empty double-quotes ("") +# use empty double-quotes (""). +# +# To override the default user:password combination or default token for a particular subscription (e.g., to send +# no Authorization header), set the user:pass/token for the subscription to empty double-quotes (""). # default-token: diff --git a/client/client_test.go b/client/client_test.go index a71ea5cb..f0b15a3f 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -21,7 +21,7 @@ func TestClient_Publish_Subscribe(t *testing.T) { defer test.StopServer(t, s, port) c := client.New(newTestConfig(port)) - subscriptionID := c.Subscribe("mytopic") + subscriptionID, _ := c.Subscribe("mytopic") time.Sleep(time.Second) msg, err := c.Publish("mytopic", "some message") diff --git a/client/config.go b/client/config.go index d4337d47..bc46ab89 100644 --- a/client/config.go +++ b/client/config.go @@ -23,9 +23,9 @@ type Config struct { // Subscribe is the struct for a Subscription within Config type Subscribe struct { Topic string `yaml:"topic"` - User string `yaml:"user"` + User *string `yaml:"user"` Password *string `yaml:"password"` - Token string `yaml:"token"` + Token *string `yaml:"token"` Command string `yaml:"command"` If map[string]string `yaml:"if"` } diff --git a/client/config_test.go b/client/config_test.go index f22e6b20..c85d3d49 100644 --- a/client/config_test.go +++ b/client/config_test.go @@ -37,7 +37,7 @@ subscribe: require.Equal(t, 4, len(conf.Subscribe)) require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic) require.Equal(t, "", conf.Subscribe[0].Command) - require.Equal(t, "phil", conf.Subscribe[0].User) + require.Equal(t, "phil", *conf.Subscribe[0].User) require.Equal(t, "mypass", *conf.Subscribe[0].Password) require.Equal(t, "echo-this", conf.Subscribe[1].Topic) require.Equal(t, `echo "Message received: $message"`, conf.Subscribe[1].Command) @@ -67,7 +67,7 @@ subscribe: require.Equal(t, 1, len(conf.Subscribe)) require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic) require.Equal(t, "", conf.Subscribe[0].Command) - require.Equal(t, "phil", conf.Subscribe[0].User) + require.Equal(t, "phil", *conf.Subscribe[0].User) require.Equal(t, "", *conf.Subscribe[0].Password) } @@ -91,7 +91,7 @@ subscribe: require.Equal(t, 1, len(conf.Subscribe)) require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic) require.Equal(t, "", conf.Subscribe[0].Command) - require.Equal(t, "phil", conf.Subscribe[0].User) + require.Equal(t, "phil", *conf.Subscribe[0].User) require.Nil(t, conf.Subscribe[0].Password) } @@ -113,7 +113,7 @@ subscribe: require.Equal(t, 1, len(conf.Subscribe)) require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic) require.Equal(t, "", conf.Subscribe[0].Command) - require.Equal(t, "phil", conf.Subscribe[0].User) + require.Equal(t, "phil", *conf.Subscribe[0].User) require.Nil(t, conf.Subscribe[0].Password) } @@ -134,7 +134,7 @@ subscribe: require.Equal(t, "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", conf.DefaultToken) require.Equal(t, 1, len(conf.Subscribe)) require.Equal(t, "mytopic", conf.Subscribe[0].Topic) - require.Equal(t, "", conf.Subscribe[0].User) + require.Nil(t, conf.Subscribe[0].User) require.Nil(t, conf.Subscribe[0].Password) - require.Equal(t, "", conf.Subscribe[0].Token) + require.Nil(t, conf.Subscribe[0].Token) } diff --git a/client/options.go b/client/options.go index dbca8c0e..630f1554 100644 --- a/client/options.go +++ b/client/options.go @@ -72,6 +72,11 @@ func WithAttach(attach string) PublishOption { return WithHeader("X-Attach", attach) } +// WithMarkdown instructs the server to interpret the message body as Markdown +func WithMarkdown() PublishOption { + return WithHeader("X-Markdown", "yes") +} + // WithFilename sets a filename for the attachment, and/or forces the HTTP body to interpreted as an attachment func WithFilename(filename string) PublishOption { return WithHeader("X-Filename", filename) @@ -92,6 +97,11 @@ func WithBearerAuth(token string) PublishOption { return WithHeader("Authorization", fmt.Sprintf("Bearer %s", token)) } +// WithEmptyAuth clears the Authorization header +func WithEmptyAuth() PublishOption { + return RemoveHeader("Authorization") +} + // WithNoCache instructs the server not to cache the message server-side func WithNoCache() PublishOption { return WithHeader("X-Cache", "no") @@ -182,3 +192,13 @@ func WithQueryParam(param, value string) RequestOption { return nil } } + +// RemoveHeader is a generic option to remove a header from a request +func RemoveHeader(header string) RequestOption { + return func(r *http.Request) error { + if header != "" { + delete(r.Header, header) + } + return nil + } +} diff --git a/cmd/publish.go b/cmd/publish.go index 0179f9fa..5ffe3adf 100644 --- a/cmd/publish.go +++ b/cmd/publish.go @@ -31,6 +31,7 @@ var flagsPublish = append( &cli.StringFlag{Name: "icon", Aliases: []string{"i"}, EnvVars: []string{"NTFY_ICON"}, Usage: "URL to use as notification icon"}, &cli.StringFlag{Name: "actions", Aliases: []string{"A"}, EnvVars: []string{"NTFY_ACTIONS"}, Usage: "actions JSON array or simple definition"}, &cli.StringFlag{Name: "attach", Aliases: []string{"a"}, EnvVars: []string{"NTFY_ATTACH"}, Usage: "URL to send as an external attachment"}, + &cli.BoolFlag{Name: "markdown", Aliases: []string{"md"}, EnvVars: []string{"NTFY_MARKDOWN"}, Usage: "Message is formatted as Markdown"}, &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"}, @@ -95,6 +96,7 @@ func execPublish(c *cli.Context) error { icon := c.String("icon") actions := c.String("actions") attach := c.String("attach") + markdown := c.Bool("markdown") filename := c.String("filename") file := c.String("file") email := c.String("email") @@ -140,6 +142,9 @@ func execPublish(c *cli.Context) error { if attach != "" { options = append(options, client.WithAttach(attach)) } + if markdown { + options = append(options, client.WithMarkdown()) + } if filename != "" { options = append(options, client.WithFilename(filename)) } diff --git a/cmd/serve.go b/cmd/serve.go index 95e63797..87b83dda 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -59,11 +59,12 @@ var flagsServe = append( altsrc.NewDurationFlag(&cli.DurationFlag{Name: "keepalive-interval", Aliases: []string{"keepalive_interval", "k"}, EnvVars: []string{"NTFY_KEEPALIVE_INTERVAL"}, Value: server.DefaultKeepaliveInterval, Usage: "interval of keepalive messages"}), altsrc.NewDurationFlag(&cli.DurationFlag{Name: "manager-interval", Aliases: []string{"manager_interval", "m"}, EnvVars: []string{"NTFY_MANAGER_INTERVAL"}, Value: server.DefaultManagerInterval, Usage: "interval of for message pruning and stats printing"}), altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "disallowed-topics", Aliases: []string{"disallowed_topics"}, EnvVars: []string{"NTFY_DISALLOWED_TOPICS"}, Usage: "topics that are not allowed to be used"}), - altsrc.NewStringFlag(&cli.StringFlag{Name: "web-root", Aliases: []string{"web_root"}, EnvVars: []string{"NTFY_WEB_ROOT"}, Value: "app", Usage: "sets web root to landing page (home), web app (app) or disabled (disable)"}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "web-root", Aliases: []string{"web_root"}, EnvVars: []string{"NTFY_WEB_ROOT"}, Value: "/", Usage: "sets root of the web app (e.g. /, or /app), or disables it (disable)"}), altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-signup", Aliases: []string{"enable_signup"}, EnvVars: []string{"NTFY_ENABLE_SIGNUP"}, Value: false, Usage: "allows users to sign up via the web app, or API"}), altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-login", Aliases: []string{"enable_login"}, EnvVars: []string{"NTFY_ENABLE_LOGIN"}, Value: false, Usage: "allows users to log in via the web app, or API"}), altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-reservations", Aliases: []string{"enable_reservations"}, EnvVars: []string{"NTFY_ENABLE_RESERVATIONS"}, Value: false, Usage: "allows users to reserve topics (if their tier allows it)"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "upstream-base-url", Aliases: []string{"upstream_base_url"}, EnvVars: []string{"NTFY_UPSTREAM_BASE_URL"}, Value: "", Usage: "forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers"}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "upstream-access-token", Aliases: []string{"upstream_access_token"}, EnvVars: []string{"NTFY_UPSTREAM_ACCESS_TOKEN"}, Value: "", Usage: "access token to use for the upstream server; needed only if upstream rate limits are exceeded or upstream server requires auth"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-addr", Aliases: []string{"smtp_sender_addr"}, EnvVars: []string{"NTFY_SMTP_SENDER_ADDR"}, Usage: "SMTP server address (host:port) for outgoing emails"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-user", Aliases: []string{"smtp_sender_user"}, EnvVars: []string{"NTFY_SMTP_SENDER_USER"}, Usage: "SMTP user (if e-mail sending is enabled)"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-pass", Aliases: []string{"smtp_sender_pass"}, EnvVars: []string{"NTFY_SMTP_SENDER_PASS"}, Usage: "SMTP password (if e-mail sending is enabled)"}), @@ -71,6 +72,10 @@ var flagsServe = append( altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-listen", Aliases: []string{"smtp_server_listen"}, EnvVars: []string{"NTFY_SMTP_SERVER_LISTEN"}, Usage: "SMTP server address (ip:port) for incoming emails, e.g. :25"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-domain", Aliases: []string{"smtp_server_domain"}, EnvVars: []string{"NTFY_SMTP_SERVER_DOMAIN"}, Usage: "SMTP domain for incoming e-mail, e.g. ntfy.sh"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-addr-prefix", Aliases: []string{"smtp_server_addr_prefix"}, EnvVars: []string{"NTFY_SMTP_SERVER_ADDR_PREFIX"}, Usage: "SMTP email address prefix for topics to prevent spam (e.g. 'ntfy-')"}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-account", Aliases: []string{"twilio_account"}, EnvVars: []string{"NTFY_TWILIO_ACCOUNT"}, Usage: "Twilio account SID, used for phone calls, e.g. AC123..."}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-auth-token", Aliases: []string{"twilio_auth_token"}, EnvVars: []string{"NTFY_TWILIO_AUTH_TOKEN"}, Usage: "Twilio auth token"}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-phone-number", Aliases: []string{"twilio_phone_number"}, EnvVars: []string{"NTFY_TWILIO_PHONE_NUMBER"}, Usage: "Twilio number to use for outgoing calls"}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-verify-service", Aliases: []string{"twilio_verify_service"}, EnvVars: []string{"NTFY_TWILIO_VERIFY_SERVICE"}, Usage: "Twilio Verify service ID, used for phone number verification"}), altsrc.NewIntFlag(&cli.IntFlag{Name: "global-topic-limit", Aliases: []string{"global_topic_limit", "T"}, EnvVars: []string{"NTFY_GLOBAL_TOPIC_LIMIT"}, Value: server.DefaultTotalTopicLimit, Usage: "total number of topics allowed"}), altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-subscription-limit", Aliases: []string{"visitor_subscription_limit"}, EnvVars: []string{"NTFY_VISITOR_SUBSCRIPTION_LIMIT"}, Value: server.DefaultVisitorSubscriptionLimit, Usage: "number of subscriptions per visitor"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-attachment-total-size-limit", Aliases: []string{"visitor_attachment_total_size_limit"}, EnvVars: []string{"NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: "100M", Usage: "total storage limit used for attachments per visitor"}), @@ -89,6 +94,11 @@ var flagsServe = append( altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-metrics", Aliases: []string{"enable_metrics"}, EnvVars: []string{"NTFY_ENABLE_METRICS"}, Value: false, Usage: "if set, Prometheus metrics are exposed via the /metrics endpoint"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "metrics-listen-http", Aliases: []string{"metrics_listen_http"}, EnvVars: []string{"NTFY_METRICS_LISTEN_HTTP"}, Usage: "ip:port used to expose the metrics endpoint (implicitly enables metrics)"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "profile-listen-http", Aliases: []string{"profile_listen_http"}, EnvVars: []string{"NTFY_PROFILE_LISTEN_HTTP"}, Usage: "ip:port used to expose the profiling endpoints (implicitly enables profiling)"}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-public-key", Aliases: []string{"web_push_public_key"}, EnvVars: []string{"NTFY_WEB_PUSH_PUBLIC_KEY"}, Usage: "public key used for web push notifications"}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-private-key", Aliases: []string{"web_push_private_key"}, EnvVars: []string{"NTFY_WEB_PUSH_PRIVATE_KEY"}, Usage: "private key used for web push notifications"}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-file", Aliases: []string{"web_push_file"}, EnvVars: []string{"NTFY_WEB_PUSH_FILE"}, Usage: "file used to store web push subscriptions"}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-email-address", Aliases: []string{"web_push_email_address"}, EnvVars: []string{"NTFY_WEB_PUSH_EMAIL_ADDRESS"}, Usage: "e-mail address of sender, required to use browser push services"}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-startup-queries", Aliases: []string{"web_push_startup_queries"}, EnvVars: []string{"NTFY_WEB_PUSH_STARTUP_QUERIES"}, Usage: "queries run when the web push database is initialized"}), ) var cmdServe = &cli.Command{ @@ -124,6 +134,11 @@ func execServe(c *cli.Context) error { keyFile := c.String("key-file") certFile := c.String("cert-file") firebaseKeyFile := c.String("firebase-key-file") + webPushPrivateKey := c.String("web-push-private-key") + webPushPublicKey := c.String("web-push-public-key") + webPushFile := c.String("web-push-file") + webPushEmailAddress := c.String("web-push-email-address") + webPushStartupQueries := c.String("web-push-startup-queries") cacheFile := c.String("cache-file") cacheDuration := c.Duration("cache-duration") cacheStartupQueries := c.String("cache-startup-queries") @@ -144,6 +159,7 @@ func execServe(c *cli.Context) error { enableLogin := c.Bool("enable-login") enableReservations := c.Bool("enable-reservations") upstreamBaseURL := c.String("upstream-base-url") + upstreamAccessToken := c.String("upstream-access-token") smtpSenderAddr := c.String("smtp-sender-addr") smtpSenderUser := c.String("smtp-sender-user") smtpSenderPass := c.String("smtp-sender-pass") @@ -151,6 +167,10 @@ func execServe(c *cli.Context) error { smtpServerListen := c.String("smtp-server-listen") smtpServerDomain := c.String("smtp-server-domain") smtpServerAddrPrefix := c.String("smtp-server-addr-prefix") + twilioAccount := c.String("twilio-account") + twilioAuthToken := c.String("twilio-auth-token") + twilioPhoneNumber := c.String("twilio-phone-number") + twilioVerifyService := c.String("twilio-verify-service") totalTopicLimit := c.Int("global-topic-limit") visitorSubscriptionLimit := c.Int("visitor-subscription-limit") visitorSubscriberRateLimiting := c.Bool("visitor-subscriber-rate-limiting") @@ -173,6 +193,8 @@ func execServe(c *cli.Context) error { // Check values if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) { return errors.New("if set, FCM key file must exist") + } else if webPushPublicKey != "" && (webPushPrivateKey == "" || webPushFile == "" || webPushEmailAddress == "" || baseURL == "") { + return errors.New("if web push is enabled, web-push-private-key, web-push-public-key, web-push-file, web-push-email-address, and base-url should be set. run 'ntfy webpush keys' to generate keys") } else if keepaliveInterval < 5*time.Second { return errors.New("keepalive interval cannot be lower than five seconds") } else if managerInterval < 5*time.Second { @@ -195,8 +217,6 @@ func execServe(c *cli.Context) error { return errors.New("if set, base-url must start with http:// or https://") } else if baseURL != "" && strings.HasSuffix(baseURL, "/") { return errors.New("if set, base-url must not end with a slash (/)") - } else if !util.Contains([]string{"app", "home", "disable"}, webRoot) { - return errors.New("if set, web-root must be 'home' or 'app'") } else if upstreamBaseURL != "" && !strings.HasPrefix(upstreamBaseURL, "http://") && !strings.HasPrefix(upstreamBaseURL, "https://") { return errors.New("if set, upstream-base-url must start with http:// or https://") } else if upstreamBaseURL != "" && strings.HasSuffix(upstreamBaseURL, "/") { @@ -211,10 +231,20 @@ func execServe(c *cli.Context) error { return errors.New("cannot set enable-signup without also setting enable-login") } else if stripeSecretKey != "" && (stripeWebhookKey == "" || baseURL == "") { return errors.New("if stripe-secret-key is set, stripe-webhook-key and base-url must also be set") + } else if twilioAccount != "" && (twilioAuthToken == "" || twilioPhoneNumber == "" || twilioVerifyService == "" || baseURL == "" || authFile == "") { + return errors.New("if twilio-account is set, twilio-auth-token, twilio-phone-number, twilio-verify-service, base-url, and auth-file must also be set") } - webRootIsApp := webRoot == "app" - enableWeb := webRoot != "disable" + // Backwards compatibility + if webRoot == "app" { + webRoot = "/" + } else if webRoot == "home" { + webRoot = "/app" + } else if webRoot == "disable" { + webRoot = "" + } else if !strings.HasPrefix(webRoot, "/") { + webRoot = "/" + webRoot + } // Default auth permissions authDefault, err := user.ParsePermission(authDefaultAccess) @@ -293,8 +323,9 @@ func execServe(c *cli.Context) error { conf.KeepaliveInterval = keepaliveInterval conf.ManagerInterval = managerInterval conf.DisallowedTopics = disallowedTopics - conf.WebRootIsApp = webRootIsApp + conf.WebRoot = webRoot conf.UpstreamBaseURL = upstreamBaseURL + conf.UpstreamAccessToken = upstreamAccessToken conf.SMTPSenderAddr = smtpSenderAddr conf.SMTPSenderUser = smtpSenderUser conf.SMTPSenderPass = smtpSenderPass @@ -302,6 +333,10 @@ func execServe(c *cli.Context) error { conf.SMTPServerListen = smtpServerListen conf.SMTPServerDomain = smtpServerDomain conf.SMTPServerAddrPrefix = smtpServerAddrPrefix + conf.TwilioAccount = twilioAccount + conf.TwilioAuthToken = twilioAuthToken + conf.TwilioPhoneNumber = twilioPhoneNumber + conf.TwilioVerifyService = twilioVerifyService conf.TotalTopicLimit = totalTopicLimit conf.VisitorSubscriptionLimit = visitorSubscriptionLimit conf.VisitorAttachmentTotalSizeLimit = visitorAttachmentTotalSizeLimit @@ -317,7 +352,6 @@ func execServe(c *cli.Context) error { conf.StripeSecretKey = stripeSecretKey conf.StripeWebhookKey = stripeWebhookKey conf.BillingContact = billingContact - conf.EnableWeb = enableWeb conf.EnableSignup = enableSignup conf.EnableLogin = enableLogin conf.EnableReservations = enableReservations @@ -325,6 +359,11 @@ func execServe(c *cli.Context) error { conf.MetricsListenHTTP = metricsListenHTTP conf.ProfileListenHTTP = profileListenHTTP conf.Version = c.App.Version + conf.WebPushPrivateKey = webPushPrivateKey + conf.WebPushPublicKey = webPushPublicKey + conf.WebPushFile = webPushFile + conf.WebPushEmailAddress = webPushEmailAddress + conf.WebPushStartupQueries = webPushStartupQueries // Set up hot-reloading of config go sigHandlerConfigReload(config) diff --git a/cmd/subscribe.go b/cmd/subscribe.go index 3b4b4477..77a1b5f1 100644 --- a/cmd/subscribe.go +++ b/cmd/subscribe.go @@ -72,7 +72,7 @@ ntfy subscribe TOPIC COMMAND $NTFY_TITLE $title, $t Message title $NTFY_PRIORITY $priority, $prio, $p Message priority (1=min, 5=max) $NTFY_TAGS $tags, $tag, $ta Message tags (comma separated list) - $NTFY_RAW $raw Raw JSON message + $NTFY_RAW $raw Raw JSON message Examples: ntfy sub mytopic 'notify-send "$m"' # Execute command for incoming messages @@ -119,8 +119,7 @@ func execSubscribe(c *cli.Context) error { } if token != "" { options = append(options, client.WithBearerAuth(token)) - } - if user != "" { + } else if user != "" { var pass string parts := strings.SplitN(user, ":", 2) if len(parts) == 2 { @@ -136,6 +135,10 @@ func execSubscribe(c *cli.Context) error { fmt.Fprintf(c.App.ErrWriter, "\r%s\r", strings.Repeat(" ", 20)) } options = append(options, client.WithBasicAuth(user, pass)) + } else if conf.DefaultToken != "" { + options = append(options, client.WithBearerAuth(conf.DefaultToken)) + } else if conf.DefaultUser != "" && conf.DefaultPassword != nil { + options = append(options, client.WithBasicAuth(conf.DefaultUser, *conf.DefaultPassword)) } if scheduled { options = append(options, client.WithScheduled()) @@ -191,7 +194,10 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic, topicOptions = append(topicOptions, auth) } - subscriptionID := cl.Subscribe(s.Topic, topicOptions...) + subscriptionID, err := cl.Subscribe(s.Topic, topicOptions...) + if err != nil { + return err + } if s.Command != "" { cmds[subscriptionID] = s.Command } else if conf.DefaultCommand != "" { @@ -201,7 +207,10 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic, } } if topic != "" { - subscriptionID := cl.Subscribe(topic, options...) + subscriptionID, err := cl.Subscribe(topic, options...) + if err != nil { + return err + } cmds[subscriptionID] = command } for m := range cl.Messages { @@ -216,12 +225,17 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic, } func maybeAddAuthHeader(s client.Subscribe, conf *client.Config) client.SubscribeOption { - // check for subscription token then subscription user:pass - if s.Token != "" { - return client.WithBearerAuth(s.Token) + // if an explicit empty token or empty user:pass is given, exit without auth + if (s.Token != nil && *s.Token == "") || (s.User != nil && *s.User == "" && s.Password != nil && *s.Password == "") { + return client.WithEmptyAuth() } - if s.User != "" && s.Password != nil { - return client.WithBasicAuth(s.User, *s.Password) + + // check for subscription token then subscription user:pass + if s.Token != nil && *s.Token != "" { + return client.WithBearerAuth(*s.Token) + } + if s.User != nil && *s.User != "" && s.Password != nil { + return client.WithBasicAuth(*s.User, *s.Password) } // if no subscription token nor subscription user:pass, check for default token then default user:pass diff --git a/cmd/subscribe_test.go b/cmd/subscribe_test.go index a22b0c97..08dbbf5d 100644 --- a/cmd/subscribe_test.go +++ b/cmd/subscribe_test.go @@ -310,3 +310,108 @@ func TestCLI_Subscribe_Token_And_UserPass(t *testing.T) { require.Error(t, err) require.Equal(t, "cannot set both --user and --token", err.Error()) } + +func TestCLI_Subscribe_Default_Token(t *testing.T) { + message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}` + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/mytopic/json", r.URL.Path) + require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization")) + + w.WriteHeader(http.StatusOK) + w.Write([]byte(message)) + })) + defer server.Close() + + filename := filepath.Join(t.TempDir(), "client.yml") + require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(` +default-host: %s +default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2 +`, server.URL)), 0600)) + + app, _, stdout, _ := newTestApp() + + require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "mytopic"})) + + require.Equal(t, message, strings.TrimSpace(stdout.String())) +} + +func TestCLI_Subscribe_Default_UserPass(t *testing.T) { + message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}` + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/mytopic/json", r.URL.Path) + require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization")) + + w.WriteHeader(http.StatusOK) + w.Write([]byte(message)) + })) + defer server.Close() + + filename := filepath.Join(t.TempDir(), "client.yml") + require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(` +default-host: %s +default-user: philipp +default-password: mypass +`, server.URL)), 0600)) + + app, _, stdout, _ := newTestApp() + + require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "mytopic"})) + + require.Equal(t, message, strings.TrimSpace(stdout.String())) +} + +func TestCLI_Subscribe_Override_Default_UserPass_With_Empty_UserPass(t *testing.T) { + message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}` + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/mytopic/json", r.URL.Path) + require.Equal(t, "", r.Header.Get("Authorization")) + + w.WriteHeader(http.StatusOK) + w.Write([]byte(message)) + })) + defer server.Close() + + filename := filepath.Join(t.TempDir(), "client.yml") + require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(` +default-host: %s +default-user: philipp +default-password: mypass +subscribe: + - topic: mytopic + user: "" + password: "" +`, server.URL)), 0600)) + + app, _, stdout, _ := newTestApp() + + require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename})) + + require.Equal(t, message, strings.TrimSpace(stdout.String())) +} + +func TestCLI_Subscribe_Override_Default_Token_With_Empty_Token(t *testing.T) { + message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}` + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/mytopic/json", r.URL.Path) + require.Equal(t, "", r.Header.Get("Authorization")) + + w.WriteHeader(http.StatusOK) + w.Write([]byte(message)) + })) + defer server.Close() + + filename := filepath.Join(t.TempDir(), "client.yml") + require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(` +default-host: %s +default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2 +subscribe: + - topic: mytopic + token: "" +`, server.URL)), 0600)) + + app, _, stdout, _ := newTestApp() + + require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename})) + + require.Equal(t, message, strings.TrimSpace(stdout.String())) +} diff --git a/cmd/tier.go b/cmd/tier.go index c0b83d71..f1c8ddcb 100644 --- a/cmd/tier.go +++ b/cmd/tier.go @@ -18,6 +18,7 @@ const ( defaultMessageLimit = 5000 defaultMessageExpiryDuration = "12h" defaultEmailLimit = 20 + defaultCallLimit = 0 defaultReservationLimit = 3 defaultAttachmentFileSizeLimit = "15M" defaultAttachmentTotalSizeLimit = "100M" @@ -48,6 +49,7 @@ var cmdTier = &cli.Command{ &cli.Int64Flag{Name: "message-limit", Value: defaultMessageLimit, Usage: "daily message limit"}, &cli.StringFlag{Name: "message-expiry-duration", Value: defaultMessageExpiryDuration, Usage: "duration after which messages are deleted"}, &cli.Int64Flag{Name: "email-limit", Value: defaultEmailLimit, Usage: "daily email limit"}, + &cli.Int64Flag{Name: "call-limit", Value: defaultCallLimit, Usage: "daily phone call limit"}, &cli.Int64Flag{Name: "reservation-limit", Value: defaultReservationLimit, Usage: "topic reservation limit"}, &cli.StringFlag{Name: "attachment-file-size-limit", Value: defaultAttachmentFileSizeLimit, Usage: "per-attachment file size limit"}, &cli.StringFlag{Name: "attachment-total-size-limit", Value: defaultAttachmentTotalSizeLimit, Usage: "total size limit of attachments for the user"}, @@ -91,6 +93,7 @@ Examples: &cli.Int64Flag{Name: "message-limit", Usage: "daily message limit"}, &cli.StringFlag{Name: "message-expiry-duration", Usage: "duration after which messages are deleted"}, &cli.Int64Flag{Name: "email-limit", Usage: "daily email limit"}, + &cli.Int64Flag{Name: "call-limit", Usage: "daily phone call limit"}, &cli.Int64Flag{Name: "reservation-limit", Usage: "topic reservation limit"}, &cli.StringFlag{Name: "attachment-file-size-limit", Usage: "per-attachment file size limit"}, &cli.StringFlag{Name: "attachment-total-size-limit", Usage: "total size limit of attachments for the user"}, @@ -215,6 +218,7 @@ func execTierAdd(c *cli.Context) error { MessageLimit: c.Int64("message-limit"), MessageExpiryDuration: messageExpiryDuration, EmailLimit: c.Int64("email-limit"), + CallLimit: c.Int64("call-limit"), ReservationLimit: c.Int64("reservation-limit"), AttachmentFileSizeLimit: attachmentFileSizeLimit, AttachmentTotalSizeLimit: attachmentTotalSizeLimit, @@ -267,6 +271,9 @@ func execTierChange(c *cli.Context) error { if c.IsSet("email-limit") { tier.EmailLimit = c.Int64("email-limit") } + if c.IsSet("call-limit") { + tier.CallLimit = c.Int64("call-limit") + } if c.IsSet("reservation-limit") { tier.ReservationLimit = c.Int64("reservation-limit") } @@ -357,6 +364,7 @@ func printTier(c *cli.Context, tier *user.Tier) { fmt.Fprintf(c.App.ErrWriter, "- Message limit: %d\n", tier.MessageLimit) fmt.Fprintf(c.App.ErrWriter, "- Message expiry duration: %s (%d seconds)\n", tier.MessageExpiryDuration.String(), int64(tier.MessageExpiryDuration.Seconds())) fmt.Fprintf(c.App.ErrWriter, "- Email limit: %d\n", tier.EmailLimit) + fmt.Fprintf(c.App.ErrWriter, "- Phone call limit: %d\n", tier.CallLimit) fmt.Fprintf(c.App.ErrWriter, "- Reservation limit: %d\n", tier.ReservationLimit) fmt.Fprintf(c.App.ErrWriter, "- Attachment file size limit: %s\n", util.FormatSize(tier.AttachmentFileSizeLimit)) fmt.Fprintf(c.App.ErrWriter, "- Attachment total size limit: %s\n", util.FormatSize(tier.AttachmentTotalSizeLimit)) diff --git a/cmd/webpush.go b/cmd/webpush.go new file mode 100644 index 00000000..ec66f083 --- /dev/null +++ b/cmd/webpush.go @@ -0,0 +1,48 @@ +//go:build !noserver + +package cmd + +import ( + "fmt" + + "github.com/SherClockHolmes/webpush-go" + "github.com/urfave/cli/v2" +) + +func init() { + commands = append(commands, cmdWebPush) +} + +var cmdWebPush = &cli.Command{ + Name: "webpush", + Usage: "Generate keys, in the future manage web push subscriptions", + UsageText: "ntfy webpush [keys]", + Category: categoryServer, + + Subcommands: []*cli.Command{ + { + Action: generateWebPushKeys, + Name: "keys", + Usage: "Generate VAPID keys to enable browser background push notifications", + UsageText: "ntfy webpush keys", + Category: categoryServer, + }, + }, +} + +func generateWebPushKeys(c *cli.Context) error { + privateKey, publicKey, err := webpush.GenerateVAPIDKeys() + if err != nil { + return err + } + _, err = fmt.Fprintf(c.App.ErrWriter, `Web Push keys generated. Add the following lines to your config file: + +web-push-public-key: %s +web-push-private-key: %s +web-push-file: /var/cache/ntfy/webpush.db # or similar +web-push-email-address: + +See https://ntfy.sh/docs/config/#web-push for details. +`, publicKey, privateKey) + return err +} diff --git a/cmd/webpush_test.go b/cmd/webpush_test.go new file mode 100644 index 00000000..1b364701 --- /dev/null +++ b/cmd/webpush_test.go @@ -0,0 +1,24 @@ +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/urfave/cli/v2" + "heckel.io/ntfy/server" +) + +func TestCLI_WebPush_GenerateKeys(t *testing.T) { + app, _, _, stderr := newTestApp() + require.Nil(t, runWebPushCommand(app, server.NewConfig(), "keys")) + require.Contains(t, stderr.String(), "Web Push keys generated.") +} + +func runWebPushCommand(app *cli.App, conf *server.Config, args ...string) error { + webPushArgs := []string{ + "ntfy", + "--log-level=ERROR", + "webpush", + } + return app.Run(append(webPushArgs, args...)) +} diff --git a/docs/_overrides/main.html b/docs/_overrides/main.html index 53a26fc4..52483ebd 100644 --- a/docs/_overrides/main.html +++ b/docs/_overrides/main.html @@ -32,11 +32,11 @@ -If you like ntfy, please consider sponsoring it via GitHub Sponsors +If you like ntfy, please consider sponsoring me via GitHub Sponsors or Liberapay - +, or subscribing to ntfy Pro. + + + diff --git a/go.mod b/go.mod index 10d32350..096bf38a 100644 --- a/go.mod +++ b/go.mod @@ -1,76 +1,84 @@ module heckel.io/ntfy -go 1.18 +go 1.21 + +toolchain go1.21.3 require ( - cloud.google.com/go/firestore v1.9.0 // indirect - cloud.google.com/go/storage v1.30.1 // indirect - github.com/BurntSushi/toml v1.2.1 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect - github.com/emersion/go-smtp v0.16.0 - github.com/gabriel-vasile/mimetype v1.4.2 - github.com/gorilla/websocket v1.5.0 - github.com/mattn/go-sqlite3 v1.14.16 - github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8 - github.com/stretchr/testify v1.8.1 - github.com/urfave/cli/v2 v2.25.1 - golang.org/x/crypto v0.7.0 - golang.org/x/oauth2 v0.6.0 // indirect - golang.org/x/sync v0.1.0 - golang.org/x/term v0.6.0 - golang.org/x/time v0.3.0 - google.golang.org/api v0.114.0 + cloud.google.com/go/firestore v1.14.0 // indirect + cloud.google.com/go/storage v1.35.1 // indirect + github.com/BurntSushi/toml v1.3.2 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect + github.com/emersion/go-smtp v0.18.0 + github.com/gabriel-vasile/mimetype v1.4.3 + github.com/gorilla/websocket v1.5.1 + github.com/mattn/go-sqlite3 v1.14.18 + github.com/olebedev/when v1.0.0 + github.com/stretchr/testify v1.8.4 + github.com/urfave/cli/v2 v2.25.7 + golang.org/x/crypto v0.15.0 + golang.org/x/oauth2 v0.14.0 // indirect + golang.org/x/sync v0.5.0 + golang.org/x/term v0.14.0 + golang.org/x/time v0.4.0 + google.golang.org/api v0.150.0 gopkg.in/yaml.v2 v2.4.0 ) +replace github.com/emersion/go-smtp => github.com/emersion/go-smtp v0.17.0 // Pin version due to breaking changes, see #839 + require github.com/pkg/errors v0.9.1 // indirect require ( - firebase.google.com/go/v4 v4.10.0 - github.com/microcosm-cc/bluemonday v1.0.23 - github.com/prometheus/client_golang v1.14.0 - github.com/stripe/stripe-go/v74 v74.14.0 + firebase.google.com/go/v4 v4.12.1 + github.com/SherClockHolmes/webpush-go v1.3.0 + github.com/microcosm-cc/bluemonday v1.0.26 + github.com/prometheus/client_golang v1.17.0 + github.com/stripe/stripe-go/v74 v74.30.0 ) require ( - cloud.google.com/go v0.110.0 // indirect - cloud.google.com/go/compute v1.19.0 // indirect + cloud.google.com/go v0.110.10 // indirect + cloud.google.com/go/compute v1.23.3 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect - cloud.google.com/go/iam v0.13.0 // indirect - cloud.google.com/go/longrunning v0.4.1 // indirect + cloud.google.com/go/iam v1.1.5 // indirect + cloud.google.com/go/longrunning v0.5.4 // indirect github.com/AlekSi/pointer v1.2.0 // indirect github.com/MicahParks/keyfunc v1.9.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead // indirect + github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/google/go-cmp v0.5.9 // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect - github.com/googleapis/gax-go/v2 v2.8.0 // indirect - github.com/gorilla/css v1.0.0 // indirect - github.com/kr/pretty v0.3.1 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/google/s2a-go v0.1.7 // indirect + github.com/google/uuid v1.4.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/googleapis/gax-go/v2 v2.12.0 // indirect + github.com/gorilla/css v1.0.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_model v0.3.0 // indirect - github.com/prometheus/common v0.42.0 // indirect - github.com/prometheus/procfs v0.9.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/stretchr/objx v0.5.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/net v0.8.0 // indirect - golang.org/x/sys v0.6.0 // indirect - golang.org/x/text v0.8.0 // indirect - golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/appengine/v2 v2.0.2 // indirect - google.golang.org/genproto v0.0.0-20230330200707-38013875ee22 // indirect - google.golang.org/grpc v1.54.0 // indirect - google.golang.org/protobuf v1.30.0 // indirect + golang.org/x/net v0.18.0 // indirect + golang.org/x/sys v0.14.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/appengine/v2 v2.0.5 // indirect + google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect + google.golang.org/grpc v1.59.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 27679caa..c672f742 100644 --- a/go.sum +++ b/go.sum @@ -1,27 +1,31 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys= -cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= -cloud.google.com/go/compute v1.19.0 h1:+9zda3WGgW1ZSTlVppLCYFIr48Pa35q1uG2N1itbCEQ= -cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= +cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y= +cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= +cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= +cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= -cloud.google.com/go/firestore v1.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA= -cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= -cloud.google.com/go/iam v0.13.0 h1:+CmB+K0J/33d0zSQ9SlFWUeCCEn5XJA0ZMZ3pHE9u8k= -cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= -cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM= -cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= -cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM= -cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E= -firebase.google.com/go/v4 v4.10.0 h1:dgK/8uwfJbzc5LZK/GyRRfIkZEDObN9q0kgEXsjlXN4= -firebase.google.com/go/v4 v4.10.0/go.mod h1:m0gLwPY9fxKggizzglgCNWOGnFnVPifLpqZzo5u3e/A= +cloud.google.com/go/firestore v1.14.0 h1:8aLcKnMPoldYU3YHgu4t2exrKhLQkqaXAGqT0ljrFVw= +cloud.google.com/go/firestore v1.14.0/go.mod h1:96MVaHLsEhbvkBEdZgfN+AS/GIkco1LRpH9Xp9YZfzQ= +cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= +cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= +cloud.google.com/go/longrunning v0.5.4 h1:w8xEcbZodnA2BbW6sVirkkoC+1gP8wS57EUUgGS0GVg= +cloud.google.com/go/longrunning v0.5.4/go.mod h1:zqNVncI0BOP8ST6XQD1+VcvuShMmq7+xFSzOL++V0dI= +cloud.google.com/go/storage v1.34.1 h1:H2Af2dU5J0PF7A5B+ECFIce+RqxVnrVilO+cu0TS3MI= +cloud.google.com/go/storage v1.34.1/go.mod h1:VN1ElqqvR9adg1k9xlkUJ55cMOP1/QjnNNuT5xQL6dY= +cloud.google.com/go/storage v1.35.1 h1:B59ahL//eDfx2IIKFBeT5Atm9wnNmj3+8xG/W4WB//w= +cloud.google.com/go/storage v1.35.1/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8= +firebase.google.com/go/v4 v4.12.1 h1:tDNvobifGsx/1HSFLnM0fmNfx/CDZSgsTO2KhZtgpcs= +firebase.google.com/go/v4 v4.12.1/go.mod h1:60c36dWLK4+j05Vw5XMllek3b3PCynU3BfI46OSwsUE= github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= -github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= +github.com/SherClockHolmes/webpush-go v1.3.0 h1:CAu3FvEE9QS4drc3iKNgpBWFfGqNthKlZhp5QpYnu6k= +github.com/SherClockHolmes/webpush-go v1.3.0/go.mod h1:AxRHmJuYwKGG1PVgYzToik1lphQvDnqFYDqimHvwhIw= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -31,8 +35,8 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -40,14 +44,18 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y= github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= -github.com/emersion/go-smtp v0.16.0 h1:eB9CY9527WdEZSs5sWisTmilDX7gG+Q/2IdRcmubpa8= -github.com/emersion/go-smtp v0.16.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= +github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 h1:hH4PQfOndHDlpzYfLAAfl63E8Le6F2+EL/cdhlkyRJY= +github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= +github.com/emersion/go-smtp v0.17.0 h1:tq90evlrcyqRfE6DSXaWVH54oX6OuZOQECEmhWBMEtI= +github.com/emersion/go-smtp v0.17.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= -github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= @@ -57,9 +65,7 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= @@ -68,6 +74,7 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -77,48 +84,49 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= -github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -github.com/googleapis/gax-go/v2 v2.8.0 h1:UBtEZqx1bjXtOQ5BVTkuYghXrr3N4V123VKJK67vJZc= -github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= -github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI= +github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= -github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/microcosm-cc/bluemonday v1.0.23 h1:SMZe2IGa0NuHvnVNAZ+6B38gsTbi5e4sViiWJyDDqFY= -github.com/microcosm-cc/bluemonday v1.0.23/go.mod h1:mN70sk7UkkF8TUr2IGBpNN0jAgStuPzlK76QuruE/z4= -github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8 h1:0uFGkScHef2Xd8g74BMHU1jFcnKEm0PzrPn4CluQ9FI= -github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8/go.mod h1:T0THb4kP9D3NNqlvCwIG4GyUioTAzEhB4RNVzig/43E= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= +github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= +github.com/olebedev/when v1.0.0 h1:T2DZCj8HxUhOVxcqaLOmzuTr+iZLtMHsZEim7mjIA2w= +github.com/olebedev/when v1.0.0/go.mod h1:T0THb4kP9D3NNqlvCwIG4GyUioTAzEhB4RNVzig/43E= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= -github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= +github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= -github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= -github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= -github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= -github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= -github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -130,42 +138,59 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stripe/stripe-go/v74 v74.14.0 h1:hB1Ocu/m3BUZ+PrTePsPSv8TKcXTrleCL5Y5JfB8zCo= -github.com/stripe/stripe-go/v74 v74.14.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw= -github.com/urfave/cli/v2 v2.25.1 h1:zw8dSP7ghX0Gmm8vugrs6q9Ku0wzweqPyshy+syu9Gw= -github.com/urfave/cli/v2 v2.25.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stripe/stripe-go/v74 v74.30.0 h1:0Kf0KkeFnY7iRhOwvTerX0Ia1BRw+eV1CVJ51mGYAUY= +github.com/stripe/stripe-go/v74 v74.30.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= +golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw= -golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= +golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= +golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= +golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0= +golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -174,49 +199,74 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8= +golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.4.0 h1:Z81tqI5ddIoXDPvVQ7/7CC9TnLM7ubaFG2qXYd5BbYY= +golang.org/x/time v0.4.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -google.golang.org/api v0.114.0 h1:1xQPji6cO2E2vLiI+C/XiFAnsn1WV3mjaEwGLhi3grE= -google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +google.golang.org/api v0.149.0 h1:b2CqT6kG+zqJIVKRQ3ELJVLN1PwHZ6DJ3dW8yl82rgY= +google.golang.org/api v0.149.0/go.mod h1:Mwn1B7JTXrzXtnvmzQE2BD6bYZQ8DShKZDZbeN9I7qI= +google.golang.org/api v0.150.0 h1:Z9k22qD289SZ8gCJrk4DrWXkNjtfvKAUo/l1ma8eBYE= +google.golang.org/api v0.150.0/go.mod h1:ccy+MJ6nrYFgE3WgRx/AMXOxOmU8Q4hSa+jjibzhxcg= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine/v2 v2.0.2 h1:MSqyWy2shDLwG7chbwBJ5uMyw6SNqJzhJHNDwYB0Akk= -google.golang.org/appengine/v2 v2.0.2/go.mod h1:PkgRUWz4o1XOvbqtWTkBtCitEJ5Tp4HoVEdMMYQR/8E= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/appengine/v2 v2.0.5 h1:4C+F3Cd3L2nWEfSmFEZDPjQvDwL8T0YCeZBysZifP3k= +google.golang.org/appengine/v2 v2.0.5/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20230330200707-38013875ee22 h1:n3ThVoQnHbCbnkhZZ1fx3+3fBAisViSwrpbtLV7vydY= -google.golang.org/genproto v0.0.0-20230330200707-38013875ee22/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= +google.golang.org/genproto v0.0.0-20231030173426-d783a09b4405 h1:I6WNifs6pF9tNdSob2W24JtyxIYjzFB9qDlpUC76q+U= +google.golang.org/genproto v0.0.0-20231030173426-d783a09b4405/go.mod h1:3WDQMjmJk36UQhjQ89emUzb1mdaHcPeeAh4SCBKznB4= +google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ= +google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY= +google.golang.org/genproto/googleapis/api v0.0.0-20231030173426-d783a09b4405 h1:HJMDndgxest5n2y77fnErkM62iUsptE/H8p0dC2Huo4= +google.golang.org/genproto/googleapis/api v0.0.0-20231030173426-d783a09b4405/go.mod h1:oT32Z4o8Zv2xPQTg0pbVaPr0MPOH6f14RgXt7zfIpwg= +google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo= +google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 h1:AB/lmRny7e2pLhFEYIbl5qkDAUt2h0ZRO4wGPhZf+ik= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405/go.mod h1:67X1fPuzjcrkymZzZV1vvkFeTn2Rvc6lYF9MYFGCcwE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag= -google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -228,8 +278,9 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/log/event.go b/log/event.go index ccde4126..b4b8f59f 100644 --- a/log/event.go +++ b/log/event.go @@ -41,34 +41,34 @@ func newEvent() *Event { // Fatal logs the event as FATAL, and exits the program with exit code 1 func (e *Event) Fatal(message string, v ...any) { - e.Field(fieldExitCode, 1).maybeLog(FatalLevel, message, v...) + e.Field(fieldExitCode, 1).Log(FatalLevel, message, v...) fmt.Fprintf(os.Stderr, message+"\n", v...) // Always output error to stderr os.Exit(1) } // Error logs the event with log level error -func (e *Event) Error(message string, v ...any) { - e.maybeLog(ErrorLevel, message, v...) +func (e *Event) Error(message string, v ...any) *Event { + return e.Log(ErrorLevel, message, v...) } // Warn logs the event with log level warn -func (e *Event) Warn(message string, v ...any) { - e.maybeLog(WarnLevel, message, v...) +func (e *Event) Warn(message string, v ...any) *Event { + return e.Log(WarnLevel, message, v...) } // Info logs the event with log level info -func (e *Event) Info(message string, v ...any) { - e.maybeLog(InfoLevel, message, v...) +func (e *Event) Info(message string, v ...any) *Event { + return e.Log(InfoLevel, message, v...) } // Debug logs the event with log level debug -func (e *Event) Debug(message string, v ...any) { - e.maybeLog(DebugLevel, message, v...) +func (e *Event) Debug(message string, v ...any) *Event { + return e.Log(DebugLevel, message, v...) } // Trace logs the event with log level trace -func (e *Event) Trace(message string, v ...any) { - e.maybeLog(TraceLevel, message, v...) +func (e *Event) Trace(message string, v ...any) *Event { + return e.Log(TraceLevel, message, v...) } // Tag adds a "tag" field to the log event @@ -108,6 +108,14 @@ func (e *Event) Field(key string, value any) *Event { return e } +// FieldIf adds a custom field and value to the log event if the given level is loggable +func (e *Event) FieldIf(key string, value any, level Level) *Event { + if e.Loggable(level) { + return e.Field(key, value) + } + return e +} + // Fields adds a map of fields to the log event func (e *Event) Fields(fields Context) *Event { if e.fields == nil { @@ -138,7 +146,7 @@ func (e *Event) With(contexters ...Contexter) *Event { // to determine if they match. This is super complicated, but required for efficiency. func (e *Event) Render(l Level, message string, v ...any) string { appliedContexters := e.maybeApplyContexters() - if !e.shouldLog(l) { + if !e.Loggable(l) { return "" } e.Message = fmt.Sprintf(message, v...) @@ -153,11 +161,12 @@ func (e *Event) Render(l Level, message string, v ...any) string { return e.String() } -// maybeLog logs the event to the defined output, or does nothing if Render returns an empty string -func (e *Event) maybeLog(l Level, message string, v ...any) { +// Log logs the event to the defined output, or does nothing if Render returns an empty string +func (e *Event) Log(l Level, message string, v ...any) *Event { if m := e.Render(l, message, v...); m != "" { log.Println(m) } + return e } // Loggable returns true if the given log level is lower or equal to the current log level @@ -199,10 +208,6 @@ func (e *Event) String() string { return fmt.Sprintf("%s %s (%s)", e.Level.String(), e.Message, strings.Join(fields, ", ")) } -func (e *Event) shouldLog(l Level) bool { - return e.globalLevelWithOverride() <= l -} - func (e *Event) globalLevelWithOverride() Level { mu.RLock() l, ov := level, overrides diff --git a/log/log_test.go b/log/log_test.go index ed35b495..d7ceb1c9 100644 --- a/log/log_test.go +++ b/log/log_test.go @@ -198,6 +198,30 @@ func TestLog_LevelOverride_ManyOnSameField(t *testing.T) { require.Equal(t, "", File()) } +func TestLog_FieldIf(t *testing.T) { + t.Cleanup(resetState) + + var out bytes.Buffer + SetOutput(&out) + SetLevel(DebugLevel) + SetFormat(JSONFormat) + + Time(time.Unix(11, 0).UTC()). + FieldIf("trace_field", "manager", TraceLevel). // This is not logged + Field("tag", "manager"). + Debug("trace_field is not logged") + SetLevel(TraceLevel) + Time(time.Unix(12, 0).UTC()). + FieldIf("trace_field", "manager", TraceLevel). // Now it is logged + Field("tag", "manager"). + Debug("trace_field is logged") + + expected := `{"time":"1970-01-01T00:00:11Z","level":"DEBUG","message":"trace_field is not logged","tag":"manager"} +{"time":"1970-01-01T00:00:12Z","level":"DEBUG","message":"trace_field is logged","tag":"manager","trace_field":"manager"} +` + require.Equal(t, expected, out.String()) +} + func TestLog_UsingStdLogger_JSON(t *testing.T) { t.Cleanup(resetState) diff --git a/mkdocs.yml b/mkdocs.yml index b49931d9..98a2d078 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -13,7 +13,7 @@ theme: language: en custom_dir: docs/_overrides logo: static/img/ntfy.png - favicon: static/img/favicon.png + favicon: static/img/favicon.ico include_search_page: false search_index_only: true palette: @@ -64,7 +64,6 @@ markdown_extensions: - attr_list - md_in_html - pymdownx.emoji: - emoji_index: !!python/name:materialx.emoji.twemoji emoji_generator: !!python/name:materialx.emoji.to_svg plugins: @@ -82,6 +81,7 @@ nav: - "Subscribing": - "From your phone": subscribe/phone.md - "From the Web app": subscribe/web.md + - "From the Desktop": subscribe/pwa.md - "From the CLI": subscribe/cli.md - "Using the API": subscribe/api.md - "Self-hosting": diff --git a/scripts/emoji-convert.sh b/scripts/emoji-convert.sh index 9817c884..8cbe397b 100755 --- a/scripts/emoji-convert.sh +++ b/scripts/emoji-convert.sh @@ -25,11 +25,11 @@ elif [[ "$1" == *.md ]]; then -You can [tag messages](../publish/#tags-emojis) with emojis 🥳 🎉 and other relevant strings. Matching tags are automatically +You can [tag messages](publish.md#tags-emojis) with emojis 🥳 🎉 and other relevant strings. Matching tags are automatically converted to emojis. This is a reference of all supported emojis. To learn more about the feature, please refer to the -[tagging and emojis page](../publish/#tags-emojis). +[tagging and emojis page](publish.md#tags-emojis). - +
" > "$1" count="$(cat "$SCRIPTDIR/emoji.json" | jq -r '.[] | .emoji' | wc -l)" @@ -37,9 +37,9 @@ converted to emojis. This is a reference of all supported emojis. To learn more for col in 0 1 2; do from="$(($col * $percolumn + 1))" to="$(($col * $percolumn + 1 + $percolumn))" - echo "
" >> "$1" + echo "" >> "$1" done diff --git a/server/config.go b/server/config.go index 7b533a55..9815aa88 100644 --- a/server/config.go +++ b/server/config.go @@ -1,10 +1,11 @@ package server import ( - "heckel.io/ntfy/user" "io/fs" "net/netip" "time" + + "heckel.io/ntfy/user" ) // Defines default config settings (excluding limits, see below) @@ -22,6 +23,12 @@ const ( DefaultStripePriceCacheDuration = 3 * time.Hour // Time to keep Stripe prices cached in memory before a refresh is needed ) +// Defines default Web Push settings +const ( + DefaultWebPushExpiryWarningDuration = 7 * 24 * time.Hour + DefaultWebPushExpiryDuration = 9 * 24 * time.Hour +) + // Defines all global and per-visitor limits // - message size limit: the max number of bytes for a message // - total topic limit: max number of topics overall @@ -92,12 +99,13 @@ type Config struct { KeepaliveInterval time.Duration ManagerInterval time.Duration DisallowedTopics []string - WebRootIsApp bool + WebRoot string // empty to disable DelayedSenderInterval time.Duration FirebaseKeepaliveInterval time.Duration FirebasePollInterval time.Duration FirebaseQuotaExceededPenaltyDuration time.Duration UpstreamBaseURL string + UpstreamAccessToken string SMTPSenderAddr string SMTPSenderUser string SMTPSenderPass string @@ -105,6 +113,12 @@ type Config struct { SMTPServerListen string SMTPServerDomain string SMTPServerAddrPrefix string + TwilioAccount string + TwilioAuthToken string + TwilioPhoneNumber string + TwilioCallsBaseURL string + TwilioVerifyBaseURL string + TwilioVerifyService string MetricsEnable bool MetricsListenHTTP string ProfileListenHTTP string @@ -133,13 +147,19 @@ type Config struct { StripeWebhookKey string StripePriceCacheDuration time.Duration BillingContact string - EnableWeb bool EnableSignup bool // Enable creation of accounts via API and UI EnableLogin bool EnableReservations bool // Allow users with role "user" to own/reserve topics EnableMetrics bool AccessControlAllowOrigin string // CORS header field to restrict access from web clients Version string // injected by App + WebPushPrivateKey string + WebPushPublicKey string + WebPushFile string + WebPushEmailAddress string + WebPushStartupQueries string + WebPushExpiryDuration time.Duration + WebPushExpiryWarningDuration time.Duration } // NewConfig instantiates a default new server config @@ -171,12 +191,13 @@ func NewConfig() *Config { KeepaliveInterval: DefaultKeepaliveInterval, ManagerInterval: DefaultManagerInterval, DisallowedTopics: DefaultDisallowedTopics, - WebRootIsApp: false, + WebRoot: "/", DelayedSenderInterval: DefaultDelayedSenderInterval, FirebaseKeepaliveInterval: DefaultFirebaseKeepaliveInterval, FirebasePollInterval: DefaultFirebasePollInterval, FirebaseQuotaExceededPenaltyDuration: DefaultFirebaseQuotaExceededPenaltyDuration, UpstreamBaseURL: "", + UpstreamAccessToken: "", SMTPSenderAddr: "", SMTPSenderUser: "", SMTPSenderPass: "", @@ -184,6 +205,12 @@ func NewConfig() *Config { SMTPServerListen: "", SMTPServerDomain: "", SMTPServerAddrPrefix: "", + TwilioCallsBaseURL: "https://api.twilio.com", // Override for tests + TwilioAccount: "", + TwilioAuthToken: "", + TwilioPhoneNumber: "", + TwilioVerifyBaseURL: "https://verify.twilio.com", // Override for tests + TwilioVerifyService: "", MessageLimit: DefaultMessageLengthLimit, MinDelay: DefaultMinDelay, MaxDelay: DefaultMaxDelay, @@ -209,11 +236,16 @@ func NewConfig() *Config { StripeWebhookKey: "", StripePriceCacheDuration: DefaultStripePriceCacheDuration, BillingContact: "", - EnableWeb: true, EnableSignup: false, EnableLogin: false, EnableReservations: false, AccessControlAllowOrigin: "*", Version: "", + WebPushPrivateKey: "", + WebPushPublicKey: "", + WebPushFile: "", + WebPushEmailAddress: "", + WebPushExpiryDuration: DefaultWebPushExpiryDuration, + WebPushExpiryWarningDuration: DefaultWebPushExpiryWarningDuration, } } diff --git a/server/errors.go b/server/errors.go index 8e565197..27ba3df0 100644 --- a/server/errors.go +++ b/server/errors.go @@ -106,12 +106,25 @@ var ( errHTTPBadRequestNotAPaidUser = &errHTTP{40027, http.StatusBadRequest, "invalid request: not a paid user", "", nil} errHTTPBadRequestBillingRequestInvalid = &errHTTP{40028, http.StatusBadRequest, "invalid request: not a valid billing request", "", nil} errHTTPBadRequestBillingSubscriptionExists = &errHTTP{40029, http.StatusBadRequest, "invalid request: billing subscription already exists", "", nil} + errHTTPBadRequestTierInvalid = &errHTTP{40030, http.StatusBadRequest, "invalid request: tier does not exist", "", nil} + errHTTPBadRequestUserNotFound = &errHTTP{40031, http.StatusBadRequest, "invalid request: user does not exist", "", nil} + errHTTPBadRequestPhoneCallsDisabled = &errHTTP{40032, http.StatusBadRequest, "invalid request: calling is disabled", "https://ntfy.sh/docs/config/#phone-calls", nil} + errHTTPBadRequestPhoneNumberInvalid = &errHTTP{40033, http.StatusBadRequest, "invalid request: phone number invalid", "https://ntfy.sh/docs/publish/#phone-calls", nil} + errHTTPBadRequestPhoneNumberNotVerified = &errHTTP{40034, http.StatusBadRequest, "invalid request: phone number not verified, or no matching verified numbers found", "https://ntfy.sh/docs/publish/#phone-calls", nil} + errHTTPBadRequestAnonymousCallsNotAllowed = &errHTTP{40035, http.StatusBadRequest, "invalid request: anonymous phone calls are not allowed", "https://ntfy.sh/docs/publish/#phone-calls", nil} + errHTTPBadRequestPhoneNumberVerifyChannelInvalid = &errHTTP{40036, http.StatusBadRequest, "invalid request: verification channel must be 'sms' or 'call'", "https://ntfy.sh/docs/publish/#phone-calls", nil} + errHTTPBadRequestDelayNoCall = &errHTTP{40037, http.StatusBadRequest, "delayed call notifications are not supported", "", nil} + errHTTPBadRequestWebPushSubscriptionInvalid = &errHTTP{40038, http.StatusBadRequest, "invalid request: web push payload malformed", "", nil} + errHTTPBadRequestWebPushEndpointUnknown = &errHTTP{40039, http.StatusBadRequest, "invalid request: web push endpoint unknown", "", nil} + errHTTPBadRequestWebPushTopicCountTooHigh = &errHTTP{40040, http.StatusBadRequest, "invalid request: too many web push topic subscriptions", "", nil} errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil} errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil} errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil} errHTTPConflictUserExists = &errHTTP{40901, http.StatusConflict, "conflict: user already exists", "", nil} errHTTPConflictTopicReserved = &errHTTP{40902, http.StatusConflict, "conflict: access control entry for topic or topic pattern already exists", "", nil} errHTTPConflictSubscriptionExists = &errHTTP{40903, http.StatusConflict, "conflict: topic subscription already exists", "", nil} + errHTTPConflictPhoneNumberExists = &errHTTP{40904, http.StatusConflict, "conflict: phone number already exists", "", nil} + errHTTPGonePhoneVerificationExpired = &errHTTP{41001, http.StatusGone, "phone number verification expired or does not exist", "", nil} errHTTPEntityTooLargeAttachment = &errHTTP{41301, http.StatusRequestEntityTooLarge, "attachment too large, or bandwidth limit reached", "https://ntfy.sh/docs/publish/#limitations", nil} errHTTPEntityTooLargeMatrixRequest = &errHTTP{41302, http.StatusRequestEntityTooLarge, "Matrix request is larger than the max allowed length", "", nil} errHTTPEntityTooLargeJSONBody = &errHTTP{41303, http.StatusRequestEntityTooLarge, "JSON body too large", "", nil} @@ -124,8 +137,10 @@ var ( errHTTPTooManyRequestsLimitReservations = &errHTTP{42907, http.StatusTooManyRequests, "limit reached: too many topic reservations for this user", "", nil} errHTTPTooManyRequestsLimitMessages = &errHTTP{42908, http.StatusTooManyRequests, "limit reached: daily message quota reached", "https://ntfy.sh/docs/publish/#limitations", nil} errHTTPTooManyRequestsLimitAuthFailure = &errHTTP{42909, http.StatusTooManyRequests, "limit reached: too many auth failures", "https://ntfy.sh/docs/publish/#limitations", nil} // FIXME document limit + errHTTPTooManyRequestsLimitCalls = &errHTTP{42910, http.StatusTooManyRequests, "limit reached: daily phone call quota reached", "https://ntfy.sh/docs/publish/#limitations", nil} errHTTPInternalError = &errHTTP{50001, http.StatusInternalServerError, "internal server error", "", nil} errHTTPInternalErrorInvalidPath = &errHTTP{50002, http.StatusInternalServerError, "internal server error: invalid path", "", nil} errHTTPInternalErrorMissingBaseURL = &errHTTP{50003, http.StatusInternalServerError, "internal server error: base-url must be be configured for this feature", "https://ntfy.sh/docs/config/", nil} + errHTTPInternalErrorWebPushUnableToPublish = &errHTTP{50004, http.StatusInternalServerError, "internal server error: unable to publish web push message", "", nil} errHTTPInsufficientStorageUnifiedPush = &errHTTP{50701, http.StatusInsufficientStorage, "cannot publish to UnifiedPush topic without previously active subscriber", "", nil} ) diff --git a/server/log.go b/server/log.go index 643f2ccb..978d0593 100644 --- a/server/log.go +++ b/server/log.go @@ -20,6 +20,7 @@ const ( tagFirebase = "firebase" tagSMTP = "smtp" // Receive email tagEmail = "email" // Send email + tagTwilio = "twilio" tagFileCache = "file_cache" tagMessageCache = "message_cache" tagStripe = "stripe" @@ -28,6 +29,7 @@ const ( tagResetter = "resetter" tagWebsocket = "websocket" tagMatrix = "matrix" + tagWebPush = "webpush" ) var ( diff --git a/server/mailer_emoji.json b/server/mailer_emoji.json deleted file mode 100644 index 4d4c32fc..00000000 --- a/server/mailer_emoji.json +++ /dev/null @@ -1 +0,0 @@ -[{"emoji":"😀","aliases":["grinning"]},{"emoji":"😃","aliases":["smiley"]},{"emoji":"😄","aliases":["smile"]},{"emoji":"😁","aliases":["grin"]},{"emoji":"😆","aliases":["laughing","satisfied"]},{"emoji":"😅","aliases":["sweat_smile"]},{"emoji":"🤣","aliases":["rofl"]},{"emoji":"😂","aliases":["joy"]},{"emoji":"🙂","aliases":["slightly_smiling_face"]},{"emoji":"🙃","aliases":["upside_down_face"]},{"emoji":"😉","aliases":["wink"]},{"emoji":"😊","aliases":["blush"]},{"emoji":"😇","aliases":["innocent"]},{"emoji":"🥰","aliases":["smiling_face_with_three_hearts"]},{"emoji":"😍","aliases":["heart_eyes"]},{"emoji":"🤩","aliases":["star_struck"]},{"emoji":"😘","aliases":["kissing_heart"]},{"emoji":"😗","aliases":["kissing"]},{"emoji":"☺️","aliases":["relaxed"]},{"emoji":"😚","aliases":["kissing_closed_eyes"]},{"emoji":"😙","aliases":["kissing_smiling_eyes"]},{"emoji":"🥲","aliases":["smiling_face_with_tear"]},{"emoji":"😋","aliases":["yum"]},{"emoji":"😛","aliases":["stuck_out_tongue"]},{"emoji":"😜","aliases":["stuck_out_tongue_winking_eye"]},{"emoji":"🤪","aliases":["zany_face"]},{"emoji":"😝","aliases":["stuck_out_tongue_closed_eyes"]},{"emoji":"🤑","aliases":["money_mouth_face"]},{"emoji":"🤗","aliases":["hugs"]},{"emoji":"🤭","aliases":["hand_over_mouth"]},{"emoji":"🤫","aliases":["shushing_face"]},{"emoji":"🤔","aliases":["thinking"]},{"emoji":"🤐","aliases":["zipper_mouth_face"]},{"emoji":"🤨","aliases":["raised_eyebrow"]},{"emoji":"😐","aliases":["neutral_face"]},{"emoji":"😑","aliases":["expressionless"]},{"emoji":"😶","aliases":["no_mouth"]},{"emoji":"😶‍🌫️","aliases":["face_in_clouds"]},{"emoji":"😏","aliases":["smirk"]},{"emoji":"😒","aliases":["unamused"]},{"emoji":"🙄","aliases":["roll_eyes"]},{"emoji":"😬","aliases":["grimacing"]},{"emoji":"😮‍💨","aliases":["face_exhaling"]},{"emoji":"🤥","aliases":["lying_face"]},{"emoji":"😌","aliases":["relieved"]},{"emoji":"😔","aliases":["pensive"]},{"emoji":"😪","aliases":["sleepy"]},{"emoji":"🤤","aliases":["drooling_face"]},{"emoji":"😴","aliases":["sleeping"]},{"emoji":"😷","aliases":["mask"]},{"emoji":"🤒","aliases":["face_with_thermometer"]},{"emoji":"🤕","aliases":["face_with_head_bandage"]},{"emoji":"🤢","aliases":["nauseated_face"]},{"emoji":"🤮","aliases":["vomiting_face"]},{"emoji":"🤧","aliases":["sneezing_face"]},{"emoji":"🥵","aliases":["hot_face"]},{"emoji":"🥶","aliases":["cold_face"]},{"emoji":"🥴","aliases":["woozy_face"]},{"emoji":"😵","aliases":["dizzy_face"]},{"emoji":"😵‍💫","aliases":["face_with_spiral_eyes"]},{"emoji":"🤯","aliases":["exploding_head"]},{"emoji":"🤠","aliases":["cowboy_hat_face"]},{"emoji":"🥳","aliases":["partying_face"]},{"emoji":"🥸","aliases":["disguised_face"]},{"emoji":"😎","aliases":["sunglasses"]},{"emoji":"🤓","aliases":["nerd_face"]},{"emoji":"🧐","aliases":["monocle_face"]},{"emoji":"😕","aliases":["confused"]},{"emoji":"😟","aliases":["worried"]},{"emoji":"🙁","aliases":["slightly_frowning_face"]},{"emoji":"☹️","aliases":["frowning_face"]},{"emoji":"😮","aliases":["open_mouth"]},{"emoji":"😯","aliases":["hushed"]},{"emoji":"😲","aliases":["astonished"]},{"emoji":"😳","aliases":["flushed"]},{"emoji":"🥺","aliases":["pleading_face"]},{"emoji":"😦","aliases":["frowning"]},{"emoji":"😧","aliases":["anguished"]},{"emoji":"😨","aliases":["fearful"]},{"emoji":"😰","aliases":["cold_sweat"]},{"emoji":"😥","aliases":["disappointed_relieved"]},{"emoji":"😢","aliases":["cry"]},{"emoji":"😭","aliases":["sob"]},{"emoji":"😱","aliases":["scream"]},{"emoji":"😖","aliases":["confounded"]},{"emoji":"😣","aliases":["persevere"]},{"emoji":"😞","aliases":["disappointed"]},{"emoji":"😓","aliases":["sweat"]},{"emoji":"😩","aliases":["weary"]},{"emoji":"😫","aliases":["tired_face"]},{"emoji":"🥱","aliases":["yawning_face"]},{"emoji":"😤","aliases":["triumph"]},{"emoji":"😡","aliases":["rage","pout"]},{"emoji":"😠","aliases":["angry"]},{"emoji":"🤬","aliases":["cursing_face"]},{"emoji":"😈","aliases":["smiling_imp"]},{"emoji":"👿","aliases":["imp"]},{"emoji":"💀","aliases":["skull"]},{"emoji":"☠️","aliases":["skull_and_crossbones"]},{"emoji":"💩","aliases":["hankey","poop","shit"]},{"emoji":"🤡","aliases":["clown_face"]},{"emoji":"👹","aliases":["japanese_ogre"]},{"emoji":"👺","aliases":["japanese_goblin"]},{"emoji":"👻","aliases":["ghost"]},{"emoji":"👽","aliases":["alien"]},{"emoji":"👾","aliases":["space_invader"]},{"emoji":"🤖","aliases":["robot"]},{"emoji":"😺","aliases":["smiley_cat"]},{"emoji":"😸","aliases":["smile_cat"]},{"emoji":"😹","aliases":["joy_cat"]},{"emoji":"😻","aliases":["heart_eyes_cat"]},{"emoji":"😼","aliases":["smirk_cat"]},{"emoji":"😽","aliases":["kissing_cat"]},{"emoji":"🙀","aliases":["scream_cat"]},{"emoji":"😿","aliases":["crying_cat_face"]},{"emoji":"😾","aliases":["pouting_cat"]},{"emoji":"🙈","aliases":["see_no_evil"]},{"emoji":"🙉","aliases":["hear_no_evil"]},{"emoji":"🙊","aliases":["speak_no_evil"]},{"emoji":"💋","aliases":["kiss"]},{"emoji":"💌","aliases":["love_letter"]},{"emoji":"💘","aliases":["cupid"]},{"emoji":"💝","aliases":["gift_heart"]},{"emoji":"💖","aliases":["sparkling_heart"]},{"emoji":"💗","aliases":["heartpulse"]},{"emoji":"💓","aliases":["heartbeat"]},{"emoji":"💞","aliases":["revolving_hearts"]},{"emoji":"💕","aliases":["two_hearts"]},{"emoji":"💟","aliases":["heart_decoration"]},{"emoji":"❣️","aliases":["heavy_heart_exclamation"]},{"emoji":"💔","aliases":["broken_heart"]},{"emoji":"❤️‍🔥","aliases":["heart_on_fire"]},{"emoji":"❤️‍🩹","aliases":["mending_heart"]},{"emoji":"❤️","aliases":["heart"]},{"emoji":"🧡","aliases":["orange_heart"]},{"emoji":"💛","aliases":["yellow_heart"]},{"emoji":"💚","aliases":["green_heart"]},{"emoji":"💙","aliases":["blue_heart"]},{"emoji":"💜","aliases":["purple_heart"]},{"emoji":"🤎","aliases":["brown_heart"]},{"emoji":"🖤","aliases":["black_heart"]},{"emoji":"🤍","aliases":["white_heart"]},{"emoji":"💯","aliases":["100"]},{"emoji":"💢","aliases":["anger"]},{"emoji":"💥","aliases":["boom","collision"]},{"emoji":"💫","aliases":["dizzy"]},{"emoji":"💦","aliases":["sweat_drops"]},{"emoji":"💨","aliases":["dash"]},{"emoji":"🕳️","aliases":["hole"]},{"emoji":"💣","aliases":["bomb"]},{"emoji":"💬","aliases":["speech_balloon"]},{"emoji":"👁️‍🗨️","aliases":["eye_speech_bubble"]},{"emoji":"🗨️","aliases":["left_speech_bubble"]},{"emoji":"🗯️","aliases":["right_anger_bubble"]},{"emoji":"💭","aliases":["thought_balloon"]},{"emoji":"💤","aliases":["zzz"]},{"emoji":"👋","aliases":["wave"]},{"emoji":"🤚","aliases":["raised_back_of_hand"]},{"emoji":"🖐️","aliases":["raised_hand_with_fingers_splayed"]},{"emoji":"✋","aliases":["hand","raised_hand"]},{"emoji":"🖖","aliases":["vulcan_salute"]},{"emoji":"👌","aliases":["ok_hand"]},{"emoji":"🤌","aliases":["pinched_fingers"]},{"emoji":"🤏","aliases":["pinching_hand"]},{"emoji":"✌️","aliases":["v"]},{"emoji":"🤞","aliases":["crossed_fingers"]},{"emoji":"🤟","aliases":["love_you_gesture"]},{"emoji":"🤘","aliases":["metal"]},{"emoji":"🤙","aliases":["call_me_hand"]},{"emoji":"👈","aliases":["point_left"]},{"emoji":"👉","aliases":["point_right"]},{"emoji":"👆","aliases":["point_up_2"]},{"emoji":"🖕","aliases":["middle_finger","fu"]},{"emoji":"👇","aliases":["point_down"]},{"emoji":"☝️","aliases":["point_up"]},{"emoji":"👍","aliases":["+1","thumbsup"]},{"emoji":"👎","aliases":["-1","thumbsdown"]},{"emoji":"✊","aliases":["fist_raised","fist"]},{"emoji":"👊","aliases":["fist_oncoming","facepunch","punch"]},{"emoji":"🤛","aliases":["fist_left"]},{"emoji":"🤜","aliases":["fist_right"]},{"emoji":"👏","aliases":["clap"]},{"emoji":"🙌","aliases":["raised_hands"]},{"emoji":"👐","aliases":["open_hands"]},{"emoji":"🤲","aliases":["palms_up_together"]},{"emoji":"🤝","aliases":["handshake"]},{"emoji":"🙏","aliases":["pray"]},{"emoji":"✍️","aliases":["writing_hand"]},{"emoji":"💅","aliases":["nail_care"]},{"emoji":"🤳","aliases":["selfie"]},{"emoji":"💪","aliases":["muscle"]},{"emoji":"🦾","aliases":["mechanical_arm"]},{"emoji":"🦿","aliases":["mechanical_leg"]},{"emoji":"🦵","aliases":["leg"]},{"emoji":"🦶","aliases":["foot"]},{"emoji":"👂","aliases":["ear"]},{"emoji":"🦻","aliases":["ear_with_hearing_aid"]},{"emoji":"👃","aliases":["nose"]},{"emoji":"🧠","aliases":["brain"]},{"emoji":"🫀","aliases":["anatomical_heart"]},{"emoji":"🫁","aliases":["lungs"]},{"emoji":"🦷","aliases":["tooth"]},{"emoji":"🦴","aliases":["bone"]},{"emoji":"👀","aliases":["eyes"]},{"emoji":"👁️","aliases":["eye"]},{"emoji":"👅","aliases":["tongue"]},{"emoji":"👄","aliases":["lips"]},{"emoji":"👶","aliases":["baby"]},{"emoji":"🧒","aliases":["child"]},{"emoji":"👦","aliases":["boy"]},{"emoji":"👧","aliases":["girl"]},{"emoji":"🧑","aliases":["adult"]},{"emoji":"👱","aliases":["blond_haired_person"]},{"emoji":"👨","aliases":["man"]},{"emoji":"🧔","aliases":["bearded_person"]},{"emoji":"🧔‍♂️","aliases":["man_beard"]},{"emoji":"🧔‍♀️","aliases":["woman_beard"]},{"emoji":"👨‍🦰","aliases":["red_haired_man"]},{"emoji":"👨‍🦱","aliases":["curly_haired_man"]},{"emoji":"👨‍🦳","aliases":["white_haired_man"]},{"emoji":"👨‍🦲","aliases":["bald_man"]},{"emoji":"👩","aliases":["woman"]},{"emoji":"👩‍🦰","aliases":["red_haired_woman"]},{"emoji":"🧑‍🦰","aliases":["person_red_hair"]},{"emoji":"👩‍🦱","aliases":["curly_haired_woman"]},{"emoji":"🧑‍🦱","aliases":["person_curly_hair"]},{"emoji":"👩‍🦳","aliases":["white_haired_woman"]},{"emoji":"🧑‍🦳","aliases":["person_white_hair"]},{"emoji":"👩‍🦲","aliases":["bald_woman"]},{"emoji":"🧑‍🦲","aliases":["person_bald"]},{"emoji":"👱‍♀️","aliases":["blond_haired_woman","blonde_woman"]},{"emoji":"👱‍♂️","aliases":["blond_haired_man"]},{"emoji":"🧓","aliases":["older_adult"]},{"emoji":"👴","aliases":["older_man"]},{"emoji":"👵","aliases":["older_woman"]},{"emoji":"🙍","aliases":["frowning_person"]},{"emoji":"🙍‍♂️","aliases":["frowning_man"]},{"emoji":"🙍‍♀️","aliases":["frowning_woman"]},{"emoji":"🙎","aliases":["pouting_face"]},{"emoji":"🙎‍♂️","aliases":["pouting_man"]},{"emoji":"🙎‍♀️","aliases":["pouting_woman"]},{"emoji":"🙅","aliases":["no_good"]},{"emoji":"🙅‍♂️","aliases":["no_good_man","ng_man"]},{"emoji":"🙅‍♀️","aliases":["no_good_woman","ng_woman"]},{"emoji":"🙆","aliases":["ok_person"]},{"emoji":"🙆‍♂️","aliases":["ok_man"]},{"emoji":"🙆‍♀️","aliases":["ok_woman"]},{"emoji":"💁","aliases":["tipping_hand_person","information_desk_person"]},{"emoji":"💁‍♂️","aliases":["tipping_hand_man","sassy_man"]},{"emoji":"💁‍♀️","aliases":["tipping_hand_woman","sassy_woman"]},{"emoji":"🙋","aliases":["raising_hand"]},{"emoji":"🙋‍♂️","aliases":["raising_hand_man"]},{"emoji":"🙋‍♀️","aliases":["raising_hand_woman"]},{"emoji":"🧏","aliases":["deaf_person"]},{"emoji":"🧏‍♂️","aliases":["deaf_man"]},{"emoji":"🧏‍♀️","aliases":["deaf_woman"]},{"emoji":"🙇","aliases":["bow"]},{"emoji":"🙇‍♂️","aliases":["bowing_man"]},{"emoji":"🙇‍♀️","aliases":["bowing_woman"]},{"emoji":"🤦","aliases":["facepalm"]},{"emoji":"🤦‍♂️","aliases":["man_facepalming"]},{"emoji":"🤦‍♀️","aliases":["woman_facepalming"]},{"emoji":"🤷","aliases":["shrug"]},{"emoji":"🤷‍♂️","aliases":["man_shrugging"]},{"emoji":"🤷‍♀️","aliases":["woman_shrugging"]},{"emoji":"🧑‍⚕️","aliases":["health_worker"]},{"emoji":"👨‍⚕️","aliases":["man_health_worker"]},{"emoji":"👩‍⚕️","aliases":["woman_health_worker"]},{"emoji":"🧑‍🎓","aliases":["student"]},{"emoji":"👨‍🎓","aliases":["man_student"]},{"emoji":"👩‍🎓","aliases":["woman_student"]},{"emoji":"🧑‍🏫","aliases":["teacher"]},{"emoji":"👨‍🏫","aliases":["man_teacher"]},{"emoji":"👩‍🏫","aliases":["woman_teacher"]},{"emoji":"🧑‍⚖️","aliases":["judge"]},{"emoji":"👨‍⚖️","aliases":["man_judge"]},{"emoji":"👩‍⚖️","aliases":["woman_judge"]},{"emoji":"🧑‍🌾","aliases":["farmer"]},{"emoji":"👨‍🌾","aliases":["man_farmer"]},{"emoji":"👩‍🌾","aliases":["woman_farmer"]},{"emoji":"🧑‍🍳","aliases":["cook"]},{"emoji":"👨‍🍳","aliases":["man_cook"]},{"emoji":"👩‍🍳","aliases":["woman_cook"]},{"emoji":"🧑‍🔧","aliases":["mechanic"]},{"emoji":"👨‍🔧","aliases":["man_mechanic"]},{"emoji":"👩‍🔧","aliases":["woman_mechanic"]},{"emoji":"🧑‍🏭","aliases":["factory_worker"]},{"emoji":"👨‍🏭","aliases":["man_factory_worker"]},{"emoji":"👩‍🏭","aliases":["woman_factory_worker"]},{"emoji":"🧑‍💼","aliases":["office_worker"]},{"emoji":"👨‍💼","aliases":["man_office_worker"]},{"emoji":"👩‍💼","aliases":["woman_office_worker"]},{"emoji":"🧑‍🔬","aliases":["scientist"]},{"emoji":"👨‍🔬","aliases":["man_scientist"]},{"emoji":"👩‍🔬","aliases":["woman_scientist"]},{"emoji":"🧑‍💻","aliases":["technologist"]},{"emoji":"👨‍💻","aliases":["man_technologist"]},{"emoji":"👩‍💻","aliases":["woman_technologist"]},{"emoji":"🧑‍🎤","aliases":["singer"]},{"emoji":"👨‍🎤","aliases":["man_singer"]},{"emoji":"👩‍🎤","aliases":["woman_singer"]},{"emoji":"🧑‍🎨","aliases":["artist"]},{"emoji":"👨‍🎨","aliases":["man_artist"]},{"emoji":"👩‍🎨","aliases":["woman_artist"]},{"emoji":"🧑‍✈️","aliases":["pilot"]},{"emoji":"👨‍✈️","aliases":["man_pilot"]},{"emoji":"👩‍✈️","aliases":["woman_pilot"]},{"emoji":"🧑‍🚀","aliases":["astronaut"]},{"emoji":"👨‍🚀","aliases":["man_astronaut"]},{"emoji":"👩‍🚀","aliases":["woman_astronaut"]},{"emoji":"🧑‍🚒","aliases":["firefighter"]},{"emoji":"👨‍🚒","aliases":["man_firefighter"]},{"emoji":"👩‍🚒","aliases":["woman_firefighter"]},{"emoji":"👮","aliases":["police_officer","cop"]},{"emoji":"👮‍♂️","aliases":["policeman"]},{"emoji":"👮‍♀️","aliases":["policewoman"]},{"emoji":"🕵️","aliases":["detective"]},{"emoji":"🕵️‍♂️","aliases":["male_detective"]},{"emoji":"🕵️‍♀️","aliases":["female_detective"]},{"emoji":"💂","aliases":["guard"]},{"emoji":"💂‍♂️","aliases":["guardsman"]},{"emoji":"💂‍♀️","aliases":["guardswoman"]},{"emoji":"🥷","aliases":["ninja"]},{"emoji":"👷","aliases":["construction_worker"]},{"emoji":"👷‍♂️","aliases":["construction_worker_man"]},{"emoji":"👷‍♀️","aliases":["construction_worker_woman"]},{"emoji":"🤴","aliases":["prince"]},{"emoji":"👸","aliases":["princess"]},{"emoji":"👳","aliases":["person_with_turban"]},{"emoji":"👳‍♂️","aliases":["man_with_turban"]},{"emoji":"👳‍♀️","aliases":["woman_with_turban"]},{"emoji":"👲","aliases":["man_with_gua_pi_mao"]},{"emoji":"🧕","aliases":["woman_with_headscarf"]},{"emoji":"🤵","aliases":["person_in_tuxedo"]},{"emoji":"🤵‍♂️","aliases":["man_in_tuxedo"]},{"emoji":"🤵‍♀️","aliases":["woman_in_tuxedo"]},{"emoji":"👰","aliases":["person_with_veil"]},{"emoji":"👰‍♂️","aliases":["man_with_veil"]},{"emoji":"👰‍♀️","aliases":["woman_with_veil","bride_with_veil"]},{"emoji":"🤰","aliases":["pregnant_woman"]},{"emoji":"🤱","aliases":["breast_feeding"]},{"emoji":"👩‍🍼","aliases":["woman_feeding_baby"]},{"emoji":"👨‍🍼","aliases":["man_feeding_baby"]},{"emoji":"🧑‍🍼","aliases":["person_feeding_baby"]},{"emoji":"👼","aliases":["angel"]},{"emoji":"🎅","aliases":["santa"]},{"emoji":"🤶","aliases":["mrs_claus"]},{"emoji":"🧑‍🎄","aliases":["mx_claus"]},{"emoji":"🦸","aliases":["superhero"]},{"emoji":"🦸‍♂️","aliases":["superhero_man"]},{"emoji":"🦸‍♀️","aliases":["superhero_woman"]},{"emoji":"🦹","aliases":["supervillain"]},{"emoji":"🦹‍♂️","aliases":["supervillain_man"]},{"emoji":"🦹‍♀️","aliases":["supervillain_woman"]},{"emoji":"🧙","aliases":["mage"]},{"emoji":"🧙‍♂️","aliases":["mage_man"]},{"emoji":"🧙‍♀️","aliases":["mage_woman"]},{"emoji":"🧚","aliases":["fairy"]},{"emoji":"🧚‍♂️","aliases":["fairy_man"]},{"emoji":"🧚‍♀️","aliases":["fairy_woman"]},{"emoji":"🧛","aliases":["vampire"]},{"emoji":"🧛‍♂️","aliases":["vampire_man"]},{"emoji":"🧛‍♀️","aliases":["vampire_woman"]},{"emoji":"🧜","aliases":["merperson"]},{"emoji":"🧜‍♂️","aliases":["merman"]},{"emoji":"🧜‍♀️","aliases":["mermaid"]},{"emoji":"🧝","aliases":["elf"]},{"emoji":"🧝‍♂️","aliases":["elf_man"]},{"emoji":"🧝‍♀️","aliases":["elf_woman"]},{"emoji":"🧞","aliases":["genie"]},{"emoji":"🧞‍♂️","aliases":["genie_man"]},{"emoji":"🧞‍♀️","aliases":["genie_woman"]},{"emoji":"🧟","aliases":["zombie"]},{"emoji":"🧟‍♂️","aliases":["zombie_man"]},{"emoji":"🧟‍♀️","aliases":["zombie_woman"]},{"emoji":"💆","aliases":["massage"]},{"emoji":"💆‍♂️","aliases":["massage_man"]},{"emoji":"💆‍♀️","aliases":["massage_woman"]},{"emoji":"💇","aliases":["haircut"]},{"emoji":"💇‍♂️","aliases":["haircut_man"]},{"emoji":"💇‍♀️","aliases":["haircut_woman"]},{"emoji":"🚶","aliases":["walking"]},{"emoji":"🚶‍♂️","aliases":["walking_man"]},{"emoji":"🚶‍♀️","aliases":["walking_woman"]},{"emoji":"🧍","aliases":["standing_person"]},{"emoji":"🧍‍♂️","aliases":["standing_man"]},{"emoji":"🧍‍♀️","aliases":["standing_woman"]},{"emoji":"🧎","aliases":["kneeling_person"]},{"emoji":"🧎‍♂️","aliases":["kneeling_man"]},{"emoji":"🧎‍♀️","aliases":["kneeling_woman"]},{"emoji":"🧑‍🦯","aliases":["person_with_probing_cane"]},{"emoji":"👨‍🦯","aliases":["man_with_probing_cane"]},{"emoji":"👩‍🦯","aliases":["woman_with_probing_cane"]},{"emoji":"🧑‍🦼","aliases":["person_in_motorized_wheelchair"]},{"emoji":"👨‍🦼","aliases":["man_in_motorized_wheelchair"]},{"emoji":"👩‍🦼","aliases":["woman_in_motorized_wheelchair"]},{"emoji":"🧑‍🦽","aliases":["person_in_manual_wheelchair"]},{"emoji":"👨‍🦽","aliases":["man_in_manual_wheelchair"]},{"emoji":"👩‍🦽","aliases":["woman_in_manual_wheelchair"]},{"emoji":"🏃","aliases":["runner","running"]},{"emoji":"🏃‍♂️","aliases":["running_man"]},{"emoji":"🏃‍♀️","aliases":["running_woman"]},{"emoji":"💃","aliases":["woman_dancing","dancer"]},{"emoji":"🕺","aliases":["man_dancing"]},{"emoji":"🕴️","aliases":["business_suit_levitating"]},{"emoji":"👯","aliases":["dancers"]},{"emoji":"👯‍♂️","aliases":["dancing_men"]},{"emoji":"👯‍♀️","aliases":["dancing_women"]},{"emoji":"🧖","aliases":["sauna_person"]},{"emoji":"🧖‍♂️","aliases":["sauna_man"]},{"emoji":"🧖‍♀️","aliases":["sauna_woman"]},{"emoji":"🧗","aliases":["climbing"]},{"emoji":"🧗‍♂️","aliases":["climbing_man"]},{"emoji":"🧗‍♀️","aliases":["climbing_woman"]},{"emoji":"🤺","aliases":["person_fencing"]},{"emoji":"🏇","aliases":["horse_racing"]},{"emoji":"⛷️","aliases":["skier"]},{"emoji":"🏂","aliases":["snowboarder"]},{"emoji":"🏌️","aliases":["golfing"]},{"emoji":"🏌️‍♂️","aliases":["golfing_man"]},{"emoji":"🏌️‍♀️","aliases":["golfing_woman"]},{"emoji":"🏄","aliases":["surfer"]},{"emoji":"🏄‍♂️","aliases":["surfing_man"]},{"emoji":"🏄‍♀️","aliases":["surfing_woman"]},{"emoji":"🚣","aliases":["rowboat"]},{"emoji":"🚣‍♂️","aliases":["rowing_man"]},{"emoji":"🚣‍♀️","aliases":["rowing_woman"]},{"emoji":"🏊","aliases":["swimmer"]},{"emoji":"🏊‍♂️","aliases":["swimming_man"]},{"emoji":"🏊‍♀️","aliases":["swimming_woman"]},{"emoji":"⛹️","aliases":["bouncing_ball_person"]},{"emoji":"⛹️‍♂️","aliases":["bouncing_ball_man","basketball_man"]},{"emoji":"⛹️‍♀️","aliases":["bouncing_ball_woman","basketball_woman"]},{"emoji":"🏋️","aliases":["weight_lifting"]},{"emoji":"🏋️‍♂️","aliases":["weight_lifting_man"]},{"emoji":"🏋️‍♀️","aliases":["weight_lifting_woman"]},{"emoji":"🚴","aliases":["bicyclist"]},{"emoji":"🚴‍♂️","aliases":["biking_man"]},{"emoji":"🚴‍♀️","aliases":["biking_woman"]},{"emoji":"🚵","aliases":["mountain_bicyclist"]},{"emoji":"🚵‍♂️","aliases":["mountain_biking_man"]},{"emoji":"🚵‍♀️","aliases":["mountain_biking_woman"]},{"emoji":"🤸","aliases":["cartwheeling"]},{"emoji":"🤸‍♂️","aliases":["man_cartwheeling"]},{"emoji":"🤸‍♀️","aliases":["woman_cartwheeling"]},{"emoji":"🤼","aliases":["wrestling"]},{"emoji":"🤼‍♂️","aliases":["men_wrestling"]},{"emoji":"🤼‍♀️","aliases":["women_wrestling"]},{"emoji":"🤽","aliases":["water_polo"]},{"emoji":"🤽‍♂️","aliases":["man_playing_water_polo"]},{"emoji":"🤽‍♀️","aliases":["woman_playing_water_polo"]},{"emoji":"🤾","aliases":["handball_person"]},{"emoji":"🤾‍♂️","aliases":["man_playing_handball"]},{"emoji":"🤾‍♀️","aliases":["woman_playing_handball"]},{"emoji":"🤹","aliases":["juggling_person"]},{"emoji":"🤹‍♂️","aliases":["man_juggling"]},{"emoji":"🤹‍♀️","aliases":["woman_juggling"]},{"emoji":"🧘","aliases":["lotus_position"]},{"emoji":"🧘‍♂️","aliases":["lotus_position_man"]},{"emoji":"🧘‍♀️","aliases":["lotus_position_woman"]},{"emoji":"🛀","aliases":["bath"]},{"emoji":"🛌","aliases":["sleeping_bed"]},{"emoji":"🧑‍🤝‍🧑","aliases":["people_holding_hands"]},{"emoji":"👭","aliases":["two_women_holding_hands"]},{"emoji":"👫","aliases":["couple"]},{"emoji":"👬","aliases":["two_men_holding_hands"]},{"emoji":"💏","aliases":["couplekiss"]},{"emoji":"👩‍❤️‍💋‍👨","aliases":["couplekiss_man_woman"]},{"emoji":"👨‍❤️‍💋‍👨","aliases":["couplekiss_man_man"]},{"emoji":"👩‍❤️‍💋‍👩","aliases":["couplekiss_woman_woman"]},{"emoji":"💑","aliases":["couple_with_heart"]},{"emoji":"👩‍❤️‍👨","aliases":["couple_with_heart_woman_man"]},{"emoji":"👨‍❤️‍👨","aliases":["couple_with_heart_man_man"]},{"emoji":"👩‍❤️‍👩","aliases":["couple_with_heart_woman_woman"]},{"emoji":"👪","aliases":["family"]},{"emoji":"👨‍👩‍👦","aliases":["family_man_woman_boy"]},{"emoji":"👨‍👩‍👧","aliases":["family_man_woman_girl"]},{"emoji":"👨‍👩‍👧‍👦","aliases":["family_man_woman_girl_boy"]},{"emoji":"👨‍👩‍👦‍👦","aliases":["family_man_woman_boy_boy"]},{"emoji":"👨‍👩‍👧‍👧","aliases":["family_man_woman_girl_girl"]},{"emoji":"👨‍👨‍👦","aliases":["family_man_man_boy"]},{"emoji":"👨‍👨‍👧","aliases":["family_man_man_girl"]},{"emoji":"👨‍👨‍👧‍👦","aliases":["family_man_man_girl_boy"]},{"emoji":"👨‍👨‍👦‍👦","aliases":["family_man_man_boy_boy"]},{"emoji":"👨‍👨‍👧‍👧","aliases":["family_man_man_girl_girl"]},{"emoji":"👩‍👩‍👦","aliases":["family_woman_woman_boy"]},{"emoji":"👩‍👩‍👧","aliases":["family_woman_woman_girl"]},{"emoji":"👩‍👩‍👧‍👦","aliases":["family_woman_woman_girl_boy"]},{"emoji":"👩‍👩‍👦‍👦","aliases":["family_woman_woman_boy_boy"]},{"emoji":"👩‍👩‍👧‍👧","aliases":["family_woman_woman_girl_girl"]},{"emoji":"👨‍👦","aliases":["family_man_boy"]},{"emoji":"👨‍👦‍👦","aliases":["family_man_boy_boy"]},{"emoji":"👨‍👧","aliases":["family_man_girl"]},{"emoji":"👨‍👧‍👦","aliases":["family_man_girl_boy"]},{"emoji":"👨‍👧‍👧","aliases":["family_man_girl_girl"]},{"emoji":"👩‍👦","aliases":["family_woman_boy"]},{"emoji":"👩‍👦‍👦","aliases":["family_woman_boy_boy"]},{"emoji":"👩‍👧","aliases":["family_woman_girl"]},{"emoji":"👩‍👧‍👦","aliases":["family_woman_girl_boy"]},{"emoji":"👩‍👧‍👧","aliases":["family_woman_girl_girl"]},{"emoji":"🗣️","aliases":["speaking_head"]},{"emoji":"👤","aliases":["bust_in_silhouette"]},{"emoji":"👥","aliases":["busts_in_silhouette"]},{"emoji":"🫂","aliases":["people_hugging"]},{"emoji":"👣","aliases":["footprints"]},{"emoji":"🐵","aliases":["monkey_face"]},{"emoji":"🐒","aliases":["monkey"]},{"emoji":"🦍","aliases":["gorilla"]},{"emoji":"🦧","aliases":["orangutan"]},{"emoji":"🐶","aliases":["dog"]},{"emoji":"🐕","aliases":["dog2"]},{"emoji":"🦮","aliases":["guide_dog"]},{"emoji":"🐕‍🦺","aliases":["service_dog"]},{"emoji":"🐩","aliases":["poodle"]},{"emoji":"🐺","aliases":["wolf"]},{"emoji":"🦊","aliases":["fox_face"]},{"emoji":"🦝","aliases":["raccoon"]},{"emoji":"🐱","aliases":["cat"]},{"emoji":"🐈","aliases":["cat2"]},{"emoji":"🐈‍⬛","aliases":["black_cat"]},{"emoji":"🦁","aliases":["lion"]},{"emoji":"🐯","aliases":["tiger"]},{"emoji":"🐅","aliases":["tiger2"]},{"emoji":"🐆","aliases":["leopard"]},{"emoji":"🐴","aliases":["horse"]},{"emoji":"🐎","aliases":["racehorse"]},{"emoji":"🦄","aliases":["unicorn"]},{"emoji":"🦓","aliases":["zebra"]},{"emoji":"🦌","aliases":["deer"]},{"emoji":"🦬","aliases":["bison"]},{"emoji":"🐮","aliases":["cow"]},{"emoji":"🐂","aliases":["ox"]},{"emoji":"🐃","aliases":["water_buffalo"]},{"emoji":"🐄","aliases":["cow2"]},{"emoji":"🐷","aliases":["pig"]},{"emoji":"🐖","aliases":["pig2"]},{"emoji":"🐗","aliases":["boar"]},{"emoji":"🐽","aliases":["pig_nose"]},{"emoji":"🐏","aliases":["ram"]},{"emoji":"🐑","aliases":["sheep"]},{"emoji":"🐐","aliases":["goat"]},{"emoji":"🐪","aliases":["dromedary_camel"]},{"emoji":"🐫","aliases":["camel"]},{"emoji":"🦙","aliases":["llama"]},{"emoji":"🦒","aliases":["giraffe"]},{"emoji":"🐘","aliases":["elephant"]},{"emoji":"🦣","aliases":["mammoth"]},{"emoji":"🦏","aliases":["rhinoceros"]},{"emoji":"🦛","aliases":["hippopotamus"]},{"emoji":"🐭","aliases":["mouse"]},{"emoji":"🐁","aliases":["mouse2"]},{"emoji":"🐀","aliases":["rat"]},{"emoji":"🐹","aliases":["hamster"]},{"emoji":"🐰","aliases":["rabbit"]},{"emoji":"🐇","aliases":["rabbit2"]},{"emoji":"🐿️","aliases":["chipmunk"]},{"emoji":"🦫","aliases":["beaver"]},{"emoji":"🦔","aliases":["hedgehog"]},{"emoji":"🦇","aliases":["bat"]},{"emoji":"🐻","aliases":["bear"]},{"emoji":"🐻‍❄️","aliases":["polar_bear"]},{"emoji":"🐨","aliases":["koala"]},{"emoji":"🐼","aliases":["panda_face"]},{"emoji":"🦥","aliases":["sloth"]},{"emoji":"🦦","aliases":["otter"]},{"emoji":"🦨","aliases":["skunk"]},{"emoji":"🦘","aliases":["kangaroo"]},{"emoji":"🦡","aliases":["badger"]},{"emoji":"🐾","aliases":["feet","paw_prints"]},{"emoji":"🦃","aliases":["turkey"]},{"emoji":"🐔","aliases":["chicken"]},{"emoji":"🐓","aliases":["rooster"]},{"emoji":"🐣","aliases":["hatching_chick"]},{"emoji":"🐤","aliases":["baby_chick"]},{"emoji":"🐥","aliases":["hatched_chick"]},{"emoji":"🐦","aliases":["bird"]},{"emoji":"🐧","aliases":["penguin"]},{"emoji":"🕊️","aliases":["dove"]},{"emoji":"🦅","aliases":["eagle"]},{"emoji":"🦆","aliases":["duck"]},{"emoji":"🦢","aliases":["swan"]},{"emoji":"🦉","aliases":["owl"]},{"emoji":"🦤","aliases":["dodo"]},{"emoji":"🪶","aliases":["feather"]},{"emoji":"🦩","aliases":["flamingo"]},{"emoji":"🦚","aliases":["peacock"]},{"emoji":"🦜","aliases":["parrot"]},{"emoji":"🐸","aliases":["frog"]},{"emoji":"🐊","aliases":["crocodile"]},{"emoji":"🐢","aliases":["turtle"]},{"emoji":"🦎","aliases":["lizard"]},{"emoji":"🐍","aliases":["snake"]},{"emoji":"🐲","aliases":["dragon_face"]},{"emoji":"🐉","aliases":["dragon"]},{"emoji":"🦕","aliases":["sauropod"]},{"emoji":"🦖","aliases":["t-rex"]},{"emoji":"🐳","aliases":["whale"]},{"emoji":"🐋","aliases":["whale2"]},{"emoji":"🐬","aliases":["dolphin","flipper"]},{"emoji":"🦭","aliases":["seal"]},{"emoji":"🐟","aliases":["fish"]},{"emoji":"🐠","aliases":["tropical_fish"]},{"emoji":"🐡","aliases":["blowfish"]},{"emoji":"🦈","aliases":["shark"]},{"emoji":"🐙","aliases":["octopus"]},{"emoji":"🐚","aliases":["shell"]},{"emoji":"🐌","aliases":["snail"]},{"emoji":"🦋","aliases":["butterfly"]},{"emoji":"🐛","aliases":["bug"]},{"emoji":"🐜","aliases":["ant"]},{"emoji":"🐝","aliases":["bee","honeybee"]},{"emoji":"🪲","aliases":["beetle"]},{"emoji":"🐞","aliases":["lady_beetle"]},{"emoji":"🦗","aliases":["cricket"]},{"emoji":"🪳","aliases":["cockroach"]},{"emoji":"🕷️","aliases":["spider"]},{"emoji":"🕸️","aliases":["spider_web"]},{"emoji":"🦂","aliases":["scorpion"]},{"emoji":"🦟","aliases":["mosquito"]},{"emoji":"🪰","aliases":["fly"]},{"emoji":"🪱","aliases":["worm"]},{"emoji":"🦠","aliases":["microbe"]},{"emoji":"💐","aliases":["bouquet"]},{"emoji":"🌸","aliases":["cherry_blossom"]},{"emoji":"💮","aliases":["white_flower"]},{"emoji":"🏵️","aliases":["rosette"]},{"emoji":"🌹","aliases":["rose"]},{"emoji":"🥀","aliases":["wilted_flower"]},{"emoji":"🌺","aliases":["hibiscus"]},{"emoji":"🌻","aliases":["sunflower"]},{"emoji":"🌼","aliases":["blossom"]},{"emoji":"🌷","aliases":["tulip"]},{"emoji":"🌱","aliases":["seedling"]},{"emoji":"🪴","aliases":["potted_plant"]},{"emoji":"🌲","aliases":["evergreen_tree"]},{"emoji":"🌳","aliases":["deciduous_tree"]},{"emoji":"🌴","aliases":["palm_tree"]},{"emoji":"🌵","aliases":["cactus"]},{"emoji":"🌾","aliases":["ear_of_rice"]},{"emoji":"🌿","aliases":["herb"]},{"emoji":"☘️","aliases":["shamrock"]},{"emoji":"🍀","aliases":["four_leaf_clover"]},{"emoji":"🍁","aliases":["maple_leaf"]},{"emoji":"🍂","aliases":["fallen_leaf"]},{"emoji":"🍃","aliases":["leaves"]},{"emoji":"🍇","aliases":["grapes"]},{"emoji":"🍈","aliases":["melon"]},{"emoji":"🍉","aliases":["watermelon"]},{"emoji":"🍊","aliases":["tangerine","orange","mandarin"]},{"emoji":"🍋","aliases":["lemon"]},{"emoji":"🍌","aliases":["banana"]},{"emoji":"🍍","aliases":["pineapple"]},{"emoji":"🥭","aliases":["mango"]},{"emoji":"🍎","aliases":["apple"]},{"emoji":"🍏","aliases":["green_apple"]},{"emoji":"🍐","aliases":["pear"]},{"emoji":"🍑","aliases":["peach"]},{"emoji":"🍒","aliases":["cherries"]},{"emoji":"🍓","aliases":["strawberry"]},{"emoji":"🫐","aliases":["blueberries"]},{"emoji":"🥝","aliases":["kiwi_fruit"]},{"emoji":"🍅","aliases":["tomato"]},{"emoji":"🫒","aliases":["olive"]},{"emoji":"🥥","aliases":["coconut"]},{"emoji":"🥑","aliases":["avocado"]},{"emoji":"🍆","aliases":["eggplant"]},{"emoji":"🥔","aliases":["potato"]},{"emoji":"🥕","aliases":["carrot"]},{"emoji":"🌽","aliases":["corn"]},{"emoji":"🌶️","aliases":["hot_pepper"]},{"emoji":"🫑","aliases":["bell_pepper"]},{"emoji":"🥒","aliases":["cucumber"]},{"emoji":"🥬","aliases":["leafy_green"]},{"emoji":"🥦","aliases":["broccoli"]},{"emoji":"🧄","aliases":["garlic"]},{"emoji":"🧅","aliases":["onion"]},{"emoji":"🍄","aliases":["mushroom"]},{"emoji":"🥜","aliases":["peanuts"]},{"emoji":"🌰","aliases":["chestnut"]},{"emoji":"🍞","aliases":["bread"]},{"emoji":"🥐","aliases":["croissant"]},{"emoji":"🥖","aliases":["baguette_bread"]},{"emoji":"🫓","aliases":["flatbread"]},{"emoji":"🥨","aliases":["pretzel"]},{"emoji":"🥯","aliases":["bagel"]},{"emoji":"🥞","aliases":["pancakes"]},{"emoji":"🧇","aliases":["waffle"]},{"emoji":"🧀","aliases":["cheese"]},{"emoji":"🍖","aliases":["meat_on_bone"]},{"emoji":"🍗","aliases":["poultry_leg"]},{"emoji":"🥩","aliases":["cut_of_meat"]},{"emoji":"🥓","aliases":["bacon"]},{"emoji":"🍔","aliases":["hamburger"]},{"emoji":"🍟","aliases":["fries"]},{"emoji":"🍕","aliases":["pizza"]},{"emoji":"🌭","aliases":["hotdog"]},{"emoji":"🥪","aliases":["sandwich"]},{"emoji":"🌮","aliases":["taco"]},{"emoji":"🌯","aliases":["burrito"]},{"emoji":"🫔","aliases":["tamale"]},{"emoji":"🥙","aliases":["stuffed_flatbread"]},{"emoji":"🧆","aliases":["falafel"]},{"emoji":"🥚","aliases":["egg"]},{"emoji":"🍳","aliases":["fried_egg"]},{"emoji":"🥘","aliases":["shallow_pan_of_food"]},{"emoji":"🍲","aliases":["stew"]},{"emoji":"🫕","aliases":["fondue"]},{"emoji":"🥣","aliases":["bowl_with_spoon"]},{"emoji":"🥗","aliases":["green_salad"]},{"emoji":"🍿","aliases":["popcorn"]},{"emoji":"🧈","aliases":["butter"]},{"emoji":"🧂","aliases":["salt"]},{"emoji":"🥫","aliases":["canned_food"]},{"emoji":"🍱","aliases":["bento"]},{"emoji":"🍘","aliases":["rice_cracker"]},{"emoji":"🍙","aliases":["rice_ball"]},{"emoji":"🍚","aliases":["rice"]},{"emoji":"🍛","aliases":["curry"]},{"emoji":"🍜","aliases":["ramen"]},{"emoji":"🍝","aliases":["spaghetti"]},{"emoji":"🍠","aliases":["sweet_potato"]},{"emoji":"🍢","aliases":["oden"]},{"emoji":"🍣","aliases":["sushi"]},{"emoji":"🍤","aliases":["fried_shrimp"]},{"emoji":"🍥","aliases":["fish_cake"]},{"emoji":"🥮","aliases":["moon_cake"]},{"emoji":"🍡","aliases":["dango"]},{"emoji":"🥟","aliases":["dumpling"]},{"emoji":"🥠","aliases":["fortune_cookie"]},{"emoji":"🥡","aliases":["takeout_box"]},{"emoji":"🦀","aliases":["crab"]},{"emoji":"🦞","aliases":["lobster"]},{"emoji":"🦐","aliases":["shrimp"]},{"emoji":"🦑","aliases":["squid"]},{"emoji":"🦪","aliases":["oyster"]},{"emoji":"🍦","aliases":["icecream"]},{"emoji":"🍧","aliases":["shaved_ice"]},{"emoji":"🍨","aliases":["ice_cream"]},{"emoji":"🍩","aliases":["doughnut"]},{"emoji":"🍪","aliases":["cookie"]},{"emoji":"🎂","aliases":["birthday"]},{"emoji":"🍰","aliases":["cake"]},{"emoji":"🧁","aliases":["cupcake"]},{"emoji":"🥧","aliases":["pie"]},{"emoji":"🍫","aliases":["chocolate_bar"]},{"emoji":"🍬","aliases":["candy"]},{"emoji":"🍭","aliases":["lollipop"]},{"emoji":"🍮","aliases":["custard"]},{"emoji":"🍯","aliases":["honey_pot"]},{"emoji":"🍼","aliases":["baby_bottle"]},{"emoji":"🥛","aliases":["milk_glass"]},{"emoji":"☕","aliases":["coffee"]},{"emoji":"🫖","aliases":["teapot"]},{"emoji":"🍵","aliases":["tea"]},{"emoji":"🍶","aliases":["sake"]},{"emoji":"🍾","aliases":["champagne"]},{"emoji":"🍷","aliases":["wine_glass"]},{"emoji":"🍸","aliases":["cocktail"]},{"emoji":"🍹","aliases":["tropical_drink"]},{"emoji":"🍺","aliases":["beer"]},{"emoji":"🍻","aliases":["beers"]},{"emoji":"🥂","aliases":["clinking_glasses"]},{"emoji":"🥃","aliases":["tumbler_glass"]},{"emoji":"🥤","aliases":["cup_with_straw"]},{"emoji":"🧋","aliases":["bubble_tea"]},{"emoji":"🧃","aliases":["beverage_box"]},{"emoji":"🧉","aliases":["mate"]},{"emoji":"🧊","aliases":["ice_cube"]},{"emoji":"🥢","aliases":["chopsticks"]},{"emoji":"🍽️","aliases":["plate_with_cutlery"]},{"emoji":"🍴","aliases":["fork_and_knife"]},{"emoji":"🥄","aliases":["spoon"]},{"emoji":"🔪","aliases":["hocho","knife"]},{"emoji":"🏺","aliases":["amphora"]},{"emoji":"🌍","aliases":["earth_africa"]},{"emoji":"🌎","aliases":["earth_americas"]},{"emoji":"🌏","aliases":["earth_asia"]},{"emoji":"🌐","aliases":["globe_with_meridians"]},{"emoji":"🗺️","aliases":["world_map"]},{"emoji":"🗾","aliases":["japan"]},{"emoji":"🧭","aliases":["compass"]},{"emoji":"🏔️","aliases":["mountain_snow"]},{"emoji":"⛰️","aliases":["mountain"]},{"emoji":"🌋","aliases":["volcano"]},{"emoji":"🗻","aliases":["mount_fuji"]},{"emoji":"🏕️","aliases":["camping"]},{"emoji":"🏖️","aliases":["beach_umbrella"]},{"emoji":"🏜️","aliases":["desert"]},{"emoji":"🏝️","aliases":["desert_island"]},{"emoji":"🏞️","aliases":["national_park"]},{"emoji":"🏟️","aliases":["stadium"]},{"emoji":"🏛️","aliases":["classical_building"]},{"emoji":"🏗️","aliases":["building_construction"]},{"emoji":"🧱","aliases":["bricks"]},{"emoji":"🪨","aliases":["rock"]},{"emoji":"🪵","aliases":["wood"]},{"emoji":"🛖","aliases":["hut"]},{"emoji":"🏘️","aliases":["houses"]},{"emoji":"🏚️","aliases":["derelict_house"]},{"emoji":"🏠","aliases":["house"]},{"emoji":"🏡","aliases":["house_with_garden"]},{"emoji":"🏢","aliases":["office"]},{"emoji":"🏣","aliases":["post_office"]},{"emoji":"🏤","aliases":["european_post_office"]},{"emoji":"🏥","aliases":["hospital"]},{"emoji":"🏦","aliases":["bank"]},{"emoji":"🏨","aliases":["hotel"]},{"emoji":"🏩","aliases":["love_hotel"]},{"emoji":"🏪","aliases":["convenience_store"]},{"emoji":"🏫","aliases":["school"]},{"emoji":"🏬","aliases":["department_store"]},{"emoji":"🏭","aliases":["factory"]},{"emoji":"🏯","aliases":["japanese_castle"]},{"emoji":"🏰","aliases":["european_castle"]},{"emoji":"💒","aliases":["wedding"]},{"emoji":"🗼","aliases":["tokyo_tower"]},{"emoji":"🗽","aliases":["statue_of_liberty"]},{"emoji":"⛪","aliases":["church"]},{"emoji":"🕌","aliases":["mosque"]},{"emoji":"🛕","aliases":["hindu_temple"]},{"emoji":"🕍","aliases":["synagogue"]},{"emoji":"⛩️","aliases":["shinto_shrine"]},{"emoji":"🕋","aliases":["kaaba"]},{"emoji":"⛲","aliases":["fountain"]},{"emoji":"⛺","aliases":["tent"]},{"emoji":"🌁","aliases":["foggy"]},{"emoji":"🌃","aliases":["night_with_stars"]},{"emoji":"🏙️","aliases":["cityscape"]},{"emoji":"🌄","aliases":["sunrise_over_mountains"]},{"emoji":"🌅","aliases":["sunrise"]},{"emoji":"🌆","aliases":["city_sunset"]},{"emoji":"🌇","aliases":["city_sunrise"]},{"emoji":"🌉","aliases":["bridge_at_night"]},{"emoji":"♨️","aliases":["hotsprings"]},{"emoji":"🎠","aliases":["carousel_horse"]},{"emoji":"🎡","aliases":["ferris_wheel"]},{"emoji":"🎢","aliases":["roller_coaster"]},{"emoji":"💈","aliases":["barber"]},{"emoji":"🎪","aliases":["circus_tent"]},{"emoji":"🚂","aliases":["steam_locomotive"]},{"emoji":"🚃","aliases":["railway_car"]},{"emoji":"🚄","aliases":["bullettrain_side"]},{"emoji":"🚅","aliases":["bullettrain_front"]},{"emoji":"🚆","aliases":["train2"]},{"emoji":"🚇","aliases":["metro"]},{"emoji":"🚈","aliases":["light_rail"]},{"emoji":"🚉","aliases":["station"]},{"emoji":"🚊","aliases":["tram"]},{"emoji":"🚝","aliases":["monorail"]},{"emoji":"🚞","aliases":["mountain_railway"]},{"emoji":"🚋","aliases":["train"]},{"emoji":"🚌","aliases":["bus"]},{"emoji":"🚍","aliases":["oncoming_bus"]},{"emoji":"🚎","aliases":["trolleybus"]},{"emoji":"🚐","aliases":["minibus"]},{"emoji":"🚑","aliases":["ambulance"]},{"emoji":"🚒","aliases":["fire_engine"]},{"emoji":"🚓","aliases":["police_car"]},{"emoji":"🚔","aliases":["oncoming_police_car"]},{"emoji":"🚕","aliases":["taxi"]},{"emoji":"🚖","aliases":["oncoming_taxi"]},{"emoji":"🚗","aliases":["car","red_car"]},{"emoji":"🚘","aliases":["oncoming_automobile"]},{"emoji":"🚙","aliases":["blue_car"]},{"emoji":"🛻","aliases":["pickup_truck"]},{"emoji":"🚚","aliases":["truck"]},{"emoji":"🚛","aliases":["articulated_lorry"]},{"emoji":"🚜","aliases":["tractor"]},{"emoji":"🏎️","aliases":["racing_car"]},{"emoji":"🏍️","aliases":["motorcycle"]},{"emoji":"🛵","aliases":["motor_scooter"]},{"emoji":"🦽","aliases":["manual_wheelchair"]},{"emoji":"🦼","aliases":["motorized_wheelchair"]},{"emoji":"🛺","aliases":["auto_rickshaw"]},{"emoji":"🚲","aliases":["bike"]},{"emoji":"🛴","aliases":["kick_scooter"]},{"emoji":"🛹","aliases":["skateboard"]},{"emoji":"🛼","aliases":["roller_skate"]},{"emoji":"🚏","aliases":["busstop"]},{"emoji":"🛣️","aliases":["motorway"]},{"emoji":"🛤️","aliases":["railway_track"]},{"emoji":"🛢️","aliases":["oil_drum"]},{"emoji":"⛽","aliases":["fuelpump"]},{"emoji":"🚨","aliases":["rotating_light"]},{"emoji":"🚥","aliases":["traffic_light"]},{"emoji":"🚦","aliases":["vertical_traffic_light"]},{"emoji":"🛑","aliases":["stop_sign"]},{"emoji":"🚧","aliases":["construction"]},{"emoji":"⚓","aliases":["anchor"]},{"emoji":"⛵","aliases":["boat","sailboat"]},{"emoji":"🛶","aliases":["canoe"]},{"emoji":"🚤","aliases":["speedboat"]},{"emoji":"🛳️","aliases":["passenger_ship"]},{"emoji":"⛴️","aliases":["ferry"]},{"emoji":"🛥️","aliases":["motor_boat"]},{"emoji":"🚢","aliases":["ship"]},{"emoji":"✈️","aliases":["airplane"]},{"emoji":"🛩️","aliases":["small_airplane"]},{"emoji":"🛫","aliases":["flight_departure"]},{"emoji":"🛬","aliases":["flight_arrival"]},{"emoji":"🪂","aliases":["parachute"]},{"emoji":"💺","aliases":["seat"]},{"emoji":"🚁","aliases":["helicopter"]},{"emoji":"🚟","aliases":["suspension_railway"]},{"emoji":"🚠","aliases":["mountain_cableway"]},{"emoji":"🚡","aliases":["aerial_tramway"]},{"emoji":"🛰️","aliases":["artificial_satellite"]},{"emoji":"🚀","aliases":["rocket"]},{"emoji":"🛸","aliases":["flying_saucer"]},{"emoji":"🛎️","aliases":["bellhop_bell"]},{"emoji":"🧳","aliases":["luggage"]},{"emoji":"⌛","aliases":["hourglass"]},{"emoji":"⏳","aliases":["hourglass_flowing_sand"]},{"emoji":"⌚","aliases":["watch"]},{"emoji":"⏰","aliases":["alarm_clock"]},{"emoji":"⏱️","aliases":["stopwatch"]},{"emoji":"⏲️","aliases":["timer_clock"]},{"emoji":"🕰️","aliases":["mantelpiece_clock"]},{"emoji":"🕛","aliases":["clock12"]},{"emoji":"🕧","aliases":["clock1230"]},{"emoji":"🕐","aliases":["clock1"]},{"emoji":"🕜","aliases":["clock130"]},{"emoji":"🕑","aliases":["clock2"]},{"emoji":"🕝","aliases":["clock230"]},{"emoji":"🕒","aliases":["clock3"]},{"emoji":"🕞","aliases":["clock330"]},{"emoji":"🕓","aliases":["clock4"]},{"emoji":"🕟","aliases":["clock430"]},{"emoji":"🕔","aliases":["clock5"]},{"emoji":"🕠","aliases":["clock530"]},{"emoji":"🕕","aliases":["clock6"]},{"emoji":"🕡","aliases":["clock630"]},{"emoji":"🕖","aliases":["clock7"]},{"emoji":"🕢","aliases":["clock730"]},{"emoji":"🕗","aliases":["clock8"]},{"emoji":"🕣","aliases":["clock830"]},{"emoji":"🕘","aliases":["clock9"]},{"emoji":"🕤","aliases":["clock930"]},{"emoji":"🕙","aliases":["clock10"]},{"emoji":"🕥","aliases":["clock1030"]},{"emoji":"🕚","aliases":["clock11"]},{"emoji":"🕦","aliases":["clock1130"]},{"emoji":"🌑","aliases":["new_moon"]},{"emoji":"🌒","aliases":["waxing_crescent_moon"]},{"emoji":"🌓","aliases":["first_quarter_moon"]},{"emoji":"🌔","aliases":["moon","waxing_gibbous_moon"]},{"emoji":"🌕","aliases":["full_moon"]},{"emoji":"🌖","aliases":["waning_gibbous_moon"]},{"emoji":"🌗","aliases":["last_quarter_moon"]},{"emoji":"🌘","aliases":["waning_crescent_moon"]},{"emoji":"🌙","aliases":["crescent_moon"]},{"emoji":"🌚","aliases":["new_moon_with_face"]},{"emoji":"🌛","aliases":["first_quarter_moon_with_face"]},{"emoji":"🌜","aliases":["last_quarter_moon_with_face"]},{"emoji":"🌡️","aliases":["thermometer"]},{"emoji":"☀️","aliases":["sunny"]},{"emoji":"🌝","aliases":["full_moon_with_face"]},{"emoji":"🌞","aliases":["sun_with_face"]},{"emoji":"🪐","aliases":["ringed_planet"]},{"emoji":"⭐","aliases":["star"]},{"emoji":"🌟","aliases":["star2"]},{"emoji":"🌠","aliases":["stars"]},{"emoji":"🌌","aliases":["milky_way"]},{"emoji":"☁️","aliases":["cloud"]},{"emoji":"⛅","aliases":["partly_sunny"]},{"emoji":"⛈️","aliases":["cloud_with_lightning_and_rain"]},{"emoji":"🌤️","aliases":["sun_behind_small_cloud"]},{"emoji":"🌥️","aliases":["sun_behind_large_cloud"]},{"emoji":"🌦️","aliases":["sun_behind_rain_cloud"]},{"emoji":"🌧️","aliases":["cloud_with_rain"]},{"emoji":"🌨️","aliases":["cloud_with_snow"]},{"emoji":"🌩️","aliases":["cloud_with_lightning"]},{"emoji":"🌪️","aliases":["tornado"]},{"emoji":"🌫️","aliases":["fog"]},{"emoji":"🌬️","aliases":["wind_face"]},{"emoji":"🌀","aliases":["cyclone"]},{"emoji":"🌈","aliases":["rainbow"]},{"emoji":"🌂","aliases":["closed_umbrella"]},{"emoji":"☂️","aliases":["open_umbrella"]},{"emoji":"☔","aliases":["umbrella"]},{"emoji":"⛱️","aliases":["parasol_on_ground"]},{"emoji":"⚡","aliases":["zap"]},{"emoji":"❄️","aliases":["snowflake"]},{"emoji":"☃️","aliases":["snowman_with_snow"]},{"emoji":"⛄","aliases":["snowman"]},{"emoji":"☄️","aliases":["comet"]},{"emoji":"🔥","aliases":["fire"]},{"emoji":"💧","aliases":["droplet"]},{"emoji":"🌊","aliases":["ocean"]},{"emoji":"🎃","aliases":["jack_o_lantern"]},{"emoji":"🎄","aliases":["christmas_tree"]},{"emoji":"🎆","aliases":["fireworks"]},{"emoji":"🎇","aliases":["sparkler"]},{"emoji":"🧨","aliases":["firecracker"]},{"emoji":"✨","aliases":["sparkles"]},{"emoji":"🎈","aliases":["balloon"]},{"emoji":"🎉","aliases":["tada"]},{"emoji":"🎊","aliases":["confetti_ball"]},{"emoji":"🎋","aliases":["tanabata_tree"]},{"emoji":"🎍","aliases":["bamboo"]},{"emoji":"🎎","aliases":["dolls"]},{"emoji":"🎏","aliases":["flags"]},{"emoji":"🎐","aliases":["wind_chime"]},{"emoji":"🎑","aliases":["rice_scene"]},{"emoji":"🧧","aliases":["red_envelope"]},{"emoji":"🎀","aliases":["ribbon"]},{"emoji":"🎁","aliases":["gift"]},{"emoji":"🎗️","aliases":["reminder_ribbon"]},{"emoji":"🎟️","aliases":["tickets"]},{"emoji":"🎫","aliases":["ticket"]},{"emoji":"🎖️","aliases":["medal_military"]},{"emoji":"🏆","aliases":["trophy"]},{"emoji":"🏅","aliases":["medal_sports"]},{"emoji":"🥇","aliases":["1st_place_medal"]},{"emoji":"🥈","aliases":["2nd_place_medal"]},{"emoji":"🥉","aliases":["3rd_place_medal"]},{"emoji":"⚽","aliases":["soccer"]},{"emoji":"⚾","aliases":["baseball"]},{"emoji":"🥎","aliases":["softball"]},{"emoji":"🏀","aliases":["basketball"]},{"emoji":"🏐","aliases":["volleyball"]},{"emoji":"🏈","aliases":["football"]},{"emoji":"🏉","aliases":["rugby_football"]},{"emoji":"🎾","aliases":["tennis"]},{"emoji":"🥏","aliases":["flying_disc"]},{"emoji":"🎳","aliases":["bowling"]},{"emoji":"🏏","aliases":["cricket_game"]},{"emoji":"🏑","aliases":["field_hockey"]},{"emoji":"🏒","aliases":["ice_hockey"]},{"emoji":"🥍","aliases":["lacrosse"]},{"emoji":"🏓","aliases":["ping_pong"]},{"emoji":"🏸","aliases":["badminton"]},{"emoji":"🥊","aliases":["boxing_glove"]},{"emoji":"🥋","aliases":["martial_arts_uniform"]},{"emoji":"🥅","aliases":["goal_net"]},{"emoji":"⛳","aliases":["golf"]},{"emoji":"⛸️","aliases":["ice_skate"]},{"emoji":"🎣","aliases":["fishing_pole_and_fish"]},{"emoji":"🤿","aliases":["diving_mask"]},{"emoji":"🎽","aliases":["running_shirt_with_sash"]},{"emoji":"🎿","aliases":["ski"]},{"emoji":"🛷","aliases":["sled"]},{"emoji":"🥌","aliases":["curling_stone"]},{"emoji":"🎯","aliases":["dart"]},{"emoji":"🪀","aliases":["yo_yo"]},{"emoji":"🪁","aliases":["kite"]},{"emoji":"🎱","aliases":["8ball"]},{"emoji":"🔮","aliases":["crystal_ball"]},{"emoji":"🪄","aliases":["magic_wand"]},{"emoji":"🧿","aliases":["nazar_amulet"]},{"emoji":"🎮","aliases":["video_game"]},{"emoji":"🕹️","aliases":["joystick"]},{"emoji":"🎰","aliases":["slot_machine"]},{"emoji":"🎲","aliases":["game_die"]},{"emoji":"🧩","aliases":["jigsaw"]},{"emoji":"🧸","aliases":["teddy_bear"]},{"emoji":"🪅","aliases":["pinata"]},{"emoji":"🪆","aliases":["nesting_dolls"]},{"emoji":"♠️","aliases":["spades"]},{"emoji":"♥️","aliases":["hearts"]},{"emoji":"♦️","aliases":["diamonds"]},{"emoji":"♣️","aliases":["clubs"]},{"emoji":"♟️","aliases":["chess_pawn"]},{"emoji":"🃏","aliases":["black_joker"]},{"emoji":"🀄","aliases":["mahjong"]},{"emoji":"🎴","aliases":["flower_playing_cards"]},{"emoji":"🎭","aliases":["performing_arts"]},{"emoji":"🖼️","aliases":["framed_picture"]},{"emoji":"🎨","aliases":["art"]},{"emoji":"🧵","aliases":["thread"]},{"emoji":"🪡","aliases":["sewing_needle"]},{"emoji":"🧶","aliases":["yarn"]},{"emoji":"🪢","aliases":["knot"]},{"emoji":"👓","aliases":["eyeglasses"]},{"emoji":"🕶️","aliases":["dark_sunglasses"]},{"emoji":"🥽","aliases":["goggles"]},{"emoji":"🥼","aliases":["lab_coat"]},{"emoji":"🦺","aliases":["safety_vest"]},{"emoji":"👔","aliases":["necktie"]},{"emoji":"👕","aliases":["shirt","tshirt"]},{"emoji":"👖","aliases":["jeans"]},{"emoji":"🧣","aliases":["scarf"]},{"emoji":"🧤","aliases":["gloves"]},{"emoji":"🧥","aliases":["coat"]},{"emoji":"🧦","aliases":["socks"]},{"emoji":"👗","aliases":["dress"]},{"emoji":"👘","aliases":["kimono"]},{"emoji":"🥻","aliases":["sari"]},{"emoji":"🩱","aliases":["one_piece_swimsuit"]},{"emoji":"🩲","aliases":["swim_brief"]},{"emoji":"🩳","aliases":["shorts"]},{"emoji":"👙","aliases":["bikini"]},{"emoji":"👚","aliases":["womans_clothes"]},{"emoji":"👛","aliases":["purse"]},{"emoji":"👜","aliases":["handbag"]},{"emoji":"👝","aliases":["pouch"]},{"emoji":"🛍️","aliases":["shopping"]},{"emoji":"🎒","aliases":["school_satchel"]},{"emoji":"🩴","aliases":["thong_sandal"]},{"emoji":"👞","aliases":["mans_shoe","shoe"]},{"emoji":"👟","aliases":["athletic_shoe"]},{"emoji":"🥾","aliases":["hiking_boot"]},{"emoji":"🥿","aliases":["flat_shoe"]},{"emoji":"👠","aliases":["high_heel"]},{"emoji":"👡","aliases":["sandal"]},{"emoji":"🩰","aliases":["ballet_shoes"]},{"emoji":"👢","aliases":["boot"]},{"emoji":"👑","aliases":["crown"]},{"emoji":"👒","aliases":["womans_hat"]},{"emoji":"🎩","aliases":["tophat"]},{"emoji":"🎓","aliases":["mortar_board"]},{"emoji":"🧢","aliases":["billed_cap"]},{"emoji":"🪖","aliases":["military_helmet"]},{"emoji":"⛑️","aliases":["rescue_worker_helmet"]},{"emoji":"📿","aliases":["prayer_beads"]},{"emoji":"💄","aliases":["lipstick"]},{"emoji":"💍","aliases":["ring"]},{"emoji":"💎","aliases":["gem"]},{"emoji":"🔇","aliases":["mute"]},{"emoji":"🔈","aliases":["speaker"]},{"emoji":"🔉","aliases":["sound"]},{"emoji":"🔊","aliases":["loud_sound"]},{"emoji":"📢","aliases":["loudspeaker"]},{"emoji":"📣","aliases":["mega"]},{"emoji":"📯","aliases":["postal_horn"]},{"emoji":"🔔","aliases":["bell"]},{"emoji":"🔕","aliases":["no_bell"]},{"emoji":"🎼","aliases":["musical_score"]},{"emoji":"🎵","aliases":["musical_note"]},{"emoji":"🎶","aliases":["notes"]},{"emoji":"🎙️","aliases":["studio_microphone"]},{"emoji":"🎚️","aliases":["level_slider"]},{"emoji":"🎛️","aliases":["control_knobs"]},{"emoji":"🎤","aliases":["microphone"]},{"emoji":"🎧","aliases":["headphones"]},{"emoji":"📻","aliases":["radio"]},{"emoji":"🎷","aliases":["saxophone"]},{"emoji":"🪗","aliases":["accordion"]},{"emoji":"🎸","aliases":["guitar"]},{"emoji":"🎹","aliases":["musical_keyboard"]},{"emoji":"🎺","aliases":["trumpet"]},{"emoji":"🎻","aliases":["violin"]},{"emoji":"🪕","aliases":["banjo"]},{"emoji":"🥁","aliases":["drum"]},{"emoji":"🪘","aliases":["long_drum"]},{"emoji":"📱","aliases":["iphone"]},{"emoji":"📲","aliases":["calling"]},{"emoji":"☎️","aliases":["phone","telephone"]},{"emoji":"📞","aliases":["telephone_receiver"]},{"emoji":"📟","aliases":["pager"]},{"emoji":"📠","aliases":["fax"]},{"emoji":"🔋","aliases":["battery"]},{"emoji":"🔌","aliases":["electric_plug"]},{"emoji":"💻","aliases":["computer"]},{"emoji":"🖥️","aliases":["desktop_computer"]},{"emoji":"🖨️","aliases":["printer"]},{"emoji":"⌨️","aliases":["keyboard"]},{"emoji":"🖱️","aliases":["computer_mouse"]},{"emoji":"🖲️","aliases":["trackball"]},{"emoji":"💽","aliases":["minidisc"]},{"emoji":"💾","aliases":["floppy_disk"]},{"emoji":"💿","aliases":["cd"]},{"emoji":"📀","aliases":["dvd"]},{"emoji":"🧮","aliases":["abacus"]},{"emoji":"🎥","aliases":["movie_camera"]},{"emoji":"🎞️","aliases":["film_strip"]},{"emoji":"📽️","aliases":["film_projector"]},{"emoji":"🎬","aliases":["clapper"]},{"emoji":"📺","aliases":["tv"]},{"emoji":"📷","aliases":["camera"]},{"emoji":"📸","aliases":["camera_flash"]},{"emoji":"📹","aliases":["video_camera"]},{"emoji":"📼","aliases":["vhs"]},{"emoji":"🔍","aliases":["mag"]},{"emoji":"🔎","aliases":["mag_right"]},{"emoji":"🕯️","aliases":["candle"]},{"emoji":"💡","aliases":["bulb"]},{"emoji":"🔦","aliases":["flashlight"]},{"emoji":"🏮","aliases":["izakaya_lantern","lantern"]},{"emoji":"🪔","aliases":["diya_lamp"]},{"emoji":"📔","aliases":["notebook_with_decorative_cover"]},{"emoji":"📕","aliases":["closed_book"]},{"emoji":"📖","aliases":["book","open_book"]},{"emoji":"📗","aliases":["green_book"]},{"emoji":"📘","aliases":["blue_book"]},{"emoji":"📙","aliases":["orange_book"]},{"emoji":"📚","aliases":["books"]},{"emoji":"📓","aliases":["notebook"]},{"emoji":"📒","aliases":["ledger"]},{"emoji":"📃","aliases":["page_with_curl"]},{"emoji":"📜","aliases":["scroll"]},{"emoji":"📄","aliases":["page_facing_up"]},{"emoji":"📰","aliases":["newspaper"]},{"emoji":"🗞️","aliases":["newspaper_roll"]},{"emoji":"📑","aliases":["bookmark_tabs"]},{"emoji":"🔖","aliases":["bookmark"]},{"emoji":"🏷️","aliases":["label"]},{"emoji":"💰","aliases":["moneybag"]},{"emoji":"🪙","aliases":["coin"]},{"emoji":"💴","aliases":["yen"]},{"emoji":"💵","aliases":["dollar"]},{"emoji":"💶","aliases":["euro"]},{"emoji":"💷","aliases":["pound"]},{"emoji":"💸","aliases":["money_with_wings"]},{"emoji":"💳","aliases":["credit_card"]},{"emoji":"🧾","aliases":["receipt"]},{"emoji":"💹","aliases":["chart"]},{"emoji":"✉️","aliases":["envelope"]},{"emoji":"📧","aliases":["email","e-mail"]},{"emoji":"📨","aliases":["incoming_envelope"]},{"emoji":"📩","aliases":["envelope_with_arrow"]},{"emoji":"📤","aliases":["outbox_tray"]},{"emoji":"📥","aliases":["inbox_tray"]},{"emoji":"📦","aliases":["package"]},{"emoji":"📫","aliases":["mailbox"]},{"emoji":"📪","aliases":["mailbox_closed"]},{"emoji":"📬","aliases":["mailbox_with_mail"]},{"emoji":"📭","aliases":["mailbox_with_no_mail"]},{"emoji":"📮","aliases":["postbox"]},{"emoji":"🗳️","aliases":["ballot_box"]},{"emoji":"✏️","aliases":["pencil2"]},{"emoji":"✒️","aliases":["black_nib"]},{"emoji":"🖋️","aliases":["fountain_pen"]},{"emoji":"🖊️","aliases":["pen"]},{"emoji":"🖌️","aliases":["paintbrush"]},{"emoji":"🖍️","aliases":["crayon"]},{"emoji":"📝","aliases":["memo","pencil"]},{"emoji":"💼","aliases":["briefcase"]},{"emoji":"📁","aliases":["file_folder"]},{"emoji":"📂","aliases":["open_file_folder"]},{"emoji":"🗂️","aliases":["card_index_dividers"]},{"emoji":"📅","aliases":["date"]},{"emoji":"📆","aliases":["calendar"]},{"emoji":"🗒️","aliases":["spiral_notepad"]},{"emoji":"🗓️","aliases":["spiral_calendar"]},{"emoji":"📇","aliases":["card_index"]},{"emoji":"📈","aliases":["chart_with_upwards_trend"]},{"emoji":"📉","aliases":["chart_with_downwards_trend"]},{"emoji":"📊","aliases":["bar_chart"]},{"emoji":"📋","aliases":["clipboard"]},{"emoji":"📌","aliases":["pushpin"]},{"emoji":"📍","aliases":["round_pushpin"]},{"emoji":"📎","aliases":["paperclip"]},{"emoji":"🖇️","aliases":["paperclips"]},{"emoji":"📏","aliases":["straight_ruler"]},{"emoji":"📐","aliases":["triangular_ruler"]},{"emoji":"✂️","aliases":["scissors"]},{"emoji":"🗃️","aliases":["card_file_box"]},{"emoji":"🗄️","aliases":["file_cabinet"]},{"emoji":"🗑️","aliases":["wastebasket"]},{"emoji":"🔒","aliases":["lock"]},{"emoji":"🔓","aliases":["unlock"]},{"emoji":"🔏","aliases":["lock_with_ink_pen"]},{"emoji":"🔐","aliases":["closed_lock_with_key"]},{"emoji":"🔑","aliases":["key"]},{"emoji":"🗝️","aliases":["old_key"]},{"emoji":"🔨","aliases":["hammer"]},{"emoji":"🪓","aliases":["axe"]},{"emoji":"⛏️","aliases":["pick"]},{"emoji":"⚒️","aliases":["hammer_and_pick"]},{"emoji":"🛠️","aliases":["hammer_and_wrench"]},{"emoji":"🗡️","aliases":["dagger"]},{"emoji":"⚔️","aliases":["crossed_swords"]},{"emoji":"🔫","aliases":["gun"]},{"emoji":"🪃","aliases":["boomerang"]},{"emoji":"🏹","aliases":["bow_and_arrow"]},{"emoji":"🛡️","aliases":["shield"]},{"emoji":"🪚","aliases":["carpentry_saw"]},{"emoji":"🔧","aliases":["wrench"]},{"emoji":"🪛","aliases":["screwdriver"]},{"emoji":"🔩","aliases":["nut_and_bolt"]},{"emoji":"⚙️","aliases":["gear"]},{"emoji":"🗜️","aliases":["clamp"]},{"emoji":"⚖️","aliases":["balance_scale"]},{"emoji":"🦯","aliases":["probing_cane"]},{"emoji":"🔗","aliases":["link"]},{"emoji":"⛓️","aliases":["chains"]},{"emoji":"🪝","aliases":["hook"]},{"emoji":"🧰","aliases":["toolbox"]},{"emoji":"🧲","aliases":["magnet"]},{"emoji":"🪜","aliases":["ladder"]},{"emoji":"⚗️","aliases":["alembic"]},{"emoji":"🧪","aliases":["test_tube"]},{"emoji":"🧫","aliases":["petri_dish"]},{"emoji":"🧬","aliases":["dna"]},{"emoji":"🔬","aliases":["microscope"]},{"emoji":"🔭","aliases":["telescope"]},{"emoji":"📡","aliases":["satellite"]},{"emoji":"💉","aliases":["syringe"]},{"emoji":"🩸","aliases":["drop_of_blood"]},{"emoji":"💊","aliases":["pill"]},{"emoji":"🩹","aliases":["adhesive_bandage"]},{"emoji":"🩺","aliases":["stethoscope"]},{"emoji":"🚪","aliases":["door"]},{"emoji":"🛗","aliases":["elevator"]},{"emoji":"🪞","aliases":["mirror"]},{"emoji":"🪟","aliases":["window"]},{"emoji":"🛏️","aliases":["bed"]},{"emoji":"🛋️","aliases":["couch_and_lamp"]},{"emoji":"🪑","aliases":["chair"]},{"emoji":"🚽","aliases":["toilet"]},{"emoji":"🪠","aliases":["plunger"]},{"emoji":"🚿","aliases":["shower"]},{"emoji":"🛁","aliases":["bathtub"]},{"emoji":"🪤","aliases":["mouse_trap"]},{"emoji":"🪒","aliases":["razor"]},{"emoji":"🧴","aliases":["lotion_bottle"]},{"emoji":"🧷","aliases":["safety_pin"]},{"emoji":"🧹","aliases":["broom"]},{"emoji":"🧺","aliases":["basket"]},{"emoji":"🧻","aliases":["roll_of_paper"]},{"emoji":"🪣","aliases":["bucket"]},{"emoji":"🧼","aliases":["soap"]},{"emoji":"🪥","aliases":["toothbrush"]},{"emoji":"🧽","aliases":["sponge"]},{"emoji":"🧯","aliases":["fire_extinguisher"]},{"emoji":"🛒","aliases":["shopping_cart"]},{"emoji":"🚬","aliases":["smoking"]},{"emoji":"⚰️","aliases":["coffin"]},{"emoji":"🪦","aliases":["headstone"]},{"emoji":"⚱️","aliases":["funeral_urn"]},{"emoji":"🗿","aliases":["moyai"]},{"emoji":"🪧","aliases":["placard"]},{"emoji":"🏧","aliases":["atm"]},{"emoji":"🚮","aliases":["put_litter_in_its_place"]},{"emoji":"🚰","aliases":["potable_water"]},{"emoji":"♿","aliases":["wheelchair"]},{"emoji":"🚹","aliases":["mens"]},{"emoji":"🚺","aliases":["womens"]},{"emoji":"🚻","aliases":["restroom"]},{"emoji":"🚼","aliases":["baby_symbol"]},{"emoji":"🚾","aliases":["wc"]},{"emoji":"🛂","aliases":["passport_control"]},{"emoji":"🛃","aliases":["customs"]},{"emoji":"🛄","aliases":["baggage_claim"]},{"emoji":"🛅","aliases":["left_luggage"]},{"emoji":"⚠️","aliases":["warning"]},{"emoji":"🚸","aliases":["children_crossing"]},{"emoji":"⛔","aliases":["no_entry"]},{"emoji":"🚫","aliases":["no_entry_sign"]},{"emoji":"🚳","aliases":["no_bicycles"]},{"emoji":"🚭","aliases":["no_smoking"]},{"emoji":"🚯","aliases":["do_not_litter"]},{"emoji":"🚱","aliases":["non-potable_water"]},{"emoji":"🚷","aliases":["no_pedestrians"]},{"emoji":"📵","aliases":["no_mobile_phones"]},{"emoji":"🔞","aliases":["underage"]},{"emoji":"☢️","aliases":["radioactive"]},{"emoji":"☣️","aliases":["biohazard"]},{"emoji":"⬆️","aliases":["arrow_up"]},{"emoji":"↗️","aliases":["arrow_upper_right"]},{"emoji":"➡️","aliases":["arrow_right"]},{"emoji":"↘️","aliases":["arrow_lower_right"]},{"emoji":"⬇️","aliases":["arrow_down"]},{"emoji":"↙️","aliases":["arrow_lower_left"]},{"emoji":"⬅️","aliases":["arrow_left"]},{"emoji":"↖️","aliases":["arrow_upper_left"]},{"emoji":"↕️","aliases":["arrow_up_down"]},{"emoji":"↔️","aliases":["left_right_arrow"]},{"emoji":"↩️","aliases":["leftwards_arrow_with_hook"]},{"emoji":"↪️","aliases":["arrow_right_hook"]},{"emoji":"⤴️","aliases":["arrow_heading_up"]},{"emoji":"⤵️","aliases":["arrow_heading_down"]},{"emoji":"🔃","aliases":["arrows_clockwise"]},{"emoji":"🔄","aliases":["arrows_counterclockwise"]},{"emoji":"🔙","aliases":["back"]},{"emoji":"🔚","aliases":["end"]},{"emoji":"🔛","aliases":["on"]},{"emoji":"🔜","aliases":["soon"]},{"emoji":"🔝","aliases":["top"]},{"emoji":"🛐","aliases":["place_of_worship"]},{"emoji":"⚛️","aliases":["atom_symbol"]},{"emoji":"🕉️","aliases":["om"]},{"emoji":"✡️","aliases":["star_of_david"]},{"emoji":"☸️","aliases":["wheel_of_dharma"]},{"emoji":"☯️","aliases":["yin_yang"]},{"emoji":"✝️","aliases":["latin_cross"]},{"emoji":"☦️","aliases":["orthodox_cross"]},{"emoji":"☪️","aliases":["star_and_crescent"]},{"emoji":"☮️","aliases":["peace_symbol"]},{"emoji":"🕎","aliases":["menorah"]},{"emoji":"🔯","aliases":["six_pointed_star"]},{"emoji":"♈","aliases":["aries"]},{"emoji":"♉","aliases":["taurus"]},{"emoji":"♊","aliases":["gemini"]},{"emoji":"♋","aliases":["cancer"]},{"emoji":"♌","aliases":["leo"]},{"emoji":"♍","aliases":["virgo"]},{"emoji":"♎","aliases":["libra"]},{"emoji":"♏","aliases":["scorpius"]},{"emoji":"♐","aliases":["sagittarius"]},{"emoji":"♑","aliases":["capricorn"]},{"emoji":"♒","aliases":["aquarius"]},{"emoji":"♓","aliases":["pisces"]},{"emoji":"⛎","aliases":["ophiuchus"]},{"emoji":"🔀","aliases":["twisted_rightwards_arrows"]},{"emoji":"🔁","aliases":["repeat"]},{"emoji":"🔂","aliases":["repeat_one"]},{"emoji":"▶️","aliases":["arrow_forward"]},{"emoji":"⏩","aliases":["fast_forward"]},{"emoji":"⏭️","aliases":["next_track_button"]},{"emoji":"⏯️","aliases":["play_or_pause_button"]},{"emoji":"◀️","aliases":["arrow_backward"]},{"emoji":"⏪","aliases":["rewind"]},{"emoji":"⏮️","aliases":["previous_track_button"]},{"emoji":"🔼","aliases":["arrow_up_small"]},{"emoji":"⏫","aliases":["arrow_double_up"]},{"emoji":"🔽","aliases":["arrow_down_small"]},{"emoji":"⏬","aliases":["arrow_double_down"]},{"emoji":"⏸️","aliases":["pause_button"]},{"emoji":"⏹️","aliases":["stop_button"]},{"emoji":"⏺️","aliases":["record_button"]},{"emoji":"⏏️","aliases":["eject_button"]},{"emoji":"🎦","aliases":["cinema"]},{"emoji":"🔅","aliases":["low_brightness"]},{"emoji":"🔆","aliases":["high_brightness"]},{"emoji":"📶","aliases":["signal_strength"]},{"emoji":"📳","aliases":["vibration_mode"]},{"emoji":"📴","aliases":["mobile_phone_off"]},{"emoji":"♀️","aliases":["female_sign"]},{"emoji":"♂️","aliases":["male_sign"]},{"emoji":"⚧️","aliases":["transgender_symbol"]},{"emoji":"✖️","aliases":["heavy_multiplication_x"]},{"emoji":"➕","aliases":["heavy_plus_sign"]},{"emoji":"➖","aliases":["heavy_minus_sign"]},{"emoji":"➗","aliases":["heavy_division_sign"]},{"emoji":"♾️","aliases":["infinity"]},{"emoji":"‼️","aliases":["bangbang"]},{"emoji":"⁉️","aliases":["interrobang"]},{"emoji":"❓","aliases":["question"]},{"emoji":"❔","aliases":["grey_question"]},{"emoji":"❕","aliases":["grey_exclamation"]},{"emoji":"❗","aliases":["exclamation","heavy_exclamation_mark"]},{"emoji":"〰️","aliases":["wavy_dash"]},{"emoji":"💱","aliases":["currency_exchange"]},{"emoji":"💲","aliases":["heavy_dollar_sign"]},{"emoji":"⚕️","aliases":["medical_symbol"]},{"emoji":"♻️","aliases":["recycle"]},{"emoji":"⚜️","aliases":["fleur_de_lis"]},{"emoji":"🔱","aliases":["trident"]},{"emoji":"📛","aliases":["name_badge"]},{"emoji":"🔰","aliases":["beginner"]},{"emoji":"⭕","aliases":["o"]},{"emoji":"✅","aliases":["white_check_mark"]},{"emoji":"☑️","aliases":["ballot_box_with_check"]},{"emoji":"✔️","aliases":["heavy_check_mark"]},{"emoji":"❌","aliases":["x"]},{"emoji":"❎","aliases":["negative_squared_cross_mark"]},{"emoji":"➰","aliases":["curly_loop"]},{"emoji":"➿","aliases":["loop"]},{"emoji":"〽️","aliases":["part_alternation_mark"]},{"emoji":"✳️","aliases":["eight_spoked_asterisk"]},{"emoji":"✴️","aliases":["eight_pointed_black_star"]},{"emoji":"❇️","aliases":["sparkle"]},{"emoji":"©️","aliases":["copyright"]},{"emoji":"®️","aliases":["registered"]},{"emoji":"™️","aliases":["tm"]},{"emoji":"#️⃣","aliases":["hash"]},{"emoji":"*️⃣","aliases":["asterisk"]},{"emoji":"0️⃣","aliases":["zero"]},{"emoji":"1️⃣","aliases":["one"]},{"emoji":"2️⃣","aliases":["two"]},{"emoji":"3️⃣","aliases":["three"]},{"emoji":"4️⃣","aliases":["four"]},{"emoji":"5️⃣","aliases":["five"]},{"emoji":"6️⃣","aliases":["six"]},{"emoji":"7️⃣","aliases":["seven"]},{"emoji":"8️⃣","aliases":["eight"]},{"emoji":"9️⃣","aliases":["nine"]},{"emoji":"🔟","aliases":["keycap_ten"]},{"emoji":"🔠","aliases":["capital_abcd"]},{"emoji":"🔡","aliases":["abcd"]},{"emoji":"🔢","aliases":["1234"]},{"emoji":"🔣","aliases":["symbols"]},{"emoji":"🔤","aliases":["abc"]},{"emoji":"🅰️","aliases":["a"]},{"emoji":"🆎","aliases":["ab"]},{"emoji":"🅱️","aliases":["b"]},{"emoji":"🆑","aliases":["cl"]},{"emoji":"🆒","aliases":["cool"]},{"emoji":"🆓","aliases":["free"]},{"emoji":"ℹ️","aliases":["information_source"]},{"emoji":"🆔","aliases":["id"]},{"emoji":"Ⓜ️","aliases":["m"]},{"emoji":"🆕","aliases":["new"]},{"emoji":"🆖","aliases":["ng"]},{"emoji":"🅾️","aliases":["o2"]},{"emoji":"🆗","aliases":["ok"]},{"emoji":"🅿️","aliases":["parking"]},{"emoji":"🆘","aliases":["sos"]},{"emoji":"🆙","aliases":["up"]},{"emoji":"🆚","aliases":["vs"]},{"emoji":"🈁","aliases":["koko"]},{"emoji":"🈂️","aliases":["sa"]},{"emoji":"🈷️","aliases":["u6708"]},{"emoji":"🈶","aliases":["u6709"]},{"emoji":"🈯","aliases":["u6307"]},{"emoji":"🉐","aliases":["ideograph_advantage"]},{"emoji":"🈹","aliases":["u5272"]},{"emoji":"🈚","aliases":["u7121"]},{"emoji":"🈲","aliases":["u7981"]},{"emoji":"🉑","aliases":["accept"]},{"emoji":"🈸","aliases":["u7533"]},{"emoji":"🈴","aliases":["u5408"]},{"emoji":"🈳","aliases":["u7a7a"]},{"emoji":"㊗️","aliases":["congratulations"]},{"emoji":"㊙️","aliases":["secret"]},{"emoji":"🈺","aliases":["u55b6"]},{"emoji":"🈵","aliases":["u6e80"]},{"emoji":"🔴","aliases":["red_circle"]},{"emoji":"🟠","aliases":["orange_circle"]},{"emoji":"🟡","aliases":["yellow_circle"]},{"emoji":"🟢","aliases":["green_circle"]},{"emoji":"🔵","aliases":["large_blue_circle"]},{"emoji":"🟣","aliases":["purple_circle"]},{"emoji":"🟤","aliases":["brown_circle"]},{"emoji":"⚫","aliases":["black_circle"]},{"emoji":"⚪","aliases":["white_circle"]},{"emoji":"🟥","aliases":["red_square"]},{"emoji":"🟧","aliases":["orange_square"]},{"emoji":"🟨","aliases":["yellow_square"]},{"emoji":"🟩","aliases":["green_square"]},{"emoji":"🟦","aliases":["blue_square"]},{"emoji":"🟪","aliases":["purple_square"]},{"emoji":"🟫","aliases":["brown_square"]},{"emoji":"⬛","aliases":["black_large_square"]},{"emoji":"⬜","aliases":["white_large_square"]},{"emoji":"◼️","aliases":["black_medium_square"]},{"emoji":"◻️","aliases":["white_medium_square"]},{"emoji":"◾","aliases":["black_medium_small_square"]},{"emoji":"◽","aliases":["white_medium_small_square"]},{"emoji":"▪️","aliases":["black_small_square"]},{"emoji":"▫️","aliases":["white_small_square"]},{"emoji":"🔶","aliases":["large_orange_diamond"]},{"emoji":"🔷","aliases":["large_blue_diamond"]},{"emoji":"🔸","aliases":["small_orange_diamond"]},{"emoji":"🔹","aliases":["small_blue_diamond"]},{"emoji":"🔺","aliases":["small_red_triangle"]},{"emoji":"🔻","aliases":["small_red_triangle_down"]},{"emoji":"💠","aliases":["diamond_shape_with_a_dot_inside"]},{"emoji":"🔘","aliases":["radio_button"]},{"emoji":"🔳","aliases":["white_square_button"]},{"emoji":"🔲","aliases":["black_square_button"]},{"emoji":"🏁","aliases":["checkered_flag"]},{"emoji":"🚩","aliases":["triangular_flag_on_post"]},{"emoji":"🎌","aliases":["crossed_flags"]},{"emoji":"🏴","aliases":["black_flag"]},{"emoji":"🏳️","aliases":["white_flag"]},{"emoji":"🏳️‍🌈","aliases":["rainbow_flag"]},{"emoji":"🏳️‍⚧️","aliases":["transgender_flag"]},{"emoji":"🏴‍☠️","aliases":["pirate_flag"]},{"emoji":"🇦🇨","aliases":["ascension_island"]},{"emoji":"🇦🇩","aliases":["andorra"]},{"emoji":"🇦🇪","aliases":["united_arab_emirates"]},{"emoji":"🇦🇫","aliases":["afghanistan"]},{"emoji":"🇦🇬","aliases":["antigua_barbuda"]},{"emoji":"🇦🇮","aliases":["anguilla"]},{"emoji":"🇦🇱","aliases":["albania"]},{"emoji":"🇦🇲","aliases":["armenia"]},{"emoji":"🇦🇴","aliases":["angola"]},{"emoji":"🇦🇶","aliases":["antarctica"]},{"emoji":"🇦🇷","aliases":["argentina"]},{"emoji":"🇦🇸","aliases":["american_samoa"]},{"emoji":"🇦🇹","aliases":["austria"]},{"emoji":"🇦🇺","aliases":["australia"]},{"emoji":"🇦🇼","aliases":["aruba"]},{"emoji":"🇦🇽","aliases":["aland_islands"]},{"emoji":"🇦🇿","aliases":["azerbaijan"]},{"emoji":"🇧🇦","aliases":["bosnia_herzegovina"]},{"emoji":"🇧🇧","aliases":["barbados"]},{"emoji":"🇧🇩","aliases":["bangladesh"]},{"emoji":"🇧🇪","aliases":["belgium"]},{"emoji":"🇧🇫","aliases":["burkina_faso"]},{"emoji":"🇧🇬","aliases":["bulgaria"]},{"emoji":"🇧🇭","aliases":["bahrain"]},{"emoji":"🇧🇮","aliases":["burundi"]},{"emoji":"🇧🇯","aliases":["benin"]},{"emoji":"🇧🇱","aliases":["st_barthelemy"]},{"emoji":"🇧🇲","aliases":["bermuda"]},{"emoji":"🇧🇳","aliases":["brunei"]},{"emoji":"🇧🇴","aliases":["bolivia"]},{"emoji":"🇧🇶","aliases":["caribbean_netherlands"]},{"emoji":"🇧🇷","aliases":["brazil"]},{"emoji":"🇧🇸","aliases":["bahamas"]},{"emoji":"🇧🇹","aliases":["bhutan"]},{"emoji":"🇧🇻","aliases":["bouvet_island"]},{"emoji":"🇧🇼","aliases":["botswana"]},{"emoji":"🇧🇾","aliases":["belarus"]},{"emoji":"🇧🇿","aliases":["belize"]},{"emoji":"🇨🇦","aliases":["canada"]},{"emoji":"🇨🇨","aliases":["cocos_islands"]},{"emoji":"🇨🇩","aliases":["congo_kinshasa"]},{"emoji":"🇨🇫","aliases":["central_african_republic"]},{"emoji":"🇨🇬","aliases":["congo_brazzaville"]},{"emoji":"🇨🇭","aliases":["switzerland"]},{"emoji":"🇨🇮","aliases":["cote_divoire"]},{"emoji":"🇨🇰","aliases":["cook_islands"]},{"emoji":"🇨🇱","aliases":["chile"]},{"emoji":"🇨🇲","aliases":["cameroon"]},{"emoji":"🇨🇳","aliases":["cn"]},{"emoji":"🇨🇴","aliases":["colombia"]},{"emoji":"🇨🇵","aliases":["clipperton_island"]},{"emoji":"🇨🇷","aliases":["costa_rica"]},{"emoji":"🇨🇺","aliases":["cuba"]},{"emoji":"🇨🇻","aliases":["cape_verde"]},{"emoji":"🇨🇼","aliases":["curacao"]},{"emoji":"🇨🇽","aliases":["christmas_island"]},{"emoji":"🇨🇾","aliases":["cyprus"]},{"emoji":"🇨🇿","aliases":["czech_republic"]},{"emoji":"🇩🇪","aliases":["de"]},{"emoji":"🇩🇬","aliases":["diego_garcia"]},{"emoji":"🇩🇯","aliases":["djibouti"]},{"emoji":"🇩🇰","aliases":["denmark"]},{"emoji":"🇩🇲","aliases":["dominica"]},{"emoji":"🇩🇴","aliases":["dominican_republic"]},{"emoji":"🇩🇿","aliases":["algeria"]},{"emoji":"🇪🇦","aliases":["ceuta_melilla"]},{"emoji":"🇪🇨","aliases":["ecuador"]},{"emoji":"🇪🇪","aliases":["estonia"]},{"emoji":"🇪🇬","aliases":["egypt"]},{"emoji":"🇪🇭","aliases":["western_sahara"]},{"emoji":"🇪🇷","aliases":["eritrea"]},{"emoji":"🇪🇸","aliases":["es"]},{"emoji":"🇪🇹","aliases":["ethiopia"]},{"emoji":"🇪🇺","aliases":["eu","european_union"]},{"emoji":"🇫🇮","aliases":["finland"]},{"emoji":"🇫🇯","aliases":["fiji"]},{"emoji":"🇫🇰","aliases":["falkland_islands"]},{"emoji":"🇫🇲","aliases":["micronesia"]},{"emoji":"🇫🇴","aliases":["faroe_islands"]},{"emoji":"🇫🇷","aliases":["fr"]},{"emoji":"🇬🇦","aliases":["gabon"]},{"emoji":"🇬🇧","aliases":["gb","uk"]},{"emoji":"🇬🇩","aliases":["grenada"]},{"emoji":"🇬🇪","aliases":["georgia"]},{"emoji":"🇬🇫","aliases":["french_guiana"]},{"emoji":"🇬🇬","aliases":["guernsey"]},{"emoji":"🇬🇭","aliases":["ghana"]},{"emoji":"🇬🇮","aliases":["gibraltar"]},{"emoji":"🇬🇱","aliases":["greenland"]},{"emoji":"🇬🇲","aliases":["gambia"]},{"emoji":"🇬🇳","aliases":["guinea"]},{"emoji":"🇬🇵","aliases":["guadeloupe"]},{"emoji":"🇬🇶","aliases":["equatorial_guinea"]},{"emoji":"🇬🇷","aliases":["greece"]},{"emoji":"🇬🇸","aliases":["south_georgia_south_sandwich_islands"]},{"emoji":"🇬🇹","aliases":["guatemala"]},{"emoji":"🇬🇺","aliases":["guam"]},{"emoji":"🇬🇼","aliases":["guinea_bissau"]},{"emoji":"🇬🇾","aliases":["guyana"]},{"emoji":"🇭🇰","aliases":["hong_kong"]},{"emoji":"🇭🇲","aliases":["heard_mcdonald_islands"]},{"emoji":"🇭🇳","aliases":["honduras"]},{"emoji":"🇭🇷","aliases":["croatia"]},{"emoji":"🇭🇹","aliases":["haiti"]},{"emoji":"🇭🇺","aliases":["hungary"]},{"emoji":"🇮🇨","aliases":["canary_islands"]},{"emoji":"🇮🇩","aliases":["indonesia"]},{"emoji":"🇮🇪","aliases":["ireland"]},{"emoji":"🇮🇱","aliases":["israel"]},{"emoji":"🇮🇲","aliases":["isle_of_man"]},{"emoji":"🇮🇳","aliases":["india"]},{"emoji":"🇮🇴","aliases":["british_indian_ocean_territory"]},{"emoji":"🇮🇶","aliases":["iraq"]},{"emoji":"🇮🇷","aliases":["iran"]},{"emoji":"🇮🇸","aliases":["iceland"]},{"emoji":"🇮🇹","aliases":["it"]},{"emoji":"🇯🇪","aliases":["jersey"]},{"emoji":"🇯🇲","aliases":["jamaica"]},{"emoji":"🇯🇴","aliases":["jordan"]},{"emoji":"🇯🇵","aliases":["jp"]},{"emoji":"🇰🇪","aliases":["kenya"]},{"emoji":"🇰🇬","aliases":["kyrgyzstan"]},{"emoji":"🇰🇭","aliases":["cambodia"]},{"emoji":"🇰🇮","aliases":["kiribati"]},{"emoji":"🇰🇲","aliases":["comoros"]},{"emoji":"🇰🇳","aliases":["st_kitts_nevis"]},{"emoji":"🇰🇵","aliases":["north_korea"]},{"emoji":"🇰🇷","aliases":["kr"]},{"emoji":"🇰🇼","aliases":["kuwait"]},{"emoji":"🇰🇾","aliases":["cayman_islands"]},{"emoji":"🇰🇿","aliases":["kazakhstan"]},{"emoji":"🇱🇦","aliases":["laos"]},{"emoji":"🇱🇧","aliases":["lebanon"]},{"emoji":"🇱🇨","aliases":["st_lucia"]},{"emoji":"🇱🇮","aliases":["liechtenstein"]},{"emoji":"🇱🇰","aliases":["sri_lanka"]},{"emoji":"🇱🇷","aliases":["liberia"]},{"emoji":"🇱🇸","aliases":["lesotho"]},{"emoji":"🇱🇹","aliases":["lithuania"]},{"emoji":"🇱🇺","aliases":["luxembourg"]},{"emoji":"🇱🇻","aliases":["latvia"]},{"emoji":"🇱🇾","aliases":["libya"]},{"emoji":"🇲🇦","aliases":["morocco"]},{"emoji":"🇲🇨","aliases":["monaco"]},{"emoji":"🇲🇩","aliases":["moldova"]},{"emoji":"🇲🇪","aliases":["montenegro"]},{"emoji":"🇲🇫","aliases":["st_martin"]},{"emoji":"🇲🇬","aliases":["madagascar"]},{"emoji":"🇲🇭","aliases":["marshall_islands"]},{"emoji":"🇲🇰","aliases":["macedonia"]},{"emoji":"🇲🇱","aliases":["mali"]},{"emoji":"🇲🇲","aliases":["myanmar"]},{"emoji":"🇲🇳","aliases":["mongolia"]},{"emoji":"🇲🇴","aliases":["macau"]},{"emoji":"🇲🇵","aliases":["northern_mariana_islands"]},{"emoji":"🇲🇶","aliases":["martinique"]},{"emoji":"🇲🇷","aliases":["mauritania"]},{"emoji":"🇲🇸","aliases":["montserrat"]},{"emoji":"🇲🇹","aliases":["malta"]},{"emoji":"🇲🇺","aliases":["mauritius"]},{"emoji":"🇲🇻","aliases":["maldives"]},{"emoji":"🇲🇼","aliases":["malawi"]},{"emoji":"🇲🇽","aliases":["mexico"]},{"emoji":"🇲🇾","aliases":["malaysia"]},{"emoji":"🇲🇿","aliases":["mozambique"]},{"emoji":"🇳🇦","aliases":["namibia"]},{"emoji":"🇳🇨","aliases":["new_caledonia"]},{"emoji":"🇳🇪","aliases":["niger"]},{"emoji":"🇳🇫","aliases":["norfolk_island"]},{"emoji":"🇳🇬","aliases":["nigeria"]},{"emoji":"🇳🇮","aliases":["nicaragua"]},{"emoji":"🇳🇱","aliases":["netherlands"]},{"emoji":"🇳🇴","aliases":["norway"]},{"emoji":"🇳🇵","aliases":["nepal"]},{"emoji":"🇳🇷","aliases":["nauru"]},{"emoji":"🇳🇺","aliases":["niue"]},{"emoji":"🇳🇿","aliases":["new_zealand"]},{"emoji":"🇴🇲","aliases":["oman"]},{"emoji":"🇵🇦","aliases":["panama"]},{"emoji":"🇵🇪","aliases":["peru"]},{"emoji":"🇵🇫","aliases":["french_polynesia"]},{"emoji":"🇵🇬","aliases":["papua_new_guinea"]},{"emoji":"🇵🇭","aliases":["philippines"]},{"emoji":"🇵🇰","aliases":["pakistan"]},{"emoji":"🇵🇱","aliases":["poland"]},{"emoji":"🇵🇲","aliases":["st_pierre_miquelon"]},{"emoji":"🇵🇳","aliases":["pitcairn_islands"]},{"emoji":"🇵🇷","aliases":["puerto_rico"]},{"emoji":"🇵🇸","aliases":["palestinian_territories"]},{"emoji":"🇵🇹","aliases":["portugal"]},{"emoji":"🇵🇼","aliases":["palau"]},{"emoji":"🇵🇾","aliases":["paraguay"]},{"emoji":"🇶🇦","aliases":["qatar"]},{"emoji":"🇷🇪","aliases":["reunion"]},{"emoji":"🇷🇴","aliases":["romania"]},{"emoji":"🇷🇸","aliases":["serbia"]},{"emoji":"🇷🇺","aliases":["ru"]},{"emoji":"🇷🇼","aliases":["rwanda"]},{"emoji":"🇸🇦","aliases":["saudi_arabia"]},{"emoji":"🇸🇧","aliases":["solomon_islands"]},{"emoji":"🇸🇨","aliases":["seychelles"]},{"emoji":"🇸🇩","aliases":["sudan"]},{"emoji":"🇸🇪","aliases":["sweden"]},{"emoji":"🇸🇬","aliases":["singapore"]},{"emoji":"🇸🇭","aliases":["st_helena"]},{"emoji":"🇸🇮","aliases":["slovenia"]},{"emoji":"🇸🇯","aliases":["svalbard_jan_mayen"]},{"emoji":"🇸🇰","aliases":["slovakia"]},{"emoji":"🇸🇱","aliases":["sierra_leone"]},{"emoji":"🇸🇲","aliases":["san_marino"]},{"emoji":"🇸🇳","aliases":["senegal"]},{"emoji":"🇸🇴","aliases":["somalia"]},{"emoji":"🇸🇷","aliases":["suriname"]},{"emoji":"🇸🇸","aliases":["south_sudan"]},{"emoji":"🇸🇹","aliases":["sao_tome_principe"]},{"emoji":"🇸🇻","aliases":["el_salvador"]},{"emoji":"🇸🇽","aliases":["sint_maarten"]},{"emoji":"🇸🇾","aliases":["syria"]},{"emoji":"🇸🇿","aliases":["swaziland"]},{"emoji":"🇹🇦","aliases":["tristan_da_cunha"]},{"emoji":"🇹🇨","aliases":["turks_caicos_islands"]},{"emoji":"🇹🇩","aliases":["chad"]},{"emoji":"🇹🇫","aliases":["french_southern_territories"]},{"emoji":"🇹🇬","aliases":["togo"]},{"emoji":"🇹🇭","aliases":["thailand"]},{"emoji":"🇹🇯","aliases":["tajikistan"]},{"emoji":"🇹🇰","aliases":["tokelau"]},{"emoji":"🇹🇱","aliases":["timor_leste"]},{"emoji":"🇹🇲","aliases":["turkmenistan"]},{"emoji":"🇹🇳","aliases":["tunisia"]},{"emoji":"🇹🇴","aliases":["tonga"]},{"emoji":"🇹🇷","aliases":["tr"]},{"emoji":"🇹🇹","aliases":["trinidad_tobago"]},{"emoji":"🇹🇻","aliases":["tuvalu"]},{"emoji":"🇹🇼","aliases":["taiwan"]},{"emoji":"🇹🇿","aliases":["tanzania"]},{"emoji":"🇺🇦","aliases":["ukraine"]},{"emoji":"🇺🇬","aliases":["uganda"]},{"emoji":"🇺🇲","aliases":["us_outlying_islands"]},{"emoji":"🇺🇳","aliases":["united_nations"]},{"emoji":"🇺🇸","aliases":["us"]},{"emoji":"🇺🇾","aliases":["uruguay"]},{"emoji":"🇺🇿","aliases":["uzbekistan"]},{"emoji":"🇻🇦","aliases":["vatican_city"]},{"emoji":"🇻🇨","aliases":["st_vincent_grenadines"]},{"emoji":"🇻🇪","aliases":["venezuela"]},{"emoji":"🇻🇬","aliases":["british_virgin_islands"]},{"emoji":"🇻🇮","aliases":["us_virgin_islands"]},{"emoji":"🇻🇳","aliases":["vietnam"]},{"emoji":"🇻🇺","aliases":["vanuatu"]},{"emoji":"🇼🇫","aliases":["wallis_futuna"]},{"emoji":"🇼🇸","aliases":["samoa"]},{"emoji":"🇽🇰","aliases":["kosovo"]},{"emoji":"🇾🇪","aliases":["yemen"]},{"emoji":"🇾🇹","aliases":["mayotte"]},{"emoji":"🇿🇦","aliases":["south_africa"]},{"emoji":"🇿🇲","aliases":["zambia"]},{"emoji":"🇿🇼","aliases":["zimbabwe"]},{"emoji":"🏴󠁧󠁢󠁥󠁮󠁧󠁿","aliases":["england"]},{"emoji":"🏴󠁧󠁢󠁳󠁣󠁴󠁿","aliases":["scotland"]},{"emoji":"🏴󠁧󠁢󠁷󠁬󠁳󠁿","aliases":["wales"]}] diff --git a/server/mailer_emoji_map.json b/server/mailer_emoji_map.json new file mode 100644 index 00000000..8520c24c --- /dev/null +++ b/server/mailer_emoji_map.json @@ -0,0 +1,1857 @@ +{ + "+1": "👍", + "-1": "👎", + "100": "💯", + "1234": "🔢", + "1st_place_medal": "🥇", + "2nd_place_medal": "🥈", + "3rd_place_medal": "🥉", + "8ball": "🎱", + "a": "🅰️", + "ab": "🆎", + "abacus": "🧮", + "abc": "🔤", + "abcd": "🔡", + "accept": "🉑", + "accordion": "🪗", + "adhesive_bandage": "🩹", + "adult": "🧑", + "aerial_tramway": "🚡", + "afghanistan": "🇦🇫", + "airplane": "✈️", + "aland_islands": "🇦🇽", + "alarm_clock": "⏰", + "albania": "🇦🇱", + "alembic": "⚗️", + "algeria": "🇩🇿", + "alien": "👽", + "ambulance": "🚑", + "american_samoa": "🇦🇸", + "amphora": "🏺", + "anatomical_heart": "🫀", + "anchor": "⚓", + "andorra": "🇦🇩", + "angel": "👼", + "anger": "💢", + "angola": "🇦🇴", + "angry": "😠", + "anguilla": "🇦🇮", + "anguished": "😧", + "ant": "🐜", + "antarctica": "🇦🇶", + "antigua_barbuda": "🇦🇬", + "apple": "🍎", + "aquarius": "♒", + "argentina": "🇦🇷", + "aries": "♈", + "armenia": "🇦🇲", + "arrow_backward": "◀️", + "arrow_double_down": "⏬", + "arrow_double_up": "⏫", + "arrow_down": "⬇️", + "arrow_down_small": "🔽", + "arrow_forward": "▶️", + "arrow_heading_down": "⤵️", + "arrow_heading_up": "⤴️", + "arrow_left": "⬅️", + "arrow_lower_left": "↙️", + "arrow_lower_right": "↘️", + "arrow_right": "➡️", + "arrow_right_hook": "↪️", + "arrow_up": "⬆️", + "arrow_up_down": "↕️", + "arrow_up_small": "🔼", + "arrow_upper_left": "↖️", + "arrow_upper_right": "↗️", + "arrows_clockwise": "🔃", + "arrows_counterclockwise": "🔄", + "art": "🎨", + "articulated_lorry": "🚛", + "artificial_satellite": "🛰️", + "artist": "🧑‍🎨", + "aruba": "🇦🇼", + "ascension_island": "🇦🇨", + "asterisk": "*️⃣", + "astonished": "😲", + "astronaut": "🧑‍🚀", + "athletic_shoe": "👟", + "atm": "🏧", + "atom_symbol": "⚛️", + "australia": "🇦🇺", + "austria": "🇦🇹", + "auto_rickshaw": "🛺", + "avocado": "🥑", + "axe": "🪓", + "azerbaijan": "🇦🇿", + "b": "🅱️", + "baby": "👶", + "baby_bottle": "🍼", + "baby_chick": "🐤", + "baby_symbol": "🚼", + "back": "🔙", + "bacon": "🥓", + "badger": "🦡", + "badminton": "🏸", + "bagel": "🥯", + "baggage_claim": "🛄", + "baguette_bread": "🥖", + "bahamas": "🇧🇸", + "bahrain": "🇧🇭", + "balance_scale": "⚖️", + "bald_man": "👨‍🦲", + "bald_woman": "👩‍🦲", + "ballet_shoes": "🩰", + "balloon": "🎈", + "ballot_box": "🗳️", + "ballot_box_with_check": "☑️", + "bamboo": "🎍", + "banana": "🍌", + "bangbang": "‼️", + "bangladesh": "🇧🇩", + "banjo": "🪕", + "bank": "🏦", + "bar_chart": "📊", + "barbados": "🇧🇧", + "barber": "💈", + "baseball": "⚾", + "basket": "🧺", + "basketball": "🏀", + "basketball_man": "⛹️‍♂️", + "basketball_woman": "⛹️‍♀️", + "bat": "🦇", + "bath": "🛀", + "bathtub": "🛁", + "battery": "🔋", + "beach_umbrella": "🏖️", + "bear": "🐻", + "bearded_person": "🧔", + "beaver": "🦫", + "bed": "🛏️", + "bee": "🐝", + "beer": "🍺", + "beers": "🍻", + "beetle": "🪲", + "beginner": "🔰", + "belarus": "🇧🇾", + "belgium": "🇧🇪", + "belize": "🇧🇿", + "bell": "🔔", + "bell_pepper": "🫑", + "bellhop_bell": "🛎️", + "benin": "🇧🇯", + "bento": "🍱", + "bermuda": "🇧🇲", + "beverage_box": "🧃", + "bhutan": "🇧🇹", + "bicyclist": "🚴", + "bike": "🚲", + "biking_man": "🚴‍♂️", + "biking_woman": "🚴‍♀️", + "bikini": "👙", + "billed_cap": "🧢", + "biohazard": "☣️", + "bird": "🐦", + "birthday": "🎂", + "bison": "🦬", + "black_cat": "🐈‍⬛", + "black_circle": "⚫", + "black_flag": "🏴", + "black_heart": "🖤", + "black_joker": "🃏", + "black_large_square": "⬛", + "black_medium_small_square": "◾", + "black_medium_square": "◼️", + "black_nib": "✒️", + "black_small_square": "▪️", + "black_square_button": "🔲", + "blond_haired_man": "👱‍♂️", + "blond_haired_person": "👱", + "blond_haired_woman": "👱‍♀️", + "blonde_woman": "👱‍♀️", + "blossom": "🌼", + "blowfish": "🐡", + "blue_book": "📘", + "blue_car": "🚙", + "blue_heart": "💙", + "blue_square": "🟦", + "blueberries": "🫐", + "blush": "😊", + "boar": "🐗", + "boat": "⛵", + "bolivia": "🇧🇴", + "bomb": "💣", + "bone": "🦴", + "book": "📖", + "bookmark": "🔖", + "bookmark_tabs": "📑", + "books": "📚", + "boom": "💥", + "boomerang": "🪃", + "boot": "👢", + "bosnia_herzegovina": "🇧🇦", + "botswana": "🇧🇼", + "bouncing_ball_man": "⛹️‍♂️", + "bouncing_ball_person": "⛹️", + "bouncing_ball_woman": "⛹️‍♀️", + "bouquet": "💐", + "bouvet_island": "🇧🇻", + "bow": "🙇", + "bow_and_arrow": "🏹", + "bowing_man": "🙇‍♂️", + "bowing_woman": "🙇‍♀️", + "bowl_with_spoon": "🥣", + "bowling": "🎳", + "boxing_glove": "🥊", + "boy": "👦", + "brain": "🧠", + "brazil": "🇧🇷", + "bread": "🍞", + "breast_feeding": "🤱", + "bricks": "🧱", + "bride_with_veil": "👰‍♀️", + "bridge_at_night": "🌉", + "briefcase": "💼", + "british_indian_ocean_territory": "🇮🇴", + "british_virgin_islands": "🇻🇬", + "broccoli": "🥦", + "broken_heart": "💔", + "broom": "🧹", + "brown_circle": "🟤", + "brown_heart": "🤎", + "brown_square": "🟫", + "brunei": "🇧🇳", + "bubble_tea": "🧋", + "bucket": "🪣", + "bug": "🐛", + "building_construction": "🏗️", + "bulb": "💡", + "bulgaria": "🇧🇬", + "bullettrain_front": "🚅", + "bullettrain_side": "🚄", + "burkina_faso": "🇧🇫", + "burrito": "🌯", + "burundi": "🇧🇮", + "bus": "🚌", + "business_suit_levitating": "🕴️", + "busstop": "🚏", + "bust_in_silhouette": "👤", + "busts_in_silhouette": "👥", + "butter": "🧈", + "butterfly": "🦋", + "cactus": "🌵", + "cake": "🍰", + "calendar": "📆", + "call_me_hand": "🤙", + "calling": "📲", + "cambodia": "🇰🇭", + "camel": "🐫", + "camera": "📷", + "camera_flash": "📸", + "cameroon": "🇨🇲", + "camping": "🏕️", + "canada": "🇨🇦", + "canary_islands": "🇮🇨", + "cancer": "♋", + "candle": "🕯️", + "candy": "🍬", + "canned_food": "🥫", + "canoe": "🛶", + "cape_verde": "🇨🇻", + "capital_abcd": "🔠", + "capricorn": "♑", + "car": "🚗", + "card_file_box": "🗃️", + "card_index": "📇", + "card_index_dividers": "🗂️", + "caribbean_netherlands": "🇧🇶", + "carousel_horse": "🎠", + "carpentry_saw": "🪚", + "carrot": "🥕", + "cartwheeling": "🤸", + "cat": "🐱", + "cat2": "🐈", + "cayman_islands": "🇰🇾", + "cd": "💿", + "central_african_republic": "🇨🇫", + "ceuta_melilla": "🇪🇦", + "chad": "🇹🇩", + "chains": "⛓️", + "chair": "🪑", + "champagne": "🍾", + "chart": "💹", + "chart_with_downwards_trend": "📉", + "chart_with_upwards_trend": "📈", + "checkered_flag": "🏁", + "cheese": "🧀", + "cherries": "🍒", + "cherry_blossom": "🌸", + "chess_pawn": "♟️", + "chestnut": "🌰", + "chicken": "🐔", + "child": "🧒", + "children_crossing": "🚸", + "chile": "🇨🇱", + "chipmunk": "🐿️", + "chocolate_bar": "🍫", + "chopsticks": "🥢", + "christmas_island": "🇨🇽", + "christmas_tree": "🎄", + "church": "⛪", + "cinema": "🎦", + "circus_tent": "🎪", + "city_sunrise": "🌇", + "city_sunset": "🌆", + "cityscape": "🏙️", + "cl": "🆑", + "clamp": "🗜️", + "clap": "👏", + "clapper": "🎬", + "classical_building": "🏛️", + "climbing": "🧗", + "climbing_man": "🧗‍♂️", + "climbing_woman": "🧗‍♀️", + "clinking_glasses": "🥂", + "clipboard": "📋", + "clipperton_island": "🇨🇵", + "clock1": "🕐", + "clock10": "🕙", + "clock1030": "🕥", + "clock11": "🕚", + "clock1130": "🕦", + "clock12": "🕛", + "clock1230": "🕧", + "clock130": "🕜", + "clock2": "🕑", + "clock230": "🕝", + "clock3": "🕒", + "clock330": "🕞", + "clock4": "🕓", + "clock430": "🕟", + "clock5": "🕔", + "clock530": "🕠", + "clock6": "🕕", + "clock630": "🕡", + "clock7": "🕖", + "clock730": "🕢", + "clock8": "🕗", + "clock830": "🕣", + "clock9": "🕘", + "clock930": "🕤", + "closed_book": "📕", + "closed_lock_with_key": "🔐", + "closed_umbrella": "🌂", + "cloud": "☁️", + "cloud_with_lightning": "🌩️", + "cloud_with_lightning_and_rain": "⛈️", + "cloud_with_rain": "🌧️", + "cloud_with_snow": "🌨️", + "clown_face": "🤡", + "clubs": "♣️", + "cn": "🇨🇳", + "coat": "🧥", + "cockroach": "🪳", + "cocktail": "🍸", + "coconut": "🥥", + "cocos_islands": "🇨🇨", + "coffee": "☕", + "coffin": "⚰️", + "coin": "🪙", + "cold_face": "🥶", + "cold_sweat": "😰", + "collision": "💥", + "colombia": "🇨🇴", + "comet": "☄️", + "comoros": "🇰🇲", + "compass": "🧭", + "computer": "💻", + "computer_mouse": "🖱️", + "confetti_ball": "🎊", + "confounded": "😖", + "confused": "😕", + "congo_brazzaville": "🇨🇬", + "congo_kinshasa": "🇨🇩", + "congratulations": "㊗️", + "construction": "🚧", + "construction_worker": "👷", + "construction_worker_man": "👷‍♂️", + "construction_worker_woman": "👷‍♀️", + "control_knobs": "🎛️", + "convenience_store": "🏪", + "cook": "🧑‍🍳", + "cook_islands": "🇨🇰", + "cookie": "🍪", + "cool": "🆒", + "cop": "👮", + "copyright": "©️", + "corn": "🌽", + "costa_rica": "🇨🇷", + "cote_divoire": "🇨🇮", + "couch_and_lamp": "🛋️", + "couple": "👫", + "couple_with_heart": "💑", + "couple_with_heart_man_man": "👨‍❤️‍👨", + "couple_with_heart_woman_man": "👩‍❤️‍👨", + "couple_with_heart_woman_woman": "👩‍❤️‍👩", + "couplekiss": "💏", + "couplekiss_man_man": "👨‍❤️‍💋‍👨", + "couplekiss_man_woman": "👩‍❤️‍💋‍👨", + "couplekiss_woman_woman": "👩‍❤️‍💋‍👩", + "cow": "🐮", + "cow2": "🐄", + "cowboy_hat_face": "🤠", + "crab": "🦀", + "crayon": "🖍️", + "credit_card": "💳", + "crescent_moon": "🌙", + "cricket": "🦗", + "cricket_game": "🏏", + "croatia": "🇭🇷", + "crocodile": "🐊", + "croissant": "🥐", + "crossed_fingers": "🤞", + "crossed_flags": "🎌", + "crossed_swords": "⚔️", + "crown": "👑", + "cry": "😢", + "crying_cat_face": "😿", + "crystal_ball": "🔮", + "cuba": "🇨🇺", + "cucumber": "🥒", + "cup_with_straw": "🥤", + "cupcake": "🧁", + "cupid": "💘", + "curacao": "🇨🇼", + "curling_stone": "🥌", + "curly_haired_man": "👨‍🦱", + "curly_haired_woman": "👩‍🦱", + "curly_loop": "➰", + "currency_exchange": "💱", + "curry": "🍛", + "cursing_face": "🤬", + "custard": "🍮", + "customs": "🛃", + "cut_of_meat": "🥩", + "cyclone": "🌀", + "cyprus": "🇨🇾", + "czech_republic": "🇨🇿", + "dagger": "🗡️", + "dancer": "💃", + "dancers": "👯", + "dancing_men": "👯‍♂️", + "dancing_women": "👯‍♀️", + "dango": "🍡", + "dark_sunglasses": "🕶️", + "dart": "🎯", + "dash": "💨", + "date": "📅", + "de": "🇩🇪", + "deaf_man": "🧏‍♂️", + "deaf_person": "🧏", + "deaf_woman": "🧏‍♀️", + "deciduous_tree": "🌳", + "deer": "🦌", + "denmark": "🇩🇰", + "department_store": "🏬", + "derelict_house": "🏚️", + "desert": "🏜️", + "desert_island": "🏝️", + "desktop_computer": "🖥️", + "detective": "🕵️", + "diamond_shape_with_a_dot_inside": "💠", + "diamonds": "♦️", + "diego_garcia": "🇩🇬", + "disappointed": "😞", + "disappointed_relieved": "😥", + "disguised_face": "🥸", + "diving_mask": "🤿", + "diya_lamp": "🪔", + "dizzy": "💫", + "dizzy_face": "😵", + "djibouti": "🇩🇯", + "dna": "🧬", + "do_not_litter": "🚯", + "dodo": "🦤", + "dog": "🐶", + "dog2": "🐕", + "dollar": "💵", + "dolls": "🎎", + "dolphin": "🐬", + "dominica": "🇩🇲", + "dominican_republic": "🇩🇴", + "door": "🚪", + "doughnut": "🍩", + "dove": "🕊️", + "dragon": "🐉", + "dragon_face": "🐲", + "dress": "👗", + "dromedary_camel": "🐪", + "drooling_face": "🤤", + "drop_of_blood": "🩸", + "droplet": "💧", + "drum": "🥁", + "duck": "🦆", + "dumpling": "🥟", + "dvd": "📀", + "e-mail": "📧", + "eagle": "🦅", + "ear": "👂", + "ear_of_rice": "🌾", + "ear_with_hearing_aid": "🦻", + "earth_africa": "🌍", + "earth_americas": "🌎", + "earth_asia": "🌏", + "ecuador": "🇪🇨", + "egg": "🥚", + "eggplant": "🍆", + "egypt": "🇪🇬", + "eight": "8️⃣", + "eight_pointed_black_star": "✴️", + "eight_spoked_asterisk": "✳️", + "eject_button": "⏏️", + "el_salvador": "🇸🇻", + "electric_plug": "🔌", + "elephant": "🐘", + "elevator": "🛗", + "elf": "🧝", + "elf_man": "🧝‍♂️", + "elf_woman": "🧝‍♀️", + "email": "📧", + "end": "🔚", + "england": "🏴󠁧󠁢󠁥󠁮󠁧󠁿", + "envelope": "✉️", + "envelope_with_arrow": "📩", + "equatorial_guinea": "🇬🇶", + "eritrea": "🇪🇷", + "es": "🇪🇸", + "estonia": "🇪🇪", + "ethiopia": "🇪🇹", + "eu": "🇪🇺", + "euro": "💶", + "european_castle": "🏰", + "european_post_office": "🏤", + "european_union": "🇪🇺", + "evergreen_tree": "🌲", + "exclamation": "❗", + "exploding_head": "🤯", + "expressionless": "😑", + "eye": "👁️", + "eye_speech_bubble": "👁️‍🗨️", + "eyeglasses": "👓", + "eyes": "👀", + "face_exhaling": "😮‍💨", + "face_in_clouds": "😶‍🌫️", + "face_with_head_bandage": "🤕", + "face_with_spiral_eyes": "😵‍💫", + "face_with_thermometer": "🤒", + "facepalm": "🤦", + "facepunch": "👊", + "factory": "🏭", + "factory_worker": "🧑‍🏭", + "fairy": "🧚", + "fairy_man": "🧚‍♂️", + "fairy_woman": "🧚‍♀️", + "falafel": "🧆", + "falkland_islands": "🇫🇰", + "fallen_leaf": "🍂", + "family": "👪", + "family_man_boy": "👨‍👦", + "family_man_boy_boy": "👨‍👦‍👦", + "family_man_girl": "👨‍👧", + "family_man_girl_boy": "👨‍👧‍👦", + "family_man_girl_girl": "👨‍👧‍👧", + "family_man_man_boy": "👨‍👨‍👦", + "family_man_man_boy_boy": "👨‍👨‍👦‍👦", + "family_man_man_girl": "👨‍👨‍👧", + "family_man_man_girl_boy": "👨‍👨‍👧‍👦", + "family_man_man_girl_girl": "👨‍👨‍👧‍👧", + "family_man_woman_boy": "👨‍👩‍👦", + "family_man_woman_boy_boy": "👨‍👩‍👦‍👦", + "family_man_woman_girl": "👨‍👩‍👧", + "family_man_woman_girl_boy": "👨‍👩‍👧‍👦", + "family_man_woman_girl_girl": "👨‍👩‍👧‍👧", + "family_woman_boy": "👩‍👦", + "family_woman_boy_boy": "👩‍👦‍👦", + "family_woman_girl": "👩‍👧", + "family_woman_girl_boy": "👩‍👧‍👦", + "family_woman_girl_girl": "👩‍👧‍👧", + "family_woman_woman_boy": "👩‍👩‍👦", + "family_woman_woman_boy_boy": "👩‍👩‍👦‍👦", + "family_woman_woman_girl": "👩‍👩‍👧", + "family_woman_woman_girl_boy": "👩‍👩‍👧‍👦", + "family_woman_woman_girl_girl": "👩‍👩‍👧‍👧", + "farmer": "🧑‍🌾", + "faroe_islands": "🇫🇴", + "fast_forward": "⏩", + "fax": "📠", + "fearful": "😨", + "feather": "🪶", + "feet": "🐾", + "female_detective": "🕵️‍♀️", + "female_sign": "♀️", + "ferris_wheel": "🎡", + "ferry": "⛴️", + "field_hockey": "🏑", + "fiji": "🇫🇯", + "file_cabinet": "🗄️", + "file_folder": "📁", + "film_projector": "📽️", + "film_strip": "🎞️", + "finland": "🇫🇮", + "fire": "🔥", + "fire_engine": "🚒", + "fire_extinguisher": "🧯", + "firecracker": "🧨", + "firefighter": "🧑‍🚒", + "fireworks": "🎆", + "first_quarter_moon": "🌓", + "first_quarter_moon_with_face": "🌛", + "fish": "🐟", + "fish_cake": "🍥", + "fishing_pole_and_fish": "🎣", + "fist": "✊", + "fist_left": "🤛", + "fist_oncoming": "👊", + "fist_raised": "✊", + "fist_right": "🤜", + "five": "5️⃣", + "flags": "🎏", + "flamingo": "🦩", + "flashlight": "🔦", + "flat_shoe": "🥿", + "flatbread": "🫓", + "fleur_de_lis": "⚜️", + "flight_arrival": "🛬", + "flight_departure": "🛫", + "flipper": "🐬", + "floppy_disk": "💾", + "flower_playing_cards": "🎴", + "flushed": "😳", + "fly": "🪰", + "flying_disc": "🥏", + "flying_saucer": "🛸", + "fog": "🌫️", + "foggy": "🌁", + "fondue": "🫕", + "foot": "🦶", + "football": "🏈", + "footprints": "👣", + "fork_and_knife": "🍴", + "fortune_cookie": "🥠", + "fountain": "⛲", + "fountain_pen": "🖋️", + "four": "4️⃣", + "four_leaf_clover": "🍀", + "fox_face": "🦊", + "fr": "🇫🇷", + "framed_picture": "🖼️", + "free": "🆓", + "french_guiana": "🇬🇫", + "french_polynesia": "🇵🇫", + "french_southern_territories": "🇹🇫", + "fried_egg": "🍳", + "fried_shrimp": "🍤", + "fries": "🍟", + "frog": "🐸", + "frowning": "😦", + "frowning_face": "☹️", + "frowning_man": "🙍‍♂️", + "frowning_person": "🙍", + "frowning_woman": "🙍‍♀️", + "fu": "🖕", + "fuelpump": "⛽", + "full_moon": "🌕", + "full_moon_with_face": "🌝", + "funeral_urn": "⚱️", + "gabon": "🇬🇦", + "gambia": "🇬🇲", + "game_die": "🎲", + "garlic": "🧄", + "gb": "🇬🇧", + "gear": "⚙️", + "gem": "💎", + "gemini": "♊", + "genie": "🧞", + "genie_man": "🧞‍♂️", + "genie_woman": "🧞‍♀️", + "georgia": "🇬🇪", + "ghana": "🇬🇭", + "ghost": "👻", + "gibraltar": "🇬🇮", + "gift": "🎁", + "gift_heart": "💝", + "giraffe": "🦒", + "girl": "👧", + "globe_with_meridians": "🌐", + "gloves": "🧤", + "goal_net": "🥅", + "goat": "🐐", + "goggles": "🥽", + "golf": "⛳", + "golfing": "🏌️", + "golfing_man": "🏌️‍♂️", + "golfing_woman": "🏌️‍♀️", + "gorilla": "🦍", + "grapes": "🍇", + "greece": "🇬🇷", + "green_apple": "🍏", + "green_book": "📗", + "green_circle": "🟢", + "green_heart": "💚", + "green_salad": "🥗", + "green_square": "🟩", + "greenland": "🇬🇱", + "grenada": "🇬🇩", + "grey_exclamation": "❕", + "grey_question": "❔", + "grimacing": "😬", + "grin": "😁", + "grinning": "😀", + "guadeloupe": "🇬🇵", + "guam": "🇬🇺", + "guard": "💂", + "guardsman": "💂‍♂️", + "guardswoman": "💂‍♀️", + "guatemala": "🇬🇹", + "guernsey": "🇬🇬", + "guide_dog": "🦮", + "guinea": "🇬🇳", + "guinea_bissau": "🇬🇼", + "guitar": "🎸", + "gun": "🔫", + "guyana": "🇬🇾", + "haircut": "💇", + "haircut_man": "💇‍♂️", + "haircut_woman": "💇‍♀️", + "haiti": "🇭🇹", + "hamburger": "🍔", + "hammer": "🔨", + "hammer_and_pick": "⚒️", + "hammer_and_wrench": "🛠️", + "hamster": "🐹", + "hand": "✋", + "hand_over_mouth": "🤭", + "handbag": "👜", + "handball_person": "🤾", + "handshake": "🤝", + "hankey": "💩", + "hash": "#️⃣", + "hatched_chick": "🐥", + "hatching_chick": "🐣", + "headphones": "🎧", + "headstone": "🪦", + "health_worker": "🧑‍⚕️", + "hear_no_evil": "🙉", + "heard_mcdonald_islands": "🇭🇲", + "heart": "❤️", + "heart_decoration": "💟", + "heart_eyes": "😍", + "heart_eyes_cat": "😻", + "heart_on_fire": "❤️‍🔥", + "heartbeat": "💓", + "heartpulse": "💗", + "hearts": "♥️", + "heavy_check_mark": "✔️", + "heavy_division_sign": "➗", + "heavy_dollar_sign": "💲", + "heavy_exclamation_mark": "❗", + "heavy_heart_exclamation": "❣️", + "heavy_minus_sign": "➖", + "heavy_multiplication_x": "✖️", + "heavy_plus_sign": "➕", + "hedgehog": "🦔", + "helicopter": "🚁", + "herb": "🌿", + "hibiscus": "🌺", + "high_brightness": "🔆", + "high_heel": "👠", + "hiking_boot": "🥾", + "hindu_temple": "🛕", + "hippopotamus": "🦛", + "hocho": "🔪", + "hole": "🕳️", + "honduras": "🇭🇳", + "honey_pot": "🍯", + "honeybee": "🐝", + "hong_kong": "🇭🇰", + "hook": "🪝", + "horse": "🐴", + "horse_racing": "🏇", + "hospital": "🏥", + "hot_face": "🥵", + "hot_pepper": "🌶️", + "hotdog": "🌭", + "hotel": "🏨", + "hotsprings": "♨️", + "hourglass": "⌛", + "hourglass_flowing_sand": "⏳", + "house": "🏠", + "house_with_garden": "🏡", + "houses": "🏘️", + "hugs": "🤗", + "hungary": "🇭🇺", + "hushed": "😯", + "hut": "🛖", + "ice_cream": "🍨", + "ice_cube": "🧊", + "ice_hockey": "🏒", + "ice_skate": "⛸️", + "icecream": "🍦", + "iceland": "🇮🇸", + "id": "🆔", + "ideograph_advantage": "🉐", + "imp": "👿", + "inbox_tray": "📥", + "incoming_envelope": "📨", + "india": "🇮🇳", + "indonesia": "🇮🇩", + "infinity": "♾️", + "information_desk_person": "💁", + "information_source": "ℹ️", + "innocent": "😇", + "interrobang": "⁉️", + "iphone": "📱", + "iran": "🇮🇷", + "iraq": "🇮🇶", + "ireland": "🇮🇪", + "isle_of_man": "🇮🇲", + "israel": "🇮🇱", + "it": "🇮🇹", + "izakaya_lantern": "🏮", + "jack_o_lantern": "🎃", + "jamaica": "🇯🇲", + "japan": "🗾", + "japanese_castle": "🏯", + "japanese_goblin": "👺", + "japanese_ogre": "👹", + "jeans": "👖", + "jersey": "🇯🇪", + "jigsaw": "🧩", + "jordan": "🇯🇴", + "joy": "😂", + "joy_cat": "😹", + "joystick": "🕹️", + "jp": "🇯🇵", + "judge": "🧑‍⚖️", + "juggling_person": "🤹", + "kaaba": "🕋", + "kangaroo": "🦘", + "kazakhstan": "🇰🇿", + "kenya": "🇰🇪", + "key": "🔑", + "keyboard": "⌨️", + "keycap_ten": "🔟", + "kick_scooter": "🛴", + "kimono": "👘", + "kiribati": "🇰🇮", + "kiss": "💋", + "kissing": "😗", + "kissing_cat": "😽", + "kissing_closed_eyes": "😚", + "kissing_heart": "😘", + "kissing_smiling_eyes": "😙", + "kite": "🪁", + "kiwi_fruit": "🥝", + "kneeling_man": "🧎‍♂️", + "kneeling_person": "🧎", + "kneeling_woman": "🧎‍♀️", + "knife": "🔪", + "knot": "🪢", + "koala": "🐨", + "koko": "🈁", + "kosovo": "🇽🇰", + "kr": "🇰🇷", + "kuwait": "🇰🇼", + "kyrgyzstan": "🇰🇬", + "lab_coat": "🥼", + "label": "🏷️", + "lacrosse": "🥍", + "ladder": "🪜", + "lady_beetle": "🐞", + "lantern": "🏮", + "laos": "🇱🇦", + "large_blue_circle": "🔵", + "large_blue_diamond": "🔷", + "large_orange_diamond": "🔶", + "last_quarter_moon": "🌗", + "last_quarter_moon_with_face": "🌜", + "latin_cross": "✝️", + "latvia": "🇱🇻", + "laughing": "😆", + "leafy_green": "🥬", + "leaves": "🍃", + "lebanon": "🇱🇧", + "ledger": "📒", + "left_luggage": "🛅", + "left_right_arrow": "↔️", + "left_speech_bubble": "🗨️", + "leftwards_arrow_with_hook": "↩️", + "leg": "🦵", + "lemon": "🍋", + "leo": "♌", + "leopard": "🐆", + "lesotho": "🇱🇸", + "level_slider": "🎚️", + "liberia": "🇱🇷", + "libra": "♎", + "libya": "🇱🇾", + "liechtenstein": "🇱🇮", + "light_rail": "🚈", + "link": "🔗", + "lion": "🦁", + "lips": "👄", + "lipstick": "💄", + "lithuania": "🇱🇹", + "lizard": "🦎", + "llama": "🦙", + "lobster": "🦞", + "lock": "🔒", + "lock_with_ink_pen": "🔏", + "lollipop": "🍭", + "long_drum": "🪘", + "loop": "➿", + "lotion_bottle": "🧴", + "lotus_position": "🧘", + "lotus_position_man": "🧘‍♂️", + "lotus_position_woman": "🧘‍♀️", + "loud_sound": "🔊", + "loudspeaker": "📢", + "love_hotel": "🏩", + "love_letter": "💌", + "love_you_gesture": "🤟", + "low_brightness": "🔅", + "luggage": "🧳", + "lungs": "🫁", + "luxembourg": "🇱🇺", + "lying_face": "🤥", + "m": "Ⓜ️", + "macau": "🇲🇴", + "macedonia": "🇲🇰", + "madagascar": "🇲🇬", + "mag": "🔍", + "mag_right": "🔎", + "mage": "🧙", + "mage_man": "🧙‍♂️", + "mage_woman": "🧙‍♀️", + "magic_wand": "🪄", + "magnet": "🧲", + "mahjong": "🀄", + "mailbox": "📫", + "mailbox_closed": "📪", + "mailbox_with_mail": "📬", + "mailbox_with_no_mail": "📭", + "malawi": "🇲🇼", + "malaysia": "🇲🇾", + "maldives": "🇲🇻", + "male_detective": "🕵️‍♂️", + "male_sign": "♂️", + "mali": "🇲🇱", + "malta": "🇲🇹", + "mammoth": "🦣", + "man": "👨", + "man_artist": "👨‍🎨", + "man_astronaut": "👨‍🚀", + "man_beard": "🧔‍♂️", + "man_cartwheeling": "🤸‍♂️", + "man_cook": "👨‍🍳", + "man_dancing": "🕺", + "man_facepalming": "🤦‍♂️", + "man_factory_worker": "👨‍🏭", + "man_farmer": "👨‍🌾", + "man_feeding_baby": "👨‍🍼", + "man_firefighter": "👨‍🚒", + "man_health_worker": "👨‍⚕️", + "man_in_manual_wheelchair": "👨‍🦽", + "man_in_motorized_wheelchair": "👨‍🦼", + "man_in_tuxedo": "🤵‍♂️", + "man_judge": "👨‍⚖️", + "man_juggling": "🤹‍♂️", + "man_mechanic": "👨‍🔧", + "man_office_worker": "👨‍💼", + "man_pilot": "👨‍✈️", + "man_playing_handball": "🤾‍♂️", + "man_playing_water_polo": "🤽‍♂️", + "man_scientist": "👨‍🔬", + "man_shrugging": "🤷‍♂️", + "man_singer": "👨‍🎤", + "man_student": "👨‍🎓", + "man_teacher": "👨‍🏫", + "man_technologist": "👨‍💻", + "man_with_gua_pi_mao": "👲", + "man_with_probing_cane": "👨‍🦯", + "man_with_turban": "👳‍♂️", + "man_with_veil": "👰‍♂️", + "mandarin": "🍊", + "mango": "🥭", + "mans_shoe": "👞", + "mantelpiece_clock": "🕰️", + "manual_wheelchair": "🦽", + "maple_leaf": "🍁", + "marshall_islands": "🇲🇭", + "martial_arts_uniform": "🥋", + "martinique": "🇲🇶", + "mask": "😷", + "massage": "💆", + "massage_man": "💆‍♂️", + "massage_woman": "💆‍♀️", + "mate": "🧉", + "mauritania": "🇲🇷", + "mauritius": "🇲🇺", + "mayotte": "🇾🇹", + "meat_on_bone": "🍖", + "mechanic": "🧑‍🔧", + "mechanical_arm": "🦾", + "mechanical_leg": "🦿", + "medal_military": "🎖️", + "medal_sports": "🏅", + "medical_symbol": "⚕️", + "mega": "📣", + "melon": "🍈", + "memo": "📝", + "men_wrestling": "🤼‍♂️", + "mending_heart": "❤️‍🩹", + "menorah": "🕎", + "mens": "🚹", + "mermaid": "🧜‍♀️", + "merman": "🧜‍♂️", + "merperson": "🧜", + "metal": "🤘", + "metro": "🚇", + "mexico": "🇲🇽", + "microbe": "🦠", + "micronesia": "🇫🇲", + "microphone": "🎤", + "microscope": "🔬", + "middle_finger": "🖕", + "military_helmet": "🪖", + "milk_glass": "🥛", + "milky_way": "🌌", + "minibus": "🚐", + "minidisc": "💽", + "mirror": "🪞", + "mobile_phone_off": "📴", + "moldova": "🇲🇩", + "monaco": "🇲🇨", + "money_mouth_face": "🤑", + "money_with_wings": "💸", + "moneybag": "💰", + "mongolia": "🇲🇳", + "monkey": "🐒", + "monkey_face": "🐵", + "monocle_face": "🧐", + "monorail": "🚝", + "montenegro": "🇲🇪", + "montserrat": "🇲🇸", + "moon": "🌔", + "moon_cake": "🥮", + "morocco": "🇲🇦", + "mortar_board": "🎓", + "mosque": "🕌", + "mosquito": "🦟", + "motor_boat": "🛥️", + "motor_scooter": "🛵", + "motorcycle": "🏍️", + "motorized_wheelchair": "🦼", + "motorway": "🛣️", + "mount_fuji": "🗻", + "mountain": "⛰️", + "mountain_bicyclist": "🚵", + "mountain_biking_man": "🚵‍♂️", + "mountain_biking_woman": "🚵‍♀️", + "mountain_cableway": "🚠", + "mountain_railway": "🚞", + "mountain_snow": "🏔️", + "mouse": "🐭", + "mouse2": "🐁", + "mouse_trap": "🪤", + "movie_camera": "🎥", + "moyai": "🗿", + "mozambique": "🇲🇿", + "mrs_claus": "🤶", + "muscle": "💪", + "mushroom": "🍄", + "musical_keyboard": "🎹", + "musical_note": "🎵", + "musical_score": "🎼", + "mute": "🔇", + "mx_claus": "🧑‍🎄", + "myanmar": "🇲🇲", + "nail_care": "💅", + "name_badge": "📛", + "namibia": "🇳🇦", + "national_park": "🏞️", + "nauru": "🇳🇷", + "nauseated_face": "🤢", + "nazar_amulet": "🧿", + "necktie": "👔", + "negative_squared_cross_mark": "❎", + "nepal": "🇳🇵", + "nerd_face": "🤓", + "nesting_dolls": "🪆", + "netherlands": "🇳🇱", + "neutral_face": "😐", + "new": "🆕", + "new_caledonia": "🇳🇨", + "new_moon": "🌑", + "new_moon_with_face": "🌚", + "new_zealand": "🇳🇿", + "newspaper": "📰", + "newspaper_roll": "🗞️", + "next_track_button": "⏭️", + "ng": "🆖", + "ng_man": "🙅‍♂️", + "ng_woman": "🙅‍♀️", + "nicaragua": "🇳🇮", + "niger": "🇳🇪", + "nigeria": "🇳🇬", + "night_with_stars": "🌃", + "nine": "9️⃣", + "ninja": "🥷", + "niue": "🇳🇺", + "no_bell": "🔕", + "no_bicycles": "🚳", + "no_entry": "⛔", + "no_entry_sign": "🚫", + "no_good": "🙅", + "no_good_man": "🙅‍♂️", + "no_good_woman": "🙅‍♀️", + "no_mobile_phones": "📵", + "no_mouth": "😶", + "no_pedestrians": "🚷", + "no_smoking": "🚭", + "non-potable_water": "🚱", + "norfolk_island": "🇳🇫", + "north_korea": "🇰🇵", + "northern_mariana_islands": "🇲🇵", + "norway": "🇳🇴", + "nose": "👃", + "notebook": "📓", + "notebook_with_decorative_cover": "📔", + "notes": "🎶", + "nut_and_bolt": "🔩", + "o": "⭕", + "o2": "🅾️", + "ocean": "🌊", + "octopus": "🐙", + "oden": "🍢", + "office": "🏢", + "office_worker": "🧑‍💼", + "oil_drum": "🛢️", + "ok": "🆗", + "ok_hand": "👌", + "ok_man": "🙆‍♂️", + "ok_person": "🙆", + "ok_woman": "🙆‍♀️", + "old_key": "🗝️", + "older_adult": "🧓", + "older_man": "👴", + "older_woman": "👵", + "olive": "🫒", + "om": "🕉️", + "oman": "🇴🇲", + "on": "🔛", + "oncoming_automobile": "🚘", + "oncoming_bus": "🚍", + "oncoming_police_car": "🚔", + "oncoming_taxi": "🚖", + "one": "1️⃣", + "one_piece_swimsuit": "🩱", + "onion": "🧅", + "open_book": "📖", + "open_file_folder": "📂", + "open_hands": "👐", + "open_mouth": "😮", + "open_umbrella": "☂️", + "ophiuchus": "⛎", + "orange": "🍊", + "orange_book": "📙", + "orange_circle": "🟠", + "orange_heart": "🧡", + "orange_square": "🟧", + "orangutan": "🦧", + "orthodox_cross": "☦️", + "otter": "🦦", + "outbox_tray": "📤", + "owl": "🦉", + "ox": "🐂", + "oyster": "🦪", + "package": "📦", + "page_facing_up": "📄", + "page_with_curl": "📃", + "pager": "📟", + "paintbrush": "🖌️", + "pakistan": "🇵🇰", + "palau": "🇵🇼", + "palestinian_territories": "🇵🇸", + "palm_tree": "🌴", + "palms_up_together": "🤲", + "panama": "🇵🇦", + "pancakes": "🥞", + "panda_face": "🐼", + "paperclip": "📎", + "paperclips": "🖇️", + "papua_new_guinea": "🇵🇬", + "parachute": "🪂", + "paraguay": "🇵🇾", + "parasol_on_ground": "⛱️", + "parking": "🅿️", + "parrot": "🦜", + "part_alternation_mark": "〽️", + "partly_sunny": "⛅", + "partying_face": "🥳", + "passenger_ship": "🛳️", + "passport_control": "🛂", + "pause_button": "⏸️", + "paw_prints": "🐾", + "peace_symbol": "☮️", + "peach": "🍑", + "peacock": "🦚", + "peanuts": "🥜", + "pear": "🍐", + "pen": "🖊️", + "pencil": "📝", + "pencil2": "✏️", + "penguin": "🐧", + "pensive": "😔", + "people_holding_hands": "🧑‍🤝‍🧑", + "people_hugging": "🫂", + "performing_arts": "🎭", + "persevere": "😣", + "person_bald": "🧑‍🦲", + "person_curly_hair": "🧑‍🦱", + "person_feeding_baby": "🧑‍🍼", + "person_fencing": "🤺", + "person_in_manual_wheelchair": "🧑‍🦽", + "person_in_motorized_wheelchair": "🧑‍🦼", + "person_in_tuxedo": "🤵", + "person_red_hair": "🧑‍🦰", + "person_white_hair": "🧑‍🦳", + "person_with_probing_cane": "🧑‍🦯", + "person_with_turban": "👳", + "person_with_veil": "👰", + "peru": "🇵🇪", + "petri_dish": "🧫", + "philippines": "🇵🇭", + "phone": "☎️", + "pick": "⛏️", + "pickup_truck": "🛻", + "pie": "🥧", + "pig": "🐷", + "pig2": "🐖", + "pig_nose": "🐽", + "pill": "💊", + "pilot": "🧑‍✈️", + "pinata": "🪅", + "pinched_fingers": "🤌", + "pinching_hand": "🤏", + "pineapple": "🍍", + "ping_pong": "🏓", + "pirate_flag": "🏴‍☠️", + "pisces": "♓", + "pitcairn_islands": "🇵🇳", + "pizza": "🍕", + "placard": "🪧", + "place_of_worship": "🛐", + "plate_with_cutlery": "🍽️", + "play_or_pause_button": "⏯️", + "pleading_face": "🥺", + "plunger": "🪠", + "point_down": "👇", + "point_left": "👈", + "point_right": "👉", + "point_up": "☝️", + "point_up_2": "👆", + "poland": "🇵🇱", + "polar_bear": "🐻‍❄️", + "police_car": "🚓", + "police_officer": "👮", + "policeman": "👮‍♂️", + "policewoman": "👮‍♀️", + "poodle": "🐩", + "poop": "💩", + "popcorn": "🍿", + "portugal": "🇵🇹", + "post_office": "🏣", + "postal_horn": "📯", + "postbox": "📮", + "potable_water": "🚰", + "potato": "🥔", + "potted_plant": "🪴", + "pouch": "👝", + "poultry_leg": "🍗", + "pound": "💷", + "pout": "😡", + "pouting_cat": "😾", + "pouting_face": "🙎", + "pouting_man": "🙎‍♂️", + "pouting_woman": "🙎‍♀️", + "pray": "🙏", + "prayer_beads": "📿", + "pregnant_woman": "🤰", + "pretzel": "🥨", + "previous_track_button": "⏮️", + "prince": "🤴", + "princess": "👸", + "printer": "🖨️", + "probing_cane": "🦯", + "puerto_rico": "🇵🇷", + "punch": "👊", + "purple_circle": "🟣", + "purple_heart": "💜", + "purple_square": "🟪", + "purse": "👛", + "pushpin": "📌", + "put_litter_in_its_place": "🚮", + "qatar": "🇶🇦", + "question": "❓", + "rabbit": "🐰", + "rabbit2": "🐇", + "raccoon": "🦝", + "racehorse": "🐎", + "racing_car": "🏎️", + "radio": "📻", + "radio_button": "🔘", + "radioactive": "☢️", + "rage": "😡", + "railway_car": "🚃", + "railway_track": "🛤️", + "rainbow": "🌈", + "rainbow_flag": "🏳️‍🌈", + "raised_back_of_hand": "🤚", + "raised_eyebrow": "🤨", + "raised_hand": "✋", + "raised_hand_with_fingers_splayed": "🖐️", + "raised_hands": "🙌", + "raising_hand": "🙋", + "raising_hand_man": "🙋‍♂️", + "raising_hand_woman": "🙋‍♀️", + "ram": "🐏", + "ramen": "🍜", + "rat": "🐀", + "razor": "🪒", + "receipt": "🧾", + "record_button": "⏺️", + "recycle": "♻️", + "red_car": "🚗", + "red_circle": "🔴", + "red_envelope": "🧧", + "red_haired_man": "👨‍🦰", + "red_haired_woman": "👩‍🦰", + "red_square": "🟥", + "registered": "®️", + "relaxed": "☺️", + "relieved": "😌", + "reminder_ribbon": "🎗️", + "repeat": "🔁", + "repeat_one": "🔂", + "rescue_worker_helmet": "⛑️", + "restroom": "🚻", + "reunion": "🇷🇪", + "revolving_hearts": "💞", + "rewind": "⏪", + "rhinoceros": "🦏", + "ribbon": "🎀", + "rice": "🍚", + "rice_ball": "🍙", + "rice_cracker": "🍘", + "rice_scene": "🎑", + "right_anger_bubble": "🗯️", + "ring": "💍", + "ringed_planet": "🪐", + "robot": "🤖", + "rock": "🪨", + "rocket": "🚀", + "rofl": "🤣", + "roll_eyes": "🙄", + "roll_of_paper": "🧻", + "roller_coaster": "🎢", + "roller_skate": "🛼", + "romania": "🇷🇴", + "rooster": "🐓", + "rose": "🌹", + "rosette": "🏵️", + "rotating_light": "🚨", + "round_pushpin": "📍", + "rowboat": "🚣", + "rowing_man": "🚣‍♂️", + "rowing_woman": "🚣‍♀️", + "ru": "🇷🇺", + "rugby_football": "🏉", + "runner": "🏃", + "running": "🏃", + "running_man": "🏃‍♂️", + "running_shirt_with_sash": "🎽", + "running_woman": "🏃‍♀️", + "rwanda": "🇷🇼", + "sa": "🈂️", + "safety_pin": "🧷", + "safety_vest": "🦺", + "sagittarius": "♐", + "sailboat": "⛵", + "sake": "🍶", + "salt": "🧂", + "samoa": "🇼🇸", + "san_marino": "🇸🇲", + "sandal": "👡", + "sandwich": "🥪", + "santa": "🎅", + "sao_tome_principe": "🇸🇹", + "sari": "🥻", + "sassy_man": "💁‍♂️", + "sassy_woman": "💁‍♀️", + "satellite": "📡", + "satisfied": "😆", + "saudi_arabia": "🇸🇦", + "sauna_man": "🧖‍♂️", + "sauna_person": "🧖", + "sauna_woman": "🧖‍♀️", + "sauropod": "🦕", + "saxophone": "🎷", + "scarf": "🧣", + "school": "🏫", + "school_satchel": "🎒", + "scientist": "🧑‍🔬", + "scissors": "✂️", + "scorpion": "🦂", + "scorpius": "♏", + "scotland": "🏴󠁧󠁢󠁳󠁣󠁴󠁿", + "scream": "😱", + "scream_cat": "🙀", + "screwdriver": "🪛", + "scroll": "📜", + "seal": "🦭", + "seat": "💺", + "secret": "㊙️", + "see_no_evil": "🙈", + "seedling": "🌱", + "selfie": "🤳", + "senegal": "🇸🇳", + "serbia": "🇷🇸", + "service_dog": "🐕‍🦺", + "seven": "7️⃣", + "sewing_needle": "🪡", + "seychelles": "🇸🇨", + "shallow_pan_of_food": "🥘", + "shamrock": "☘️", + "shark": "🦈", + "shaved_ice": "🍧", + "sheep": "🐑", + "shell": "🐚", + "shield": "🛡️", + "shinto_shrine": "⛩️", + "ship": "🚢", + "shirt": "👕", + "shit": "💩", + "shoe": "👞", + "shopping": "🛍️", + "shopping_cart": "🛒", + "shorts": "🩳", + "shower": "🚿", + "shrimp": "🦐", + "shrug": "🤷", + "shushing_face": "🤫", + "sierra_leone": "🇸🇱", + "signal_strength": "📶", + "singapore": "🇸🇬", + "singer": "🧑‍🎤", + "sint_maarten": "🇸🇽", + "six": "6️⃣", + "six_pointed_star": "🔯", + "skateboard": "🛹", + "ski": "🎿", + "skier": "⛷️", + "skull": "💀", + "skull_and_crossbones": "☠️", + "skunk": "🦨", + "sled": "🛷", + "sleeping": "😴", + "sleeping_bed": "🛌", + "sleepy": "😪", + "slightly_frowning_face": "🙁", + "slightly_smiling_face": "🙂", + "slot_machine": "🎰", + "sloth": "🦥", + "slovakia": "🇸🇰", + "slovenia": "🇸🇮", + "small_airplane": "🛩️", + "small_blue_diamond": "🔹", + "small_orange_diamond": "🔸", + "small_red_triangle": "🔺", + "small_red_triangle_down": "🔻", + "smile": "😄", + "smile_cat": "😸", + "smiley": "😃", + "smiley_cat": "😺", + "smiling_face_with_tear": "🥲", + "smiling_face_with_three_hearts": "🥰", + "smiling_imp": "😈", + "smirk": "😏", + "smirk_cat": "😼", + "smoking": "🚬", + "snail": "🐌", + "snake": "🐍", + "sneezing_face": "🤧", + "snowboarder": "🏂", + "snowflake": "❄️", + "snowman": "⛄", + "snowman_with_snow": "☃️", + "soap": "🧼", + "sob": "😭", + "soccer": "⚽", + "socks": "🧦", + "softball": "🥎", + "solomon_islands": "🇸🇧", + "somalia": "🇸🇴", + "soon": "🔜", + "sos": "🆘", + "sound": "🔉", + "south_africa": "🇿🇦", + "south_georgia_south_sandwich_islands": "🇬🇸", + "south_sudan": "🇸🇸", + "space_invader": "👾", + "spades": "♠️", + "spaghetti": "🍝", + "sparkle": "❇️", + "sparkler": "🎇", + "sparkles": "✨", + "sparkling_heart": "💖", + "speak_no_evil": "🙊", + "speaker": "🔈", + "speaking_head": "🗣️", + "speech_balloon": "💬", + "speedboat": "🚤", + "spider": "🕷️", + "spider_web": "🕸️", + "spiral_calendar": "🗓️", + "spiral_notepad": "🗒️", + "sponge": "🧽", + "spoon": "🥄", + "squid": "🦑", + "sri_lanka": "🇱🇰", + "st_barthelemy": "🇧🇱", + "st_helena": "🇸🇭", + "st_kitts_nevis": "🇰🇳", + "st_lucia": "🇱🇨", + "st_martin": "🇲🇫", + "st_pierre_miquelon": "🇵🇲", + "st_vincent_grenadines": "🇻🇨", + "stadium": "🏟️", + "standing_man": "🧍‍♂️", + "standing_person": "🧍", + "standing_woman": "🧍‍♀️", + "star": "⭐", + "star2": "🌟", + "star_and_crescent": "☪️", + "star_of_david": "✡️", + "star_struck": "🤩", + "stars": "🌠", + "station": "🚉", + "statue_of_liberty": "🗽", + "steam_locomotive": "🚂", + "stethoscope": "🩺", + "stew": "🍲", + "stop_button": "⏹️", + "stop_sign": "🛑", + "stopwatch": "⏱️", + "straight_ruler": "📏", + "strawberry": "🍓", + "stuck_out_tongue": "😛", + "stuck_out_tongue_closed_eyes": "😝", + "stuck_out_tongue_winking_eye": "😜", + "student": "🧑‍🎓", + "studio_microphone": "🎙️", + "stuffed_flatbread": "🥙", + "sudan": "🇸🇩", + "sun_behind_large_cloud": "🌥️", + "sun_behind_rain_cloud": "🌦️", + "sun_behind_small_cloud": "🌤️", + "sun_with_face": "🌞", + "sunflower": "🌻", + "sunglasses": "😎", + "sunny": "☀️", + "sunrise": "🌅", + "sunrise_over_mountains": "🌄", + "superhero": "🦸", + "superhero_man": "🦸‍♂️", + "superhero_woman": "🦸‍♀️", + "supervillain": "🦹", + "supervillain_man": "🦹‍♂️", + "supervillain_woman": "🦹‍♀️", + "surfer": "🏄", + "surfing_man": "🏄‍♂️", + "surfing_woman": "🏄‍♀️", + "suriname": "🇸🇷", + "sushi": "🍣", + "suspension_railway": "🚟", + "svalbard_jan_mayen": "🇸🇯", + "swan": "🦢", + "swaziland": "🇸🇿", + "sweat": "😓", + "sweat_drops": "💦", + "sweat_smile": "😅", + "sweden": "🇸🇪", + "sweet_potato": "🍠", + "swim_brief": "🩲", + "swimmer": "🏊", + "swimming_man": "🏊‍♂️", + "swimming_woman": "🏊‍♀️", + "switzerland": "🇨🇭", + "symbols": "🔣", + "synagogue": "🕍", + "syria": "🇸🇾", + "syringe": "💉", + "t-rex": "🦖", + "taco": "🌮", + "tada": "🎉", + "taiwan": "🇹🇼", + "tajikistan": "🇹🇯", + "takeout_box": "🥡", + "tamale": "🫔", + "tanabata_tree": "🎋", + "tangerine": "🍊", + "tanzania": "🇹🇿", + "taurus": "♉", + "taxi": "🚕", + "tea": "🍵", + "teacher": "🧑‍🏫", + "teapot": "🫖", + "technologist": "🧑‍💻", + "teddy_bear": "🧸", + "telephone": "☎️", + "telephone_receiver": "📞", + "telescope": "🔭", + "tennis": "🎾", + "tent": "⛺", + "test_tube": "🧪", + "thailand": "🇹🇭", + "thermometer": "🌡️", + "thinking": "🤔", + "thong_sandal": "🩴", + "thought_balloon": "💭", + "thread": "🧵", + "three": "3️⃣", + "thumbsdown": "👎", + "thumbsup": "👍", + "ticket": "🎫", + "tickets": "🎟️", + "tiger": "🐯", + "tiger2": "🐅", + "timer_clock": "⏲️", + "timor_leste": "🇹🇱", + "tipping_hand_man": "💁‍♂️", + "tipping_hand_person": "💁", + "tipping_hand_woman": "💁‍♀️", + "tired_face": "😫", + "tm": "™️", + "togo": "🇹🇬", + "toilet": "🚽", + "tokelau": "🇹🇰", + "tokyo_tower": "🗼", + "tomato": "🍅", + "tonga": "🇹🇴", + "tongue": "👅", + "toolbox": "🧰", + "tooth": "🦷", + "toothbrush": "🪥", + "top": "🔝", + "tophat": "🎩", + "tornado": "🌪️", + "tr": "🇹🇷", + "trackball": "🖲️", + "tractor": "🚜", + "traffic_light": "🚥", + "train": "🚋", + "train2": "🚆", + "tram": "🚊", + "transgender_flag": "🏳️‍⚧️", + "transgender_symbol": "⚧️", + "triangular_flag_on_post": "🚩", + "triangular_ruler": "📐", + "trident": "🔱", + "trinidad_tobago": "🇹🇹", + "tristan_da_cunha": "🇹🇦", + "triumph": "😤", + "trolleybus": "🚎", + "trophy": "🏆", + "tropical_drink": "🍹", + "tropical_fish": "🐠", + "truck": "🚚", + "trumpet": "🎺", + "tshirt": "👕", + "tulip": "🌷", + "tumbler_glass": "🥃", + "tunisia": "🇹🇳", + "turkey": "🦃", + "turkmenistan": "🇹🇲", + "turks_caicos_islands": "🇹🇨", + "turtle": "🐢", + "tuvalu": "🇹🇻", + "tv": "📺", + "twisted_rightwards_arrows": "🔀", + "two": "2️⃣", + "two_hearts": "💕", + "two_men_holding_hands": "👬", + "two_women_holding_hands": "👭", + "u5272": "🈹", + "u5408": "🈴", + "u55b6": "🈺", + "u6307": "🈯", + "u6708": "🈷️", + "u6709": "🈶", + "u6e80": "🈵", + "u7121": "🈚", + "u7533": "🈸", + "u7981": "🈲", + "u7a7a": "🈳", + "uganda": "🇺🇬", + "uk": "🇬🇧", + "ukraine": "🇺🇦", + "umbrella": "☔", + "unamused": "😒", + "underage": "🔞", + "unicorn": "🦄", + "united_arab_emirates": "🇦🇪", + "united_nations": "🇺🇳", + "unlock": "🔓", + "up": "🆙", + "upside_down_face": "🙃", + "uruguay": "🇺🇾", + "us": "🇺🇸", + "us_outlying_islands": "🇺🇲", + "us_virgin_islands": "🇻🇮", + "uzbekistan": "🇺🇿", + "v": "✌️", + "vampire": "🧛", + "vampire_man": "🧛‍♂️", + "vampire_woman": "🧛‍♀️", + "vanuatu": "🇻🇺", + "vatican_city": "🇻🇦", + "venezuela": "🇻🇪", + "vertical_traffic_light": "🚦", + "vhs": "📼", + "vibration_mode": "📳", + "video_camera": "📹", + "video_game": "🎮", + "vietnam": "🇻🇳", + "violin": "🎻", + "virgo": "♍", + "volcano": "🌋", + "volleyball": "🏐", + "vomiting_face": "🤮", + "vs": "🆚", + "vulcan_salute": "🖖", + "waffle": "🧇", + "wales": "🏴󠁧󠁢󠁷󠁬󠁳󠁿", + "walking": "🚶", + "walking_man": "🚶‍♂️", + "walking_woman": "🚶‍♀️", + "wallis_futuna": "🇼🇫", + "waning_crescent_moon": "🌘", + "waning_gibbous_moon": "🌖", + "warning": "⚠️", + "wastebasket": "🗑️", + "watch": "⌚", + "water_buffalo": "🐃", + "water_polo": "🤽", + "watermelon": "🍉", + "wave": "👋", + "wavy_dash": "〰️", + "waxing_crescent_moon": "🌒", + "waxing_gibbous_moon": "🌔", + "wc": "🚾", + "weary": "😩", + "wedding": "💒", + "weight_lifting": "🏋️", + "weight_lifting_man": "🏋️‍♂️", + "weight_lifting_woman": "🏋️‍♀️", + "western_sahara": "🇪🇭", + "whale": "🐳", + "whale2": "🐋", + "wheel_of_dharma": "☸️", + "wheelchair": "♿", + "white_check_mark": "✅", + "white_circle": "⚪", + "white_flag": "🏳️", + "white_flower": "💮", + "white_haired_man": "👨‍🦳", + "white_haired_woman": "👩‍🦳", + "white_heart": "🤍", + "white_large_square": "⬜", + "white_medium_small_square": "◽", + "white_medium_square": "◻️", + "white_small_square": "▫️", + "white_square_button": "🔳", + "wilted_flower": "🥀", + "wind_chime": "🎐", + "wind_face": "🌬️", + "window": "🪟", + "wine_glass": "🍷", + "wink": "😉", + "wolf": "🐺", + "woman": "👩", + "woman_artist": "👩‍🎨", + "woman_astronaut": "👩‍🚀", + "woman_beard": "🧔‍♀️", + "woman_cartwheeling": "🤸‍♀️", + "woman_cook": "👩‍🍳", + "woman_dancing": "💃", + "woman_facepalming": "🤦‍♀️", + "woman_factory_worker": "👩‍🏭", + "woman_farmer": "👩‍🌾", + "woman_feeding_baby": "👩‍🍼", + "woman_firefighter": "👩‍🚒", + "woman_health_worker": "👩‍⚕️", + "woman_in_manual_wheelchair": "👩‍🦽", + "woman_in_motorized_wheelchair": "👩‍🦼", + "woman_in_tuxedo": "🤵‍♀️", + "woman_judge": "👩‍⚖️", + "woman_juggling": "🤹‍♀️", + "woman_mechanic": "👩‍🔧", + "woman_office_worker": "👩‍💼", + "woman_pilot": "👩‍✈️", + "woman_playing_handball": "🤾‍♀️", + "woman_playing_water_polo": "🤽‍♀️", + "woman_scientist": "👩‍🔬", + "woman_shrugging": "🤷‍♀️", + "woman_singer": "👩‍🎤", + "woman_student": "👩‍🎓", + "woman_teacher": "👩‍🏫", + "woman_technologist": "👩‍💻", + "woman_with_headscarf": "🧕", + "woman_with_probing_cane": "👩‍🦯", + "woman_with_turban": "👳‍♀️", + "woman_with_veil": "👰‍♀️", + "womans_clothes": "👚", + "womans_hat": "👒", + "women_wrestling": "🤼‍♀️", + "womens": "🚺", + "wood": "🪵", + "woozy_face": "🥴", + "world_map": "🗺️", + "worm": "🪱", + "worried": "😟", + "wrench": "🔧", + "wrestling": "🤼", + "writing_hand": "✍️", + "x": "❌", + "yarn": "🧶", + "yawning_face": "🥱", + "yellow_circle": "🟡", + "yellow_heart": "💛", + "yellow_square": "🟨", + "yemen": "🇾🇪", + "yen": "💴", + "yin_yang": "☯️", + "yo_yo": "🪀", + "yum": "😋", + "zambia": "🇿🇲", + "zany_face": "🤪", + "zap": "⚡", + "zebra": "🦓", + "zero": "0️⃣", + "zimbabwe": "🇿🇼", + "zipper_mouth_face": "🤐", + "zombie": "🧟", + "zombie_man": "🧟‍♂️", + "zombie_woman": "🧟‍♀️", + "zzz": "💤" +} \ No newline at end of file diff --git a/server/message_cache.go b/server/message_cache.go index a8b8ff1a..8a613ff1 100644 --- a/server/message_cache.go +++ b/server/message_cache.go @@ -17,6 +17,7 @@ import ( var ( errUnexpectedMessageType = errors.New("unexpected message type") errMessageNotFound = errors.New("message not found") + errNoRows = errors.New("no rows found") ) // Messages cache @@ -44,6 +45,7 @@ const ( attachment_deleted INT NOT NULL, sender TEXT NOT NULL, user TEXT NOT NULL, + content_type TEXT NOT NULL, encoding TEXT NOT NULL, published INT NOT NULL ); @@ -54,46 +56,51 @@ const ( CREATE INDEX IF NOT EXISTS idx_sender ON messages (sender); CREATE INDEX IF NOT EXISTS idx_user ON messages (user); CREATE INDEX IF NOT EXISTS idx_attachment_expires ON messages (attachment_expires); + CREATE TABLE IF NOT EXISTS stats ( + key TEXT PRIMARY KEY, + value INT + ); + INSERT INTO stats (key, value) VALUES ('messages', 0); COMMIT; ` insertMessageQuery = ` - INSERT INTO messages (mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, encoding, published) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO messages (mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, content_type, encoding, published) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` deleteMessageQuery = `DELETE FROM messages WHERE mid = ?` updateMessagesForTopicExpiryQuery = `UPDATE messages SET expires = ? WHERE topic = ?` selectRowIDFromMessageID = `SELECT id FROM messages WHERE mid = ?` // Do not include topic, see #336 and TestServer_PollSinceID_MultipleTopics selectMessagesByIDQuery = ` - SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, encoding + SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding FROM messages WHERE mid = ? ` selectMessagesSinceTimeQuery = ` - SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, encoding + SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding FROM messages WHERE topic = ? AND time >= ? AND published = 1 ORDER BY time, id ` selectMessagesSinceTimeIncludeScheduledQuery = ` - SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, encoding + SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding FROM messages WHERE topic = ? AND time >= ? ORDER BY time, id ` selectMessagesSinceIDQuery = ` - SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, encoding + SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding FROM messages WHERE topic = ? AND id > ? AND published = 1 ORDER BY time, id ` selectMessagesSinceIDIncludeScheduledQuery = ` - SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, encoding + SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding FROM messages WHERE topic = ? AND (id > ? OR published = 0) ORDER BY time, id ` selectMessagesDueQuery = ` - SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, encoding + SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding FROM messages WHERE time <= ? AND published = 0 ORDER BY time, id @@ -108,11 +115,14 @@ const ( selectAttachmentsExpiredQuery = `SELECT mid FROM messages WHERE attachment_expires > 0 AND attachment_expires <= ? AND attachment_deleted = 0` selectAttachmentsSizeBySenderQuery = `SELECT IFNULL(SUM(attachment_size), 0) FROM messages WHERE user = '' AND sender = ? AND attachment_expires >= ?` selectAttachmentsSizeByUserIDQuery = `SELECT IFNULL(SUM(attachment_size), 0) FROM messages WHERE user = ? AND attachment_expires >= ?` + + selectStatsQuery = `SELECT value FROM stats WHERE key = 'messages'` + updateStatsQuery = `UPDATE stats SET value = ? WHERE key = 'messages'` ) // Schema management queries const ( - currentSchemaVersion = 10 + currentSchemaVersion = 12 createSchemaVersionTableQuery = ` CREATE TABLE IF NOT EXISTS schemaVersion ( id INT PRIMARY KEY, @@ -222,20 +232,36 @@ const ( CREATE INDEX IF NOT EXISTS idx_attachment_expires ON messages (attachment_expires); ` migrate9To10UpdateMessageExpiryQuery = `UPDATE messages SET expires = time + ?` + + // 10 -> 11 + migrate10To11AlterMessagesTableQuery = ` + CREATE TABLE IF NOT EXISTS stats ( + key TEXT PRIMARY KEY, + value INT + ); + INSERT INTO stats (key, value) VALUES ('messages', 0); + ` + + // 11 -> 12 + migrate11To12AlterMessagesTableQuery = ` + ALTER TABLE messages ADD COLUMN content_type TEXT NOT NULL DEFAULT(''); + ` ) var ( migrations = map[int]func(db *sql.DB, cacheDuration time.Duration) error{ - 0: migrateFrom0, - 1: migrateFrom1, - 2: migrateFrom2, - 3: migrateFrom3, - 4: migrateFrom4, - 5: migrateFrom5, - 6: migrateFrom6, - 7: migrateFrom7, - 8: migrateFrom8, - 9: migrateFrom9, + 0: migrateFrom0, + 1: migrateFrom1, + 2: migrateFrom2, + 3: migrateFrom3, + 4: migrateFrom4, + 5: migrateFrom5, + 6: migrateFrom6, + 7: migrateFrom7, + 8: migrateFrom8, + 9: migrateFrom9, + 10: migrateFrom10, + 11: migrateFrom11, } ) @@ -251,7 +277,7 @@ func newSqliteCache(filename, startupQueries string, cacheDuration time.Duration if err != nil { return nil, err } - if err := setupDB(db, startupQueries, cacheDuration); err != nil { + if err := setupMessagesDB(db, startupQueries, cacheDuration); err != nil { return nil, err } var queue *util.BatchingQueue[*message] @@ -365,6 +391,7 @@ func (c *messageCache) addMessages(ms []*message) error { attachmentDeleted, // Always zero sender, m.User, + m.ContentType, m.Encoding, published, ) @@ -637,7 +664,7 @@ func readMessages(rows *sql.Rows) ([]*message, error) { func readMessage(rows *sql.Rows) (*message, error) { var timestamp, expires, attachmentSize, attachmentExpires int64 var priority int - var id, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, encoding string + var id, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, contentType, encoding string err := rows.Scan( &id, ×tamp, @@ -657,6 +684,7 @@ func readMessage(rows *sql.Rows) (*message, error) { &attachmentURL, &sender, &user, + &contentType, &encoding, ) if err != nil { @@ -687,30 +715,51 @@ func readMessage(rows *sql.Rows) (*message, error) { } } return &message{ - ID: id, - Time: timestamp, - Expires: expires, - Event: messageEvent, - Topic: topic, - Message: msg, - Title: title, - Priority: priority, - Tags: tags, - Click: click, - Icon: icon, - Actions: actions, - Attachment: att, - Sender: senderIP, // Must parse assuming database must be correct - User: user, - Encoding: encoding, + ID: id, + Time: timestamp, + Expires: expires, + Event: messageEvent, + Topic: topic, + Message: msg, + Title: title, + Priority: priority, + Tags: tags, + Click: click, + Icon: icon, + Actions: actions, + Attachment: att, + Sender: senderIP, // Must parse assuming database must be correct + User: user, + ContentType: contentType, + Encoding: encoding, }, nil } +func (c *messageCache) UpdateStats(messages int64) error { + _, err := c.db.Exec(updateStatsQuery, messages) + return err +} + +func (c *messageCache) Stats() (messages int64, err error) { + rows, err := c.db.Query(selectStatsQuery) + if err != nil { + return 0, err + } + defer rows.Close() + if !rows.Next() { + return 0, errNoRows + } + if err := rows.Scan(&messages); err != nil { + return 0, err + } + return messages, nil +} + func (c *messageCache) Close() error { return c.db.Close() } -func setupDB(db *sql.DB, startupQueries string, cacheDuration time.Duration) error { +func setupMessagesDB(db *sql.DB, startupQueries string, cacheDuration time.Duration) error { // Run startup queries if startupQueries != "" { if _, err := db.Exec(startupQueries); err != nil { @@ -889,3 +938,35 @@ func migrateFrom9(db *sql.DB, cacheDuration time.Duration) error { } return tx.Commit() } + +func migrateFrom10(db *sql.DB, _ time.Duration) error { + log.Tag(tagMessageCache).Info("Migrating cache database schema: from 10 to 11") + tx, err := db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + if _, err := tx.Exec(migrate10To11AlterMessagesTableQuery); err != nil { + return err + } + if _, err := tx.Exec(updateSchemaVersion, 11); err != nil { + return err + } + return tx.Commit() +} + +func migrateFrom11(db *sql.DB, _ time.Duration) error { + log.Tag(tagMessageCache).Info("Migrating cache database schema: from 11 to 12") + tx, err := db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + if _, err := tx.Exec(migrate11To12AlterMessagesTableQuery); err != nil { + return err + } + if _, err := tx.Exec(updateSchemaVersion, 12); err != nil { + return err + } + return tx.Commit() +} diff --git a/server/server.go b/server/server.go index c86307d2..0ab36524 100644 --- a/server/server.go +++ b/server/server.go @@ -9,13 +9,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/emersion/go-smtp" - "github.com/gorilla/websocket" - "github.com/prometheus/client_golang/prometheus/promhttp" - "golang.org/x/sync/errgroup" - "heckel.io/ntfy/log" - "heckel.io/ntfy/user" - "heckel.io/ntfy/util" "io" "net" "net/http" @@ -32,6 +25,14 @@ import ( "sync" "time" "unicode/utf8" + + "github.com/emersion/go-smtp" + "github.com/gorilla/websocket" + "github.com/prometheus/client_golang/prometheus/promhttp" + "golang.org/x/sync/errgroup" + "heckel.io/ntfy/log" + "heckel.io/ntfy/user" + "heckel.io/ntfy/util" ) // Server is the main server, providing the UI and API for ntfy @@ -48,15 +49,17 @@ type Server struct { topics map[string]*topic visitors map[string]*visitor // ip: or user: firebaseClient *firebaseClient - messages int64 + messages int64 // Total number of messages (persisted if messageCache enabled) + messagesHistory []int64 // Last n values of the messages counter, used to determine rate userManager *user.Manager // Might be nil! messageCache *messageCache // Database that stores the messages + webPush *webPushStore // Database that stores web push subscriptions fileCache *fileCache // File system based cache that stores attachments stripe stripeAPI // Stripe API, can be replaced with a mock priceCache *util.LookupCache[map[string]int64] // Stripe price ID -> price as cents (USD implied!) metricsHandler http.Handler // Handles /metrics if enable-metrics set, and listen-metrics-http not set closeChan chan bool - mu sync.Mutex + mu sync.RWMutex } // handleFunc extends the normal http.HandlerFunc to be able to easily return errors @@ -75,17 +78,26 @@ var ( publishPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}/(publish|send|trigger)$`) webConfigPath = "/config.js" + webManifestPath = "/manifest.webmanifest" + webRootHTMLPath = "/app.html" + webServiceWorkerPath = "/sw.js" accountPath = "/account" matrixPushPath = "/_matrix/push/v1/notify" metricsPath = "/metrics" apiHealthPath = "/v1/health" - apiTiers = "/v1/tiers" + apiStatsPath = "/v1/stats" + apiWebPushPath = "/v1/webpush" + apiTiersPath = "/v1/tiers" + apiUsersPath = "/v1/users" + apiUsersAccessPath = "/v1/users/access" apiAccountPath = "/v1/account" apiAccountTokenPath = "/v1/account/token" apiAccountPasswordPath = "/v1/account/password" apiAccountSettingsPath = "/v1/account/settings" apiAccountSubscriptionPath = "/v1/account/subscription" apiAccountReservationPath = "/v1/account/reservation" + apiAccountPhonePath = "/v1/account/phone" + apiAccountPhoneVerifyPath = "/v1/account/phone/verify" apiAccountBillingPortalPath = "/v1/account/billing/portal" apiAccountBillingWebhookPath = "/v1/account/billing/webhook" apiAccountBillingSubscriptionPath = "/v1/account/billing/subscription" @@ -96,13 +108,13 @@ var ( docsRegex = regexp.MustCompile(`^/docs(|/.*)$`) fileRegex = regexp.MustCompile(`^/file/([-_A-Za-z0-9]{1,64})(?:\.[A-Za-z0-9]{1,16})?$`) urlRegex = regexp.MustCompile(`^https?://`) + phoneNumberRegex = regexp.MustCompile(`^\+\d{1,100}$`) //go:embed site - webFs embed.FS - webFsCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: webFs} - webSiteDir = "/site" - webHomeIndex = "/home.html" // Landing page, only if "web-root: home" - webAppIndex = "/app.html" // React app + webFs embed.FS + webFsCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: webFs} + webSiteDir = "/site" + webAppIndex = "/app.html" // React app //go:embed docs docsStaticFs embed.FS @@ -116,9 +128,10 @@ const ( newMessageBody = "New message" // Used in poll requests as generic message defaultAttachmentMessage = "You received a file: %s" // Used if message body is empty, and there is an attachment encodingBase64 = "base64" // Used mainly for binary UnifiedPush messages - jsonBodyBytesLimit = 16384 - unifiedPushTopicPrefix = "up" // Temporarily, we rate limit all "up*" topics based on the subscriber - unifiedPushTopicLength = 14 + jsonBodyBytesLimit = 16384 // Max number of bytes for a JSON request body + unifiedPushTopicPrefix = "up" // Temporarily, we rate limit all "up*" topics based on the subscriber + unifiedPushTopicLength = 14 // Length of UnifiedPush topics, including the "up" part + messagesHistoryMax = 10 // Number of message count values to keep in memory ) // WebSocket constants @@ -144,10 +157,21 @@ func New(conf *Config) (*Server, error) { if err != nil { return nil, err } + var webPush *webPushStore + if conf.WebPushPublicKey != "" { + webPush, err = newWebPushStore(conf.WebPushFile, conf.WebPushStartupQueries) + if err != nil { + return nil, err + } + } topics, err := messageCache.Topics() if err != nil { return nil, err } + messages, err := messageCache.Stats() + if err != nil { + return nil, err + } var fileCache *fileCache if conf.AttachmentCacheDir != "" { fileCache, err = newFileCache(conf.AttachmentCacheDir, conf.AttachmentTotalSizeLimit) @@ -177,15 +201,18 @@ func New(conf *Config) (*Server, error) { firebaseClient = newFirebaseClient(sender, auther) } s := &Server{ - config: conf, - messageCache: messageCache, - fileCache: fileCache, - firebaseClient: firebaseClient, - smtpSender: mailer, - topics: topics, - userManager: userManager, - visitors: make(map[string]*visitor), - stripe: stripe, + config: conf, + messageCache: messageCache, + webPush: webPush, + fileCache: fileCache, + firebaseClient: firebaseClient, + smtpSender: mailer, + topics: topics, + userManager: userManager, + messages: messages, + messagesHistory: []int64{messages}, + visitors: make(map[string]*visitor), + stripe: stripe, } s.priceCache = util.NewLookupCache(s.fetchStripePrices, conf.StripePriceCacheDuration) return s, nil @@ -329,6 +356,9 @@ func (s *Server) closeDatabases() { s.userManager.Close() } s.messageCache.Close() + if s.webPush != nil { + s.webPush.Close() + } } // handle is the main entry point for all HTTP requests @@ -395,14 +425,26 @@ func (s *Server) handleError(w http.ResponseWriter, r *http.Request, v *visitor, } func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visitor) error { - if r.Method == http.MethodGet && r.URL.Path == "/" { - return s.ensureWebEnabled(s.handleHome)(w, r, v) + if r.Method == http.MethodGet && r.URL.Path == "/" && s.config.WebRoot == "/" { + return s.ensureWebEnabled(s.handleRoot)(w, r, v) } else if r.Method == http.MethodHead && r.URL.Path == "/" { return s.ensureWebEnabled(s.handleEmpty)(w, r, v) } else if r.Method == http.MethodGet && r.URL.Path == apiHealthPath { return s.handleHealth(w, r, v) } else if r.Method == http.MethodGet && r.URL.Path == webConfigPath { return s.ensureWebEnabled(s.handleWebConfig)(w, r, v) + } else if r.Method == http.MethodGet && r.URL.Path == webManifestPath { + return s.ensureWebPushEnabled(s.handleWebManifest)(w, r, v) + } else if r.Method == http.MethodGet && r.URL.Path == apiUsersPath { + return s.ensureAdmin(s.handleUsersGet)(w, r, v) + } else if r.Method == http.MethodPut && r.URL.Path == apiUsersPath { + return s.ensureAdmin(s.handleUsersAdd)(w, r, v) + } else if r.Method == http.MethodDelete && r.URL.Path == apiUsersPath { + return s.ensureAdmin(s.handleUsersDelete)(w, r, v) + } else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && r.URL.Path == apiUsersAccessPath { + return s.ensureAdmin(s.handleAccessAllow)(w, r, v) + } else if r.Method == http.MethodDelete && r.URL.Path == apiUsersAccessPath { + return s.ensureAdmin(s.handleAccessReset)(w, r, v) } else if r.Method == http.MethodPost && r.URL.Path == apiAccountPath { return s.ensureUserManager(s.handleAccountCreate)(w, r, v) } else if r.Method == http.MethodGet && r.URL.Path == apiAccountPath { @@ -441,13 +483,25 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit return s.ensurePaymentsEnabled(s.ensureStripeCustomer(s.handleAccountBillingPortalSessionCreate))(w, r, v) } else if r.Method == http.MethodPost && r.URL.Path == apiAccountBillingWebhookPath { return s.ensurePaymentsEnabled(s.ensureUserManager(s.handleAccountBillingWebhook))(w, r, v) // This request comes from Stripe! - } else if r.Method == http.MethodGet && r.URL.Path == apiTiers { + } else if r.Method == http.MethodPut && r.URL.Path == apiAccountPhoneVerifyPath { + return s.ensureUser(s.ensureCallsEnabled(s.withAccountSync(s.handleAccountPhoneNumberVerify)))(w, r, v) + } else if r.Method == http.MethodPut && r.URL.Path == apiAccountPhonePath { + return s.ensureUser(s.ensureCallsEnabled(s.withAccountSync(s.handleAccountPhoneNumberAdd)))(w, r, v) + } else if r.Method == http.MethodDelete && r.URL.Path == apiAccountPhonePath { + return s.ensureUser(s.ensureCallsEnabled(s.withAccountSync(s.handleAccountPhoneNumberDelete)))(w, r, v) + } else if r.Method == http.MethodPost && apiWebPushPath == r.URL.Path { + return s.ensureWebPushEnabled(s.limitRequests(s.handleWebPushUpdate))(w, r, v) + } else if r.Method == http.MethodDelete && apiWebPushPath == r.URL.Path { + return s.ensureWebPushEnabled(s.limitRequests(s.handleWebPushDelete))(w, r, v) + } else if r.Method == http.MethodGet && r.URL.Path == apiStatsPath { + return s.handleStats(w, r, v) + } else if r.Method == http.MethodGet && r.URL.Path == apiTiersPath { return s.ensurePaymentsEnabled(s.handleBillingTiersGet)(w, r, v) } else if r.Method == http.MethodGet && r.URL.Path == matrixPushPath { return s.handleMatrixDiscovery(w) } else if r.Method == http.MethodGet && r.URL.Path == metricsPath && s.metricsHandler != nil { return s.handleMetrics(w, r, v) - } else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) { + } else if r.Method == http.MethodGet && (staticRegex.MatchString(r.URL.Path) || r.URL.Path == webServiceWorkerPath || r.URL.Path == webRootHTMLPath) { return s.ensureWebEnabled(s.handleStatic)(w, r, v) } else if r.Method == http.MethodGet && docsRegex.MatchString(r.URL.Path) { return s.ensureWebEnabled(s.handleDocs)(w, r, v) @@ -479,12 +533,8 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit return errHTTPNotFound } -func (s *Server) handleHome(w http.ResponseWriter, r *http.Request, v *visitor) error { - if s.config.WebRootIsApp { - r.URL.Path = webAppIndex - } else { - r.URL.Path = webHomeIndex - } +func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request, v *visitor) error { + r.URL.Path = webAppIndex return s.handleStatic(w, r, v) } @@ -516,18 +566,18 @@ func (s *Server) handleHealth(w http.ResponseWriter, _ *http.Request, _ *visitor } func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visitor) error { - appRoot := "/" - if !s.config.WebRootIsApp { - appRoot = "/app" - } response := &apiConfigResponse{ BaseURL: "", // Will translate to window.location.origin - AppRoot: appRoot, + AppRoot: s.config.WebRoot, EnableLogin: s.config.EnableLogin, EnableSignup: s.config.EnableSignup, EnablePayments: s.config.StripeSecretKey != "", + EnableCalls: s.config.TwilioAccount != "", + EnableEmails: s.config.SMTPSenderFrom != "", EnableReservations: s.config.EnableReservations, + EnableWebPush: s.config.WebPushPublicKey != "", BillingContact: s.config.BillingContact, + WebPushPublicKey: s.config.WebPushPublicKey, DisallowedTopics: s.config.DisallowedTopics, } b, err := json.MarshalIndent(response, "", " ") @@ -539,6 +589,25 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi return err } +// handleWebManifest serves the web app manifest for the progressive web app (PWA) +func (s *Server) handleWebManifest(w http.ResponseWriter, _ *http.Request, _ *visitor) error { + response := &webManifestResponse{ + Name: "ntfy web", + Description: "ntfy lets you send push notifications via scripts from any computer or phone", + ShortName: "ntfy", + Scope: "/", + StartURL: s.config.WebRoot, + Display: "standalone", + BackgroundColor: "#ffffff", + ThemeColor: "#317f6f", + Icons: []*webManifestIcon{ + {SRC: "/static/images/pwa-192x192.png", Sizes: "192x192", Type: "image/png"}, + {SRC: "/static/images/pwa-512x512.png", Sizes: "512x512", Type: "image/png"}, + }, + } + return s.writeJSONWithContentType(w, response, "application/manifest+json") +} + // handleMetrics returns Prometheus metrics. This endpoint is only called if enable-metrics is set, // and listen-metrics-http is not set. func (s *Server) handleMetrics(w http.ResponseWriter, r *http.Request, _ *visitor) error { @@ -546,17 +615,34 @@ func (s *Server) handleMetrics(w http.ResponseWriter, r *http.Request, _ *visito return nil } +// handleStatic returns all static resources (excluding the docs), including the web app func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request, _ *visitor) error { r.URL.Path = webSiteDir + r.URL.Path util.Gzip(http.FileServer(http.FS(webFsCached))).ServeHTTP(w, r) return nil } +// handleDocs returns static resources related to the docs func (s *Server) handleDocs(w http.ResponseWriter, r *http.Request, _ *visitor) error { util.Gzip(http.FileServer(http.FS(docsStaticCached))).ServeHTTP(w, r) return nil } +// handleStats returns the publicly available server stats +func (s *Server) handleStats(w http.ResponseWriter, _ *http.Request, _ *visitor) error { + s.mu.RLock() + messages, n, rate := s.messages, len(s.messagesHistory), float64(0) + if n > 1 { + rate = float64(s.messagesHistory[n-1]-s.messagesHistory[0]) / (float64(n-1) * s.config.ManagerInterval.Seconds()) + } + s.mu.RUnlock() + response := &apiStatsResponse{ + Messages: messages, + MessagesRate: rate, + } + return s.writeJSON(w, response) +} + // handleFile processes the download of attachment files. The method handles GET and HEAD requests against a file. // Before streaming the file to a client, it locates uploader (m.Sender or m.User) in the message cache, so it // can associate the download bandwidth with the uploader. @@ -623,6 +709,9 @@ func (s *Server) handleFile(w http.ResponseWriter, r *http.Request, v *visitor) return err } defer f.Close() + if m.Attachment.Name != "" { + w.Header().Set("Content-Disposition", "attachment; filename="+strconv.Quote(m.Attachment.Name)) + } _, err = io.Copy(util.NewContentTypeWriter(w, r.URL.Path), f) return err } @@ -649,7 +738,7 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e return nil, err } m := newDefaultMessage(t.ID, "") - cache, firebase, email, unifiedpush, e := s.parsePublishParams(r, m) + cache, firebase, email, call, unifiedpush, e := s.parsePublishParams(r, m) if e != nil { return nil, e.With(t) } @@ -663,6 +752,14 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e return nil, errHTTPTooManyRequestsLimitMessages.With(t) } else if email != "" && !vrate.EmailAllowed() { return nil, errHTTPTooManyRequestsLimitEmails.With(t) + } else if call != "" { + var httpErr *errHTTP + call, httpErr = s.convertPhoneNumber(v.User(), call) + if httpErr != nil { + return nil, httpErr.With(t) + } else if !vrate.CallAllowed() { + return nil, errHTTPTooManyRequestsLimitCalls.With(t) + } } if m.PollID != "" { m = newPollRequestMessage(t.ID, m.PollID) @@ -687,6 +784,7 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e "message_firebase": firebase, "message_unifiedpush": unifiedpush, "message_email": email, + "message_call": call, }) if ev.IsTrace() { ev.Field("message_body", util.MaybeMarshalJSON(m)).Trace("Received message") @@ -703,9 +801,15 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e if s.smtpSender != nil && email != "" { go s.sendEmail(v, m, email) } - if s.config.UpstreamBaseURL != "" { + if s.config.TwilioAccount != "" && call != "" { + go s.callPhone(v, r, m, call) + } + if s.config.UpstreamBaseURL != "" && !unifiedpush { // UP messages are not sent to upstream go s.forwardPollRequest(v, m) } + if s.config.WebPushPublicKey != "" { + go s.publishToWebPushEndpoints(v, m) + } } else { logvrm(v, r, m).Tag(tagPublish).Debug("Message delayed, will process later") } @@ -798,7 +902,11 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) { logvm(v, m).Err(err).Warn("Unable to publish poll request") return } + req.Header.Set("User-Agent", "ntfy/"+s.config.Version) req.Header.Set("X-Poll-ID", m.ID) + if s.config.UpstreamAccessToken != "" { + req.Header.Set("Authorization", util.BearerAuth(s.config.UpstreamAccessToken)) + } var httpClient = &http.Client{ Timeout: time.Second * 10, } @@ -807,12 +915,16 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) { logvm(v, m).Err(err).Warn("Unable to publish poll request") return } else if response.StatusCode != http.StatusOK { - logvm(v, m).Err(err).Warn("Unable to publish poll request, unexpected HTTP status: %d", response.StatusCode) + if response.StatusCode == http.StatusTooManyRequests { + logvm(v, m).Err(err).Warn("Unable to publish poll request, the upstream server %s responded with HTTP %s; you may solve this by sending fewer daily messages, or by configuring upstream-access-token (assuming you have an account with higher rate limits) ", s.config.UpstreamBaseURL, response.Status) + } else { + logvm(v, m).Err(err).Warn("Unable to publish poll request, the upstream server %s responded with HTTP %s", s.config.UpstreamBaseURL, response.Status) + } return } } -func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email string, unifiedpush bool, err *errHTTP) { +func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email, call string, unifiedpush bool, err *errHTTP) { cache = readBoolParam(r, true, "x-cache", "cache") firebase = readBoolParam(r, true, "x-firebase", "firebase") m.Title = readParam(r, "x-title", "title", "t") @@ -828,7 +940,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi } if attach != "" { if !urlRegex.MatchString(attach) { - return false, false, "", false, errHTTPBadRequestAttachmentURLInvalid + return false, false, "", "", false, errHTTPBadRequestAttachmentURLInvalid } m.Attachment.URL = attach if m.Attachment.Name == "" { @@ -846,13 +958,19 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi } if icon != "" { if !urlRegex.MatchString(icon) { - return false, false, "", false, errHTTPBadRequestIconURLInvalid + return false, false, "", "", false, errHTTPBadRequestIconURLInvalid } m.Icon = icon } email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e") if s.smtpSender == nil && email != "" { - return false, false, "", false, errHTTPBadRequestEmailDisabled + return false, false, "", "", false, errHTTPBadRequestEmailDisabled + } + call = readParam(r, "x-call", "call") + if call != "" && (s.config.TwilioAccount == "" || s.userManager == nil) { + return false, false, "", "", false, errHTTPBadRequestPhoneCallsDisabled + } else if call != "" && !isBoolValue(call) && !phoneNumberRegex.MatchString(call) { + return false, false, "", "", false, errHTTPBadRequestPhoneNumberInvalid } messageStr := strings.ReplaceAll(readParam(r, "x-message", "message", "m"), "\\n", "\n") if messageStr != "" { @@ -861,24 +979,27 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi var e error m.Priority, e = util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p")) if e != nil { - return false, false, "", false, errHTTPBadRequestPriorityInvalid + return false, false, "", "", false, errHTTPBadRequestPriorityInvalid } m.Tags = readCommaSeparatedParam(r, "x-tags", "tags", "tag", "ta") delayStr := readParam(r, "x-delay", "delay", "x-at", "at", "x-in", "in") if delayStr != "" { if !cache { - return false, false, "", false, errHTTPBadRequestDelayNoCache + return false, false, "", "", false, errHTTPBadRequestDelayNoCache } if email != "" { - return false, false, "", false, errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet) + return false, false, "", "", false, errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet) + } + if call != "" { + return false, false, "", "", false, errHTTPBadRequestDelayNoCall // we cannot store the phone number (yet) } delay, err := util.ParseFutureTime(delayStr, time.Now()) if err != nil { - return false, false, "", false, errHTTPBadRequestDelayCannotParse + return false, false, "", "", false, errHTTPBadRequestDelayCannotParse } else if delay.Unix() < time.Now().Add(s.config.MinDelay).Unix() { - return false, false, "", false, errHTTPBadRequestDelayTooSmall + return false, false, "", "", false, errHTTPBadRequestDelayTooSmall } else if delay.Unix() > time.Now().Add(s.config.MaxDelay).Unix() { - return false, false, "", false, errHTTPBadRequestDelayTooLarge + return false, false, "", "", false, errHTTPBadRequestDelayTooLarge } m.Time = delay.Unix() } @@ -886,9 +1007,13 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi if actionsStr != "" { m.Actions, e = parseActions(actionsStr) if e != nil { - return false, false, "", false, errHTTPBadRequestActionsInvalid.Wrap(e.Error()) + return false, false, "", "", false, errHTTPBadRequestActionsInvalid.Wrap(e.Error()) } } + contentType, markdown := readParam(r, "content-type", "content_type"), readBoolParam(r, false, "x-markdown", "markdown", "md") + if markdown || strings.ToLower(contentType) == "text/markdown" { + m.ContentType = "text/markdown" + } unifiedpush = readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see GET too! if unifiedpush { firebase = false @@ -900,7 +1025,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi cache = false email = "" } - return cache, firebase, email, unifiedpush, nil + return cache, firebase, email, call, unifiedpush, nil } // handlePublishBody consumes the PUT/POST body and decides whether the body is an attachment or the message. @@ -1170,7 +1295,7 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi } defer conn.Close() - // Subscription connections can be canceled externally, see topic.CancelSubscribers + // Subscription connections can be canceled externally, see topic.CancelSubscribersExceptUser cancelCtx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -1412,6 +1537,7 @@ func (s *Server) handleOptions(w http.ResponseWriter, _ *http.Request, _ *visito return nil } +// topicFromPath returns the topic from a root path (e.g. /mytopic), creating it if it doesn't exist. func (s *Server) topicFromPath(path string) (*topic, error) { parts := strings.Split(path, "/") if len(parts) < 2 { @@ -1420,6 +1546,7 @@ func (s *Server) topicFromPath(path string) (*topic, error) { return s.topicFromID(parts[1]) } +// topicsFromPath returns the topic from a root path (e.g. /mytopic,mytopic2), creating it if it doesn't exist. func (s *Server) topicsFromPath(path string) ([]*topic, string, error) { parts := strings.Split(path, "/") if len(parts) < 2 { @@ -1433,6 +1560,7 @@ func (s *Server) topicsFromPath(path string) ([]*topic, string, error) { return topics, parts[1], nil } +// topicsFromIDs returns the topics with the given IDs, creating them if they don't exist. func (s *Server) topicsFromIDs(ids ...string) ([]*topic, error) { s.mu.Lock() defer s.mu.Unlock() @@ -1452,6 +1580,7 @@ func (s *Server) topicsFromIDs(ids ...string) ([]*topic, error) { return topics, nil } +// topicFromID returns the topic with the given ID, creating it if it doesn't exist. func (s *Server) topicFromID(id string) (*topic, error) { topics, err := s.topicsFromIDs(id) if err != nil { @@ -1460,6 +1589,23 @@ func (s *Server) topicFromID(id string) (*topic, error) { return topics[0], nil } +// topicsFromPattern returns a list of topics matching the given pattern, but it does not create them. +func (s *Server) topicsFromPattern(pattern string) ([]*topic, error) { + s.mu.RLock() + defer s.mu.RUnlock() + patternRegexp, err := regexp.Compile("^" + strings.ReplaceAll(pattern, "*", ".*") + "$") + if err != nil { + return nil, err + } + topics := make([]*topic, 0) + for _, t := range s.topics { + if patternRegexp.MatchString(t.ID) { + topics = append(topics, t) + } + } + return topics, nil +} + func (s *Server) runSMTPServer() error { s.smtpServerBackend = newMailBackend(s.config, s.handle) s.smtpServer = smtp.NewServer(s.smtpServerBackend) @@ -1580,9 +1726,9 @@ func (s *Server) sendDelayedMessages() error { func (s *Server) sendDelayedMessage(v *visitor, m *message) error { logvm(v, m).Debug("Sending delayed message") - s.mu.Lock() + s.mu.RLock() t, ok := s.topics[m.Topic] // If no subscribers, just mark message as published - s.mu.Unlock() + s.mu.RUnlock() if ok { go func() { // We do not rate-limit messages here, since we've rate limited them in the PUT/POST handler @@ -1597,6 +1743,9 @@ func (s *Server) sendDelayedMessage(v *visitor, m *message) error { if s.config.UpstreamBaseURL != "" { go s.forwardPollRequest(v, m) } + if s.config.WebPushPublicKey != "" { + go s.publishToWebPushEndpoints(v, m) + } if err := s.messageCache.MarkPublished(m); err != nil { return err } @@ -1640,6 +1789,9 @@ func (s *Server) transformBodyJSON(next handleFunc) handleFunc { if m.Icon != "" { r.Header.Set("X-Icon", m.Icon) } + if m.Markdown { + r.Header.Set("X-Markdown", "yes") + } if len(m.Actions) > 0 { actionsStr, err := json.Marshal(m.Actions) if err != nil { @@ -1653,6 +1805,9 @@ func (s *Server) transformBodyJSON(next handleFunc) handleFunc { if m.Delay != "" { r.Header.Set("X-Delay", m.Delay) } + if m.Call != "" { + r.Header.Set("X-Call", m.Call) + } return next(w, r, v) } } @@ -1814,10 +1969,28 @@ func (s *Server) visitor(ip netip.Addr, user *user.User) *visitor { } func (s *Server) writeJSON(w http.ResponseWriter, v any) error { - w.Header().Set("Content-Type", "application/json") + return s.writeJSONWithContentType(w, v, "application/json") +} + +func (s *Server) writeJSONWithContentType(w http.ResponseWriter, v any, contentType string) error { + w.Header().Set("Content-Type", contentType) w.Header().Set("Access-Control-Allow-Origin", s.config.AccessControlAllowOrigin) // CORS, allow cross-origin requests if err := json.NewEncoder(w).Encode(v); err != nil { return err } return nil } + +func (s *Server) updateAndWriteStats(messagesCount int64) { + s.mu.Lock() + s.messagesHistory = append(s.messagesHistory, messagesCount) + if len(s.messagesHistory) > messagesHistoryMax { + s.messagesHistory = s.messagesHistory[1:] + } + s.mu.Unlock() + go func() { + if err := s.messageCache.UpdateStats(messagesCount); err != nil { + log.Tag(tagManager).Err(err).Warn("Cannot write messages stats") + } + }() +} diff --git a/server/server.yml b/server/server.yml index 24371b65..b044a914 100644 --- a/server/server.yml +++ b/server/server.yml @@ -144,6 +144,39 @@ # smtp-server-domain: # smtp-server-addr-prefix: +# Web Push support (background notifications for browsers) +# +# If enabled, allows ntfy to receive push notifications, even when the ntfy web app is closed. When enabled, users +# can enable background notifications in the web app. Once enabled, ntfy will forward published messages to the push +# endpoint, which will then forward it to the browser. +# +# You must configure web-push-public/private key, web-push-file, and web-push-email-address below to enable Web Push. +# Run "ntfy webpush keys" to generate the keys. +# +# - web-push-public-key is the generated VAPID public key, e.g. AA1234BBCCddvveekaabcdfqwertyuiopasdfghjklzxcvbnm1234567890 +# - web-push-private-key is the generated VAPID private key, e.g. AA2BB1234567890abcdefzxcvbnm1234567890 +# - web-push-file is a database file to keep track of browser subscription endpoints, e.g. `/var/cache/ntfy/webpush.db` +# - web-push-email-address is the admin email address send to the push provider, e.g. `sysadmin@example.com` +# - web-push-startup-queries is an optional list of queries to run on startup` +# +# web-push-public-key: +# web-push-private-key: +# web-push-file: +# web-push-email-address: +# web-push-startup-queries: + +# If enabled, ntfy can perform voice calls via Twilio via the "X-Call" header. +# +# - twilio-account is the Twilio account SID, e.g. AC12345beefbeef67890beefbeef122586 +# - twilio-auth-token is the Twilio auth token, e.g. affebeef258625862586258625862586 +# - twilio-phone-number is the outgoing phone number you purchased, e.g. +18775132586 +# - twilio-verify-service is the Twilio Verify service SID, e.g. VA12345beefbeef67890beefbeef122586 +# +# twilio-account: +# twilio-auth-token: +# twilio-phone-number: +# twilio-verify-service: + # Interval in which keepalive messages are sent to the client. This is to prevent # intermediaries closing the connection for inactivity. # @@ -167,11 +200,13 @@ # # disallowed-topics: -# Defines if the root route (/) is pointing to the landing page (as on ntfy.sh) or the -# web app. If you self-host, you don't want to change this. -# Can be "app" (default), "home" or "disable" to disable the web app entirely. +# Defines the root path of the web app, or disables the web app entirely. # -# web-root: app +# Can be any simple path, e.g. "/", "/app", or "/ntfy". For backwards-compatibility reasons, +# the values "app" (maps to "/"), "home" (maps to "/app"), or "disable" (maps to "") to disable +# the web app entirely. +# +# web-root: / # Various feature flags used to control the web app, and API access, mainly around user and # account management. @@ -194,7 +229,12 @@ # the message ID of the original message, instructing the iOS app to poll this server for the actual message contents. # This is to prevent the upstream server and Firebase/APNS from being able to read the message. # +# - upstream-base-url is the base URL of the upstream server. Should be "https://ntfy.sh". +# - upstream-access-token is the token used to authenticate with the upstream server. This is only required +# if you exceed the upstream rate limits, or the uptream server requires authentication. +# # upstream-base-url: +# upstream-access-token: # Rate limiting: Total number of topics before the server rejects new topics. # @@ -302,6 +342,10 @@ # - "field -> level" to match any value, e.g. "time_taken_ms -> debug" # Warning: Using log-level-overrides has a performance penalty. Only use it for temporary debugging. # +# Check your permissions: +# If you are running ntfy with systemd, make sure this log file is owned by the +# ntfy user and group by running: chown ntfy.ntfy . +# # Example (good for production): # log-level: info # log-format: json diff --git a/server/server_account.go b/server/server_account.go index 1b2c0ce4..f26cc2ff 100644 --- a/server/server_account.go +++ b/server/server_account.go @@ -56,6 +56,7 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis Messages: limits.MessageLimit, MessagesExpiryDuration: int64(limits.MessageExpiryDuration.Seconds()), Emails: limits.EmailLimit, + Calls: limits.CallLimit, Reservations: limits.ReservationsLimit, AttachmentTotalSize: limits.AttachmentTotalSizeLimit, AttachmentFileSize: limits.AttachmentFileSizeLimit, @@ -67,6 +68,8 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis MessagesRemaining: stats.MessagesRemaining, Emails: stats.Emails, EmailsRemaining: stats.EmailsRemaining, + Calls: stats.Calls, + CallsRemaining: stats.CallsRemaining, Reservations: stats.Reservations, ReservationsRemaining: stats.ReservationsRemaining, AttachmentTotalSize: stats.AttachmentTotalSize, @@ -105,17 +108,19 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis CancelAt: u.Billing.StripeSubscriptionCancelAt.Unix(), } } - reservations, err := s.userManager.Reservations(u.Name) - if err != nil { - return err - } - if len(reservations) > 0 { - response.Reservations = make([]*apiAccountReservation, 0) - for _, r := range reservations { - response.Reservations = append(response.Reservations, &apiAccountReservation{ - Topic: r.Topic, - Everyone: r.Everyone.String(), - }) + if s.config.EnableReservations { + reservations, err := s.userManager.Reservations(u.Name) + if err != nil { + return err + } + if len(reservations) > 0 { + response.Reservations = make([]*apiAccountReservation, 0) + for _, r := range reservations { + response.Reservations = append(response.Reservations, &apiAccountReservation{ + Topic: r.Topic, + Everyone: r.Everyone.String(), + }) + } } } tokens, err := s.userManager.Tokens(u.ID) @@ -138,6 +143,15 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis }) } } + if s.config.TwilioAccount != "" { + phoneNumbers, err := s.userManager.PhoneNumbers(u.ID) + if err != nil { + return err + } + if len(phoneNumbers) > 0 { + response.PhoneNumbers = phoneNumbers + } + } } else { response.Username = user.Everyone response.Role = string(user.RoleAnonymous) @@ -156,6 +170,11 @@ func (s *Server) handleAccountDelete(w http.ResponseWriter, r *http.Request, v * if _, err := s.userManager.Authenticate(u.Name, req.Password); err != nil { return errHTTPBadRequestIncorrectPasswordConfirmation } + if s.webPush != nil && u.ID != "" { + if err := s.webPush.RemoveSubscriptionsByUserID(u.ID); err != nil { + logvr(v, r).Err(err).Warn("Error removing web push subscriptions for %s", u.Name) + } + } if u.Billing.StripeSubscriptionID != "" { logvr(v, r).Tag(tagStripe).Info("Canceling billing subscription for user %s", u.Name) if _, err := s.stripe.CancelSubscription(u.Billing.StripeSubscriptionID); err != nil { @@ -444,7 +463,7 @@ func (s *Server) handleAccountReservationAdd(w http.ResponseWriter, r *http.Requ if err != nil { return err } - t.CancelSubscribers(u.ID) + t.CancelSubscribersExceptUser(u.ID) return s.writeJSON(w, newSuccessResponse()) } @@ -511,6 +530,72 @@ func (s *Server) maybeRemoveMessagesAndExcessReservations(r *http.Request, v *vi return nil } +func (s *Server) handleAccountPhoneNumberVerify(w http.ResponseWriter, r *http.Request, v *visitor) error { + u := v.User() + req, err := readJSONWithLimit[apiAccountPhoneNumberVerifyRequest](r.Body, jsonBodyBytesLimit, false) + if err != nil { + return err + } else if !phoneNumberRegex.MatchString(req.Number) { + return errHTTPBadRequestPhoneNumberInvalid + } else if req.Channel != "sms" && req.Channel != "call" { + return errHTTPBadRequestPhoneNumberVerifyChannelInvalid + } + // Check user is allowed to add phone numbers + if u == nil || (u.IsUser() && u.Tier == nil) { + return errHTTPUnauthorized + } else if u.IsUser() && u.Tier.CallLimit == 0 { + return errHTTPUnauthorized + } + // Check if phone number exists + phoneNumbers, err := s.userManager.PhoneNumbers(u.ID) + if err != nil { + return err + } else if util.Contains(phoneNumbers, req.Number) { + return errHTTPConflictPhoneNumberExists + } + // Actually add the unverified number, and send verification + logvr(v, r).Tag(tagAccount).Field("phone_number", req.Number).Debug("Sending phone number verification") + if err := s.verifyPhoneNumber(v, r, req.Number, req.Channel); err != nil { + return err + } + return s.writeJSON(w, newSuccessResponse()) +} + +func (s *Server) handleAccountPhoneNumberAdd(w http.ResponseWriter, r *http.Request, v *visitor) error { + u := v.User() + req, err := readJSONWithLimit[apiAccountPhoneNumberAddRequest](r.Body, jsonBodyBytesLimit, false) + if err != nil { + return err + } + if !phoneNumberRegex.MatchString(req.Number) { + return errHTTPBadRequestPhoneNumberInvalid + } + if err := s.verifyPhoneNumberCheck(v, r, req.Number, req.Code); err != nil { + return err + } + logvr(v, r).Tag(tagAccount).Field("phone_number", req.Number).Debug("Adding phone number as verified") + if err := s.userManager.AddPhoneNumber(u.ID, req.Number); err != nil { + return err + } + return s.writeJSON(w, newSuccessResponse()) +} + +func (s *Server) handleAccountPhoneNumberDelete(w http.ResponseWriter, r *http.Request, v *visitor) error { + u := v.User() + req, err := readJSONWithLimit[apiAccountPhoneNumberAddRequest](r.Body, jsonBodyBytesLimit, false) + if err != nil { + return err + } + if !phoneNumberRegex.MatchString(req.Number) { + return errHTTPBadRequestPhoneNumberInvalid + } + logvr(v, r).Tag(tagAccount).Field("phone_number", req.Number).Debug("Deleting phone number") + if err := s.userManager.RemovePhoneNumber(u.ID, req.Number); err != nil { + return err + } + return s.writeJSON(w, newSuccessResponse()) +} + // publishSyncEventAsync kicks of a Go routine to publish a sync message to the user's sync topic func (s *Server) publishSyncEventAsync(v *visitor) { go func() { diff --git a/server/server_account_test.go b/server/server_account_test.go index 465e4be1..119efb16 100644 --- a/server/server_account_test.go +++ b/server/server_account_test.go @@ -151,6 +151,8 @@ func TestAccount_Get_Anonymous(t *testing.T) { require.Equal(t, int64(1004), account.Stats.MessagesRemaining) require.Equal(t, int64(0), account.Stats.Emails) require.Equal(t, int64(24), account.Stats.EmailsRemaining) + require.Equal(t, int64(0), account.Stats.Calls) + require.Equal(t, int64(0), account.Stats.CallsRemaining) rr = request(t, s, "POST", "/mytopic", "", nil) require.Equal(t, 200, rr.Code) @@ -498,6 +500,8 @@ func TestAccount_Reservation_AddAdminSuccess(t *testing.T) { func TestAccount_Reservation_AddRemoveUserWithTierSuccess(t *testing.T) { conf := newTestConfigWithAuthFile(t) conf.EnableSignup = true + conf.EnableReservations = true + conf.TwilioAccount = "dummy" s := newTestServer(t, conf) // Create user @@ -510,6 +514,7 @@ func TestAccount_Reservation_AddRemoveUserWithTierSuccess(t *testing.T) { MessageLimit: 123, MessageExpiryDuration: 86400 * time.Second, EmailLimit: 32, + CallLimit: 10, ReservationLimit: 2, AttachmentFileSizeLimit: 1231231, AttachmentTotalSizeLimit: 123123, @@ -551,6 +556,7 @@ func TestAccount_Reservation_AddRemoveUserWithTierSuccess(t *testing.T) { require.Equal(t, int64(123), account.Limits.Messages) require.Equal(t, int64(86400), account.Limits.MessagesExpiryDuration) require.Equal(t, int64(32), account.Limits.Emails) + require.Equal(t, int64(10), account.Limits.Calls) require.Equal(t, int64(2), account.Limits.Reservations) require.Equal(t, int64(1231231), account.Limits.AttachmentFileSize) require.Equal(t, int64(123123), account.Limits.AttachmentTotalSize) diff --git a/server/server_admin.go b/server/server_admin.go new file mode 100644 index 00000000..9380a5ff --- /dev/null +++ b/server/server_admin.go @@ -0,0 +1,143 @@ +package server + +import ( + "heckel.io/ntfy/user" + "net/http" +) + +func (s *Server) handleUsersGet(w http.ResponseWriter, r *http.Request, v *visitor) error { + users, err := s.userManager.Users() + if err != nil { + return err + } + grants, err := s.userManager.AllGrants() + if err != nil { + return err + } + usersResponse := make([]*apiUserResponse, len(users)) + for i, u := range users { + tier := "" + if u.Tier != nil { + tier = u.Tier.Code + } + userGrants := make([]*apiUserGrantResponse, len(grants[u.ID])) + for i, g := range grants[u.ID] { + userGrants[i] = &apiUserGrantResponse{ + Topic: g.TopicPattern, + Permission: g.Allow.String(), + } + } + usersResponse[i] = &apiUserResponse{ + Username: u.Name, + Role: string(u.Role), + Tier: tier, + Grants: userGrants, + } + } + return s.writeJSON(w, usersResponse) +} + +func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visitor) error { + req, err := readJSONWithLimit[apiUserAddRequest](r.Body, jsonBodyBytesLimit, false) + if err != nil { + return err + } else if !user.AllowedUsername(req.Username) || req.Password == "" { + return errHTTPBadRequest.Wrap("username invalid, or password missing") + } + u, err := s.userManager.User(req.Username) + if err != nil && err != user.ErrUserNotFound { + return err + } else if u != nil { + return errHTTPConflictUserExists + } + var tier *user.Tier + if req.Tier != "" { + tier, err = s.userManager.Tier(req.Tier) + if err == user.ErrTierNotFound { + return errHTTPBadRequestTierInvalid + } else if err != nil { + return err + } + } + if err := s.userManager.AddUser(req.Username, req.Password, user.RoleUser); err != nil { + return err + } + if tier != nil { + if err := s.userManager.ChangeTier(req.Username, req.Tier); err != nil { + return err + } + } + return s.writeJSON(w, newSuccessResponse()) +} + +func (s *Server) handleUsersDelete(w http.ResponseWriter, r *http.Request, v *visitor) error { + req, err := readJSONWithLimit[apiUserDeleteRequest](r.Body, jsonBodyBytesLimit, false) + if err != nil { + return err + } + u, err := s.userManager.User(req.Username) + if err == user.ErrUserNotFound { + return errHTTPBadRequestUserNotFound + } else if err != nil { + return err + } else if !u.IsUser() { + return errHTTPUnauthorized.Wrap("can only remove regular users from API") + } + if err := s.userManager.RemoveUser(req.Username); err != nil { + return err + } + if err := s.killUserSubscriber(u, "*"); err != nil { // FIXME super inefficient + return err + } + return s.writeJSON(w, newSuccessResponse()) +} + +func (s *Server) handleAccessAllow(w http.ResponseWriter, r *http.Request, v *visitor) error { + req, err := readJSONWithLimit[apiAccessAllowRequest](r.Body, jsonBodyBytesLimit, false) + if err != nil { + return err + } + _, err = s.userManager.User(req.Username) + if err == user.ErrUserNotFound { + return errHTTPBadRequestUserNotFound + } else if err != nil { + return err + } + permission, err := user.ParsePermission(req.Permission) + if err != nil { + return errHTTPBadRequestPermissionInvalid + } + if err := s.userManager.AllowAccess(req.Username, req.Topic, permission); err != nil { + return err + } + return s.writeJSON(w, newSuccessResponse()) +} + +func (s *Server) handleAccessReset(w http.ResponseWriter, r *http.Request, v *visitor) error { + req, err := readJSONWithLimit[apiAccessResetRequest](r.Body, jsonBodyBytesLimit, false) + if err != nil { + return err + } + u, err := s.userManager.User(req.Username) + if err != nil { + return err + } + if err := s.userManager.ResetAccess(req.Username, req.Topic); err != nil { + return err + } + if err := s.killUserSubscriber(u, req.Topic); err != nil { // This may be a pattern + return err + } + return s.writeJSON(w, newSuccessResponse()) +} + +func (s *Server) killUserSubscriber(u *user.User, topicPattern string) error { + topics, err := s.topicsFromPattern(topicPattern) + if err != nil { + return err + } + for _, t := range topics { + t.CancelSubscriberUser(u.ID) + } + return nil +} diff --git a/server/server_admin_test.go b/server/server_admin_test.go new file mode 100644 index 00000000..1513ea40 --- /dev/null +++ b/server/server_admin_test.go @@ -0,0 +1,181 @@ +package server + +import ( + "github.com/stretchr/testify/require" + "heckel.io/ntfy/user" + "heckel.io/ntfy/util" + "sync/atomic" + "testing" + "time" +) + +func TestUser_AddRemove(t *testing.T) { + s := newTestServer(t, newTestConfigWithAuthFile(t)) + defer s.closeDatabases() + + // Create admin, tier + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "tier1", + })) + + // Create user via API + rr := request(t, s, "PUT", "/v1/users", `{"username": "ben", "password":"ben"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Create user with tier via API + rr = request(t, s, "PUT", "/v1/users", `{"username": "emma", "password":"emma", "tier": "tier1"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Check users + users, err := s.userManager.Users() + require.Nil(t, err) + require.Equal(t, 4, len(users)) + require.Equal(t, "phil", users[0].Name) + require.Equal(t, "ben", users[1].Name) + require.Equal(t, user.RoleUser, users[1].Role) + require.Nil(t, users[1].Tier) + require.Equal(t, "emma", users[2].Name) + require.Equal(t, user.RoleUser, users[2].Role) + require.Equal(t, "tier1", users[2].Tier.Code) + require.Equal(t, user.Everyone, users[3].Name) + + // Delete user via API + rr = request(t, s, "DELETE", "/v1/users", `{"username": "ben"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) +} + +func TestUser_AddRemove_Failures(t *testing.T) { + s := newTestServer(t, newTestConfigWithAuthFile(t)) + defer s.closeDatabases() + + // Create admin + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) + + // Cannot create user with invalid username + rr := request(t, s, "PUT", "/v1/users", `{"username": "not valid", "password":"ben"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 400, rr.Code) + + // Cannot create user if user already exists + rr = request(t, s, "PUT", "/v1/users", `{"username": "phil", "password":"phil"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 40901, toHTTPError(t, rr.Body.String()).Code) + + // Cannot create user with invalid tier + rr = request(t, s, "PUT", "/v1/users", `{"username": "emma", "password":"emma", "tier": "invalid"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 40030, toHTTPError(t, rr.Body.String()).Code) + + // Cannot delete user as non-admin + rr = request(t, s, "DELETE", "/v1/users", `{"username": "ben"}`, map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 401, rr.Code) + + // Delete user via API + rr = request(t, s, "DELETE", "/v1/users", `{"username": "ben"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) +} + +func TestAccess_AllowReset(t *testing.T) { + c := newTestConfigWithAuthFile(t) + c.AuthDefault = user.PermissionDenyAll + s := newTestServer(t, c) + defer s.closeDatabases() + + // User and admin + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) + + // Subscribing not allowed + rr := request(t, s, "GET", "/gold/json?poll=1", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 403, rr.Code) + + // Grant access + rr = request(t, s, "POST", "/v1/users/access", `{"username": "ben", "topic":"gold", "permission":"ro"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Now subscribing is allowed + rr = request(t, s, "GET", "/gold/json?poll=1", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 200, rr.Code) + + // Reset access + rr = request(t, s, "DELETE", "/v1/users/access", `{"username": "ben", "topic":"gold"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Subscribing not allowed (again) + rr = request(t, s, "GET", "/gold/json?poll=1", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 403, rr.Code) +} + +func TestAccess_AllowReset_NonAdminAttempt(t *testing.T) { + c := newTestConfigWithAuthFile(t) + c.AuthDefault = user.PermissionDenyAll + s := newTestServer(t, c) + defer s.closeDatabases() + + // User + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) + + // Grant access fails, because non-admin + rr := request(t, s, "POST", "/v1/users/access", `{"username": "ben", "topic":"gold", "permission":"ro"}`, map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 401, rr.Code) +} + +func TestAccess_AllowReset_KillConnection(t *testing.T) { + c := newTestConfigWithAuthFile(t) + c.AuthDefault = user.PermissionDenyAll + s := newTestServer(t, c) + defer s.closeDatabases() + + // User and admin, grant access to "gol*" topics + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) + require.Nil(t, s.userManager.AllowAccess("ben", "gol*", user.PermissionRead)) // Wildcard! + + start, timeTaken := time.Now(), atomic.Int64{} + go func() { + rr := request(t, s, "GET", "/gold/json", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 200, rr.Code) + timeTaken.Store(time.Since(start).Milliseconds()) + }() + time.Sleep(500 * time.Millisecond) + + // Reset access + rr := request(t, s, "DELETE", "/v1/users/access", `{"username": "ben", "topic":"gol*"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Wait for connection to be killed; this will fail if the connection is never killed + waitFor(t, func() bool { + return timeTaken.Load() >= 500 + }) +} diff --git a/server/server_firebase.go b/server/server_firebase.go index 6318b98e..b8158d2f 100644 --- a/server/server_firebase.go +++ b/server/server_firebase.go @@ -144,17 +144,18 @@ func toFirebaseMessage(m *message, auther user.Auther) (*messaging.Message, erro } if allowForward { data = map[string]string{ - "id": m.ID, - "time": fmt.Sprintf("%d", m.Time), - "event": m.Event, - "topic": m.Topic, - "priority": fmt.Sprintf("%d", m.Priority), - "tags": strings.Join(m.Tags, ","), - "click": m.Click, - "icon": m.Icon, - "title": m.Title, - "message": m.Message, - "encoding": m.Encoding, + "id": m.ID, + "time": fmt.Sprintf("%d", m.Time), + "event": m.Event, + "topic": m.Topic, + "priority": fmt.Sprintf("%d", m.Priority), + "tags": strings.Join(m.Tags, ","), + "click": m.Click, + "icon": m.Icon, + "title": m.Title, + "message": m.Message, + "content_type": m.ContentType, + "encoding": m.Encoding, } if len(m.Actions) > 0 { actions, err := json.Marshal(m.Actions) diff --git a/server/server_firebase_test.go b/server/server_firebase_test.go index f18abe13..fb27ea05 100644 --- a/server/server_firebase_test.go +++ b/server/server_firebase_test.go @@ -182,6 +182,7 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) { "title": "some title", "message": "this is a message", "actions": `[{"id":"123","action":"view","label":"Open page","clear":true,"url":"https://ntfy.sh"},{"id":"456","action":"http","label":"Close door","clear":false,"url":"https://door.com/close","method":"PUT","headers":{"really":"yes"}}]`, + "content_type": "", "encoding": "", "attachment_name": "some file.jpg", "attachment_type": "image/jpeg", @@ -203,6 +204,7 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) { "title": "some title", "message": "this is a message", "actions": `[{"id":"123","action":"view","label":"Open page","clear":true,"url":"https://ntfy.sh"},{"id":"456","action":"http","label":"Close door","clear":false,"url":"https://door.com/close","method":"PUT","headers":{"really":"yes"}}]`, + "content_type": "", "encoding": "", "attachment_name": "some file.jpg", "attachment_type": "image/jpeg", diff --git a/server/server_manager.go b/server/server_manager.go index 891366f7..66d449de 100644 --- a/server/server_manager.go +++ b/server/server_manager.go @@ -15,6 +15,7 @@ func (s *Server) execManager() { s.pruneTokens() s.pruneAttachments() s.pruneMessages() + s.pruneAndNotifyWebPushSubscriptions() // Message count per topic var messagesCached int @@ -73,9 +74,14 @@ func (s *Server) execManager() { } // Print stats - s.mu.Lock() + s.mu.RLock() messagesCount, topicsCount, visitorsCount := s.messages, len(s.topics), len(s.visitors) - s.mu.Unlock() + s.mu.RUnlock() + + // Update stats + s.updateAndWriteStats(messagesCount) + + // Log stats log. Tag(tagManager). Fields(log.Context{ diff --git a/server/server_metrics.go b/server/server_metrics.go index d3f17929..88fa9f15 100644 --- a/server/server_metrics.go +++ b/server/server_metrics.go @@ -15,6 +15,8 @@ var ( metricEmailsPublishedFailure prometheus.Counter metricEmailsReceivedSuccess prometheus.Counter metricEmailsReceivedFailure prometheus.Counter + metricCallsMadeSuccess prometheus.Counter + metricCallsMadeFailure prometheus.Counter metricUnifiedPushPublishedSuccess prometheus.Counter metricMatrixPublishedSuccess prometheus.Counter metricMatrixPublishedFailure prometheus.Counter @@ -57,6 +59,12 @@ func initMetrics() { metricEmailsReceivedFailure = prometheus.NewCounter(prometheus.CounterOpts{ Name: "ntfy_emails_received_failure", }) + metricCallsMadeSuccess = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "ntfy_calls_made_success", + }) + metricCallsMadeFailure = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "ntfy_calls_made_failure", + }) metricUnifiedPushPublishedSuccess = prometheus.NewCounter(prometheus.CounterOpts{ Name: "ntfy_unifiedpush_published_success", }) @@ -95,6 +103,8 @@ func initMetrics() { metricEmailsPublishedFailure, metricEmailsReceivedSuccess, metricEmailsReceivedFailure, + metricCallsMadeSuccess, + metricCallsMadeFailure, metricUnifiedPushPublishedSuccess, metricMatrixPublishedSuccess, metricMatrixPublishedFailure, diff --git a/server/server_middleware.go b/server/server_middleware.go index 5c83cf70..b1428154 100644 --- a/server/server_middleware.go +++ b/server/server_middleware.go @@ -51,7 +51,16 @@ func (s *Server) limitRequestsWithTopic(next handleFunc) handleFunc { func (s *Server) ensureWebEnabled(next handleFunc) handleFunc { return func(w http.ResponseWriter, r *http.Request, v *visitor) error { - if !s.config.EnableWeb { + if s.config.WebRoot == "" { + return errHTTPNotFound + } + return next(w, r, v) + } +} + +func (s *Server) ensureWebPushEnabled(next handleFunc) handleFunc { + return func(w http.ResponseWriter, r *http.Request, v *visitor) error { + if s.config.WebRoot == "" || s.config.WebPushPublicKey == "" { return errHTTPNotFound } return next(w, r, v) @@ -76,6 +85,24 @@ func (s *Server) ensureUser(next handleFunc) handleFunc { }) } +func (s *Server) ensureAdmin(next handleFunc) handleFunc { + return s.ensureUserManager(func(w http.ResponseWriter, r *http.Request, v *visitor) error { + if !v.User().IsAdmin() { + return errHTTPUnauthorized + } + return next(w, r, v) + }) +} + +func (s *Server) ensureCallsEnabled(next handleFunc) handleFunc { + return func(w http.ResponseWriter, r *http.Request, v *visitor) error { + if s.config.TwilioAccount == "" || s.userManager == nil { + return errHTTPNotFound + } + return next(w, r, v) + } +} + func (s *Server) ensurePaymentsEnabled(next handleFunc) handleFunc { return func(w http.ResponseWriter, r *http.Request, v *visitor) error { if s.config.StripeSecretKey == "" || s.stripe == nil { diff --git a/server/server_payments.go b/server/server_payments.go index cb585966..1e98d059 100644 --- a/server/server_payments.go +++ b/server/server_payments.go @@ -68,6 +68,7 @@ func (s *Server) handleBillingTiersGet(w http.ResponseWriter, _ *http.Request, _ Messages: freeTier.MessageLimit, MessagesExpiryDuration: int64(freeTier.MessageExpiryDuration.Seconds()), Emails: freeTier.EmailLimit, + Calls: freeTier.CallLimit, Reservations: freeTier.ReservationsLimit, AttachmentTotalSize: freeTier.AttachmentTotalSizeLimit, AttachmentFileSize: freeTier.AttachmentFileSizeLimit, @@ -96,6 +97,7 @@ func (s *Server) handleBillingTiersGet(w http.ResponseWriter, _ *http.Request, _ Messages: tier.MessageLimit, MessagesExpiryDuration: int64(tier.MessageExpiryDuration.Seconds()), Emails: tier.EmailLimit, + Calls: tier.CallLimit, Reservations: tier.ReservationLimit, AttachmentTotalSize: tier.AttachmentTotalSizeLimit, AttachmentFileSize: tier.AttachmentFileSizeLimit, diff --git a/server/server_test.go b/server/server_test.go index 943fc3a8..d60c775a 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -18,11 +18,11 @@ import ( "runtime/debug" "strings" "sync" + "sync/atomic" "testing" "time" - "github.com/stretchr/testify/assert" - + "github.com/SherClockHolmes/webpush-go" "github.com/stretchr/testify/require" "heckel.io/ntfy/log" "heckel.io/ntfy/util" @@ -220,11 +220,7 @@ func TestServer_StaticSites(t *testing.T) { rr = request(t, s, "GET", "/mytopic", "", nil) require.Equal(t, 200, rr.Code) - require.Contains(t, rr.Body.String(), ``) - - rr = request(t, s, "GET", "/static/css/home.css", "", nil) - require.Equal(t, 200, rr.Code) - require.Contains(t, rr.Body.String(), `/* general styling */`) + require.Contains(t, rr.Body.String(), ``) rr = request(t, s, "GET", "/docs", "", nil) require.Equal(t, 301, rr.Code) @@ -234,7 +230,7 @@ func TestServer_StaticSites(t *testing.T) { func TestServer_WebEnabled(t *testing.T) { conf := newTestConfig(t) - conf.EnableWeb = false + conf.WebRoot = "" // Disable web app s := newTestServer(t, conf) rr := request(t, s, "GET", "/", "", nil) @@ -243,11 +239,17 @@ func TestServer_WebEnabled(t *testing.T) { rr = request(t, s, "GET", "/config.js", "", nil) require.Equal(t, 404, rr.Code) + rr = request(t, s, "GET", "/sw.js", "", nil) + require.Equal(t, 404, rr.Code) + + rr = request(t, s, "GET", "/app.html", "", nil) + require.Equal(t, 404, rr.Code) + rr = request(t, s, "GET", "/static/css/home.css", "", nil) require.Equal(t, 404, rr.Code) conf2 := newTestConfig(t) - conf2.EnableWeb = true + conf2.WebRoot = "/" s2 := newTestServer(t, conf2) rr = request(t, s2, "GET", "/", "", nil) @@ -256,8 +258,34 @@ func TestServer_WebEnabled(t *testing.T) { rr = request(t, s2, "GET", "/config.js", "", nil) require.Equal(t, 200, rr.Code) - rr = request(t, s2, "GET", "/static/css/home.css", "", nil) + rr = request(t, s2, "GET", "/sw.js", "", nil) require.Equal(t, 200, rr.Code) + + rr = request(t, s2, "GET", "/app.html", "", nil) + require.Equal(t, 200, rr.Code) +} + +func TestServer_WebPushEnabled(t *testing.T) { + conf := newTestConfig(t) + conf.WebRoot = "" // Disable web app + s := newTestServer(t, conf) + + rr := request(t, s, "GET", "/manifest.webmanifest", "", nil) + require.Equal(t, 404, rr.Code) + + conf2 := newTestConfig(t) + s2 := newTestServer(t, conf2) + + rr = request(t, s2, "GET", "/manifest.webmanifest", "", nil) + require.Equal(t, 404, rr.Code) + + conf3 := newTestConfigWithWebPush(t) + s3 := newTestServer(t, conf3) + + rr = request(t, s3, "GET", "/manifest.webmanifest", "", nil) + require.Equal(t, 200, rr.Code) + require.Equal(t, "application/manifest+json", rr.Header().Get("Content-Type")) + } func TestServer_PublishLargeMessage(t *testing.T) { @@ -301,6 +329,27 @@ func TestServer_PublishPriority(t *testing.T) { require.Equal(t, 40007, toHTTPError(t, response.Body.String()).Code) } +func TestServer_PublishPriority_SpecialHTTPHeader(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + + response := request(t, s, "POST", "/mytopic", "test", map[string]string{ + "Priority": "u=4", + "X-Priority": "5", + }) + require.Equal(t, 5, toMessage(t, response.Body.String()).Priority) + + response = request(t, s, "POST", "/mytopic?priority=4", "test", map[string]string{ + "Priority": "u=9", + }) + require.Equal(t, 4, toMessage(t, response.Body.String()).Priority) + + response = request(t, s, "POST", "/mytopic", "test", map[string]string{ + "p": "2", + "priority": "u=9, i", + }) + require.Equal(t, 2, toMessage(t, response.Body.String()).Priority) +} + func TestServer_PublishGETOnlyOneTopic(t *testing.T) { // This tests a bug that allowed publishing topics with a comma in the name (no ticket) @@ -463,6 +512,8 @@ func TestServer_PublishAtAndPrune(t *testing.T) { messages := toMessages(t, response.Body.String()) require.Equal(t, 1, len(messages)) // Not affected by pruning require.Equal(t, "a message", messages[0].Message) + + time.Sleep(time.Second) // FIXME CI failing not sure why } func TestServer_PublishAndMultiPoll(t *testing.T) { @@ -1199,7 +1250,20 @@ func TestServer_PublishDelayedEmail_Fail(t *testing.T) { "E-Mail": "test@example.com", "Delay": "20 min", }) - require.Equal(t, 400, response.Code) + require.Equal(t, 40003, toHTTPError(t, response.Body.String()).Code) +} + +func TestServer_PublishDelayedCall_Fail(t *testing.T) { + c := newTestConfigWithAuthFile(t) + c.TwilioAccount = "AC1234567890" + c.TwilioAuthToken = "AAEAA1234567890" + c.TwilioPhoneNumber = "+1234567890" + s := newTestServer(t, c) + response := request(t, s, "PUT", "/mytopic", "fail", map[string]string{ + "Call": "yes", + "Delay": "20 min", + }) + require.Equal(t, 40037, toHTTPError(t, response.Body.String()).Code) } func TestServer_PublishEmailNoMailer_Fail(t *testing.T) { @@ -1477,6 +1541,39 @@ func TestServer_PublishActions_AndPoll(t *testing.T) { require.Equal(t, "target_temp_f=65", m.Actions[1].Body) } +func TestServer_PublishMarkdown(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", "**make this bold**", map[string]string{ + "Content-Type": "text/markdown", + }) + require.Equal(t, 200, response.Code) + + m := toMessage(t, response.Body.String()) + require.Equal(t, "**make this bold**", m.Message) + require.Equal(t, "text/markdown", m.ContentType) +} + +func TestServer_PublishMarkdown_QueryParam(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic?md=1", "**make this bold**", nil) + require.Equal(t, 200, response.Code) + + m := toMessage(t, response.Body.String()) + require.Equal(t, "**make this bold**", m.Message) + require.Equal(t, "text/markdown", m.ContentType) +} + +func TestServer_PublishMarkdown_NotMarkdown(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", "**make this bold**", map[string]string{ + "Content-Type": "not-markdown", + }) + require.Equal(t, 200, response.Code) + + m := toMessage(t, response.Body.String()) + require.Equal(t, "", m.ContentType) +} + func TestServer_PublishAsJSON(t *testing.T) { s := newTestServer(t, newTestConfig(t)) body := `{"topic":"mytopic","message":"A message","title":"a title\nwith lines","tags":["tag1","tag 2"],` + @@ -1494,12 +1591,25 @@ func TestServer_PublishAsJSON(t *testing.T) { require.Equal(t, "google.pdf", m.Attachment.Name) require.Equal(t, "http://ntfy.sh", m.Click) require.Equal(t, "https://ntfy.sh/static/img/ntfy.png", m.Icon) + require.Equal(t, "", m.ContentType) require.Equal(t, 4, m.Priority) require.True(t, m.Time > time.Now().Unix()+29*60) require.True(t, m.Time < time.Now().Unix()+31*60) } +func TestServer_PublishAsJSON_Markdown(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + body := `{"topic":"mytopic","message":"**This is bold**","markdown":true}` + response := request(t, s, "PUT", "/", body, nil) + require.Equal(t, 200, response.Code) + + m := toMessage(t, response.Body.String()) + require.Equal(t, "mytopic", m.Topic) + require.Equal(t, "**This is bold**", m.Message) + require.Equal(t, "text/markdown", m.ContentType) +} + func TestServer_PublishAsJSON_RateLimit_MessageDailyLimit(t *testing.T) { // Publishing as JSON follows a different path. This ensures that rate // limiting works for this endpoint as well @@ -2106,8 +2216,8 @@ func TestServer_PublishWhileUpdatingStatsWithLotsOfMessages(t *testing.T) { start = time.Now() response := request(t, s, "PUT", "/mytopic", "some body", nil) m := toMessage(t, response.Body.String()) - assert.Equal(t, "some body", m.Message) - assert.True(t, time.Since(start) < 100*time.Millisecond) + require.Equal(t, "some body", m.Message) + require.True(t, time.Since(start) < 100*time.Millisecond) log.Info("Done: Publishing message; took %s", time.Since(start).Round(time.Millisecond)) // Wait for all goroutines @@ -2399,6 +2509,184 @@ func TestServer_SubscriberRateLimiting_ProtectedTopics_WithDefaultReadWrite(t *t require.Nil(t, s.topics["announcements"].rateVisitor) } +func TestServer_MessageHistoryAndStatsEndpoint(t *testing.T) { + c := newTestConfig(t) + c.ManagerInterval = 2 * time.Second + s := newTestServer(t, c) + + // Publish some messages, and get stats + for i := 0; i < 5; i++ { + response := request(t, s, "POST", "/mytopic", "some message", nil) + require.Equal(t, 200, response.Code) + } + require.Equal(t, int64(5), s.messages) + require.Equal(t, []int64{0}, s.messagesHistory) + + response := request(t, s, "GET", "/v1/stats", "", nil) + require.Equal(t, 200, response.Code) + require.Equal(t, `{"messages":5,"messages_rate":0}`+"\n", response.Body.String()) + + // Run manager and see message history update + s.execManager() + require.Equal(t, []int64{0, 5}, s.messagesHistory) + + response = request(t, s, "GET", "/v1/stats", "", nil) + require.Equal(t, 200, response.Code) + require.Equal(t, `{"messages":5,"messages_rate":2.5}`+"\n", response.Body.String()) // 5 messages in 2 seconds = 2.5 messages per second + + // Publish some more messages + for i := 0; i < 10; i++ { + response := request(t, s, "POST", "/mytopic", "some message", nil) + require.Equal(t, 200, response.Code) + } + require.Equal(t, int64(15), s.messages) + require.Equal(t, []int64{0, 5}, s.messagesHistory) + + response = request(t, s, "GET", "/v1/stats", "", nil) + require.Equal(t, 200, response.Code) + require.Equal(t, `{"messages":15,"messages_rate":2.5}`+"\n", response.Body.String()) // Rate did not update yet + + // Run manager and see message history update + s.execManager() + require.Equal(t, []int64{0, 5, 15}, s.messagesHistory) + + response = request(t, s, "GET", "/v1/stats", "", nil) + require.Equal(t, 200, response.Code) + require.Equal(t, `{"messages":15,"messages_rate":3.75}`+"\n", response.Body.String()) // 15 messages in 4 seconds = 3.75 messages per second +} + +func TestServer_MessageHistoryMaxSize(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + for i := 0; i < 20; i++ { + s.messages = int64(i) + s.execManager() + } + require.Equal(t, []int64{10, 11, 12, 13, 14, 15, 16, 17, 18, 19}, s.messagesHistory) +} + +func TestServer_MessageCountPersistence(t *testing.T) { + c := newTestConfig(t) + s := newTestServer(t, c) + s.messages = 1234 + s.execManager() + waitFor(t, func() bool { + messages, err := s.messageCache.Stats() + require.Nil(t, err) + return messages == 1234 + }) + + s = newTestServer(t, c) + require.Equal(t, int64(1234), s.messages) +} + +func TestServer_PublishWithUTF8MimeHeader(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + + response := request(t, s, "POST", "/mytopic", "some attachment", map[string]string{ + "X-Filename": "some =?UTF-8?q?=C3=A4?=ttachment.txt", + "X-Message": "=?UTF-8?B?8J+HqfCfh6o=?=", + "X-Title": "=?UTF-8?B?bnRmeSDlvojmo5I=?=, no really I mean it! =?UTF-8?Q?This is q=C3=BC=C3=B6ted-print=C3=A4ble.?=", + "X-Tags": "=?UTF-8?B?8J+HqfCfh6o=?=, =?UTF-8?B?bnRmeSDlvojmo5I=?=", + "X-Click": "=?uTf-8?b?aHR0cHM6Ly/wn5KpLmxh?=", + "X-Actions": "http, \"=?utf-8?q?Mettre =C3=A0 jour?=\", \"https://my.tld/webhook/netbird-update\"; =?utf-8?b?aHR0cCwg6L+Z5piv5LiA5Liq5qCH562+LCBodHRwczovL/CfkqkubGE=?=", + }) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "🇩🇪", m.Message) + require.Equal(t, "ntfy 很棒, no really I mean it! This is qüöted-printäble.", m.Title) + require.Equal(t, "some ättachment.txt", m.Attachment.Name) + require.Equal(t, "🇩🇪", m.Tags[0]) + require.Equal(t, "ntfy 很棒", m.Tags[1]) + require.Equal(t, "https://💩.la", m.Click) + require.Equal(t, "Mettre à jour", m.Actions[0].Label) + require.Equal(t, "http", m.Actions[1].Action) + require.Equal(t, "这是一个标签", m.Actions[1].Label) + require.Equal(t, "https://💩.la", m.Actions[1].URL) +} + +func TestServer_UpstreamBaseURL_Success(t *testing.T) { + var pollID atomic.Pointer[string] + upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + require.Nil(t, err) + require.Equal(t, "/87c9cddf7b0105f5fe849bf084c6e600be0fde99be3223335199b4965bd7b735", r.URL.Path) + require.Equal(t, "", string(body)) + require.NotEmpty(t, r.Header.Get("X-Poll-ID")) + pollID.Store(util.String(r.Header.Get("X-Poll-ID"))) + })) + defer upstreamServer.Close() + + c := newTestConfigWithAuthFile(t) + c.BaseURL = "http://myserver.internal" + c.UpstreamBaseURL = upstreamServer.URL + s := newTestServer(t, c) + + // Send message, and wait for upstream server to receive it + response := request(t, s, "PUT", "/mytopic", `hi there`, nil) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.NotEmpty(t, m.ID) + require.Equal(t, "hi there", m.Message) + waitFor(t, func() bool { + pID := pollID.Load() + return pID != nil && *pID == m.ID + }) +} + +func TestServer_UpstreamBaseURL_With_Access_Token_Success(t *testing.T) { + var pollID atomic.Pointer[string] + upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + require.Nil(t, err) + require.Equal(t, "/a1c72bcb4daf5af54d13ef86aea8f76c11e8b88320d55f1811d5d7b173bcc1df", r.URL.Path) + require.Equal(t, "Bearer tk_1234567890", r.Header.Get("Authorization")) + require.Equal(t, "", string(body)) + require.NotEmpty(t, r.Header.Get("X-Poll-ID")) + pollID.Store(util.String(r.Header.Get("X-Poll-ID"))) + })) + defer upstreamServer.Close() + + c := newTestConfigWithAuthFile(t) + c.BaseURL = "http://myserver.internal" + c.UpstreamBaseURL = upstreamServer.URL + c.UpstreamAccessToken = "tk_1234567890" + s := newTestServer(t, c) + + // Send message, and wait for upstream server to receive it + response := request(t, s, "PUT", "/mytopic1", `hi there`, nil) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.NotEmpty(t, m.ID) + require.Equal(t, "hi there", m.Message) + waitFor(t, func() bool { + pID := pollID.Load() + return pID != nil && *pID == m.ID + }) +} + +func TestServer_UpstreamBaseURL_DoNotForwardUnifiedPush(t *testing.T) { + upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("UnifiedPush messages should not be forwarded") + })) + defer upstreamServer.Close() + + c := newTestConfigWithAuthFile(t) + c.BaseURL = "http://myserver.internal" + c.UpstreamBaseURL = upstreamServer.URL + s := newTestServer(t, c) + + // Send UP message, this should not forward to upstream server + response := request(t, s, "PUT", "/mytopic?up=1", `hi there`, nil) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.NotEmpty(t, m.ID) + require.Equal(t, "hi there", m.Message) + + // Forwarding is done asynchronously, so wait a bit. + // This ensures that the t.Fatal above is actually not triggered. + time.Sleep(500 * time.Millisecond) +} + func newTestConfig(t *testing.T) *Config { conf := NewConfig() conf.BaseURL = "http://127.0.0.1:12345" @@ -2408,19 +2696,33 @@ func newTestConfig(t *testing.T) *Config { return conf } -func newTestConfigWithAuthFile(t *testing.T) *Config { - conf := newTestConfig(t) +func configureAuth(t *testing.T, conf *Config) *Config { conf.AuthFile = filepath.Join(t.TempDir(), "user.db") conf.AuthStartupQueries = "pragma journal_mode = WAL; pragma synchronous = normal; pragma temp_store = memory;" conf.AuthBcryptCost = bcrypt.MinCost // This speeds up tests a lot return conf } +func newTestConfigWithAuthFile(t *testing.T) *Config { + conf := newTestConfig(t) + conf = configureAuth(t, conf) + return conf +} + +func newTestConfigWithWebPush(t *testing.T) *Config { + conf := newTestConfig(t) + privateKey, publicKey, err := webpush.GenerateVAPIDKeys() + require.Nil(t, err) + conf.WebPushFile = filepath.Join(t.TempDir(), "webpush.db") + conf.WebPushEmailAddress = "testing@example.com" + conf.WebPushPrivateKey = privateKey + conf.WebPushPublicKey = publicKey + return conf +} + func newTestServer(t *testing.T, config *Config) *Server { server, err := New(config) - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) return server } @@ -2500,7 +2802,7 @@ func waitForWithMaxWait(t *testing.T, maxWait time.Duration, f func() bool) { if f() { return } - time.Sleep(100 * time.Millisecond) + time.Sleep(50 * time.Millisecond) } t.Fatalf("Function f did not succeed after %v: %v", maxWait, string(debug.Stack())) } diff --git a/server/server_twilio.go b/server/server_twilio.go new file mode 100644 index 00000000..093abe63 --- /dev/null +++ b/server/server_twilio.go @@ -0,0 +1,176 @@ +package server + +import ( + "bytes" + "encoding/xml" + "fmt" + "heckel.io/ntfy/log" + "heckel.io/ntfy/user" + "heckel.io/ntfy/util" + "io" + "net/http" + "net/url" + "strings" +) + +const ( + twilioCallFormat = ` + + + + You have a message from notify on topic %s. Message: + + %s + + End of message. + + This message was sent by user %s. It will be repeated three times. + To unsubscribe from calls like this, remove your phone number in the notify web app. + + + Goodbye. +` +) + +// convertPhoneNumber checks if the given phone number is verified for the given user, and if so, returns the verified +// phone number. It also converts a boolean string ("yes", "1", "true") to the first verified phone number. +// If the user is anonymous, it will return an error. +func (s *Server) convertPhoneNumber(u *user.User, phoneNumber string) (string, *errHTTP) { + if u == nil { + return "", errHTTPBadRequestAnonymousCallsNotAllowed + } + phoneNumbers, err := s.userManager.PhoneNumbers(u.ID) + if err != nil { + return "", errHTTPInternalError + } else if len(phoneNumbers) == 0 { + return "", errHTTPBadRequestPhoneNumberNotVerified + } + if toBool(phoneNumber) { + return phoneNumbers[0], nil + } else if util.Contains(phoneNumbers, phoneNumber) { + return phoneNumber, nil + } + for _, p := range phoneNumbers { + if p == phoneNumber { + return phoneNumber, nil + } + } + return "", errHTTPBadRequestPhoneNumberNotVerified +} + +// callPhone calls the Twilio API to make a phone call to the given phone number, using the given message. +// Failures will be logged, but not returned to the caller. +func (s *Server) callPhone(v *visitor, r *http.Request, m *message, to string) { + u, sender := v.User(), m.Sender.String() + if u != nil { + sender = u.Name + } + body := fmt.Sprintf(twilioCallFormat, xmlEscapeText(m.Topic), xmlEscapeText(m.Message), xmlEscapeText(sender)) + data := url.Values{} + data.Set("From", s.config.TwilioPhoneNumber) + data.Set("To", to) + data.Set("Twiml", body) + ev := logvrm(v, r, m).Tag(tagTwilio).Field("twilio_to", to).FieldIf("twilio_body", body, log.TraceLevel).Debug("Sending Twilio request") + response, err := s.callPhoneInternal(data) + if err != nil { + ev.Field("twilio_response", response).Err(err).Warn("Error sending Twilio request") + minc(metricCallsMadeFailure) + return + } + ev.FieldIf("twilio_response", response, log.TraceLevel).Debug("Received successful Twilio response") + minc(metricCallsMadeSuccess) +} + +func (s *Server) callPhoneInternal(data url.Values) (string, error) { + requestURL := fmt.Sprintf("%s/2010-04-01/Accounts/%s/Calls.json", s.config.TwilioCallsBaseURL, s.config.TwilioAccount) + req, err := http.NewRequest(http.MethodPost, requestURL, strings.NewReader(data.Encode())) + if err != nil { + return "", err + } + req.Header.Set("User-Agent", "ntfy/"+s.config.Version) + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Authorization", util.BasicAuth(s.config.TwilioAccount, s.config.TwilioAuthToken)) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + response, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + return string(response), nil +} + +func (s *Server) verifyPhoneNumber(v *visitor, r *http.Request, phoneNumber, channel string) error { + ev := logvr(v, r).Tag(tagTwilio).Field("twilio_to", phoneNumber).Field("twilio_channel", channel).Debug("Sending phone verification") + data := url.Values{} + data.Set("To", phoneNumber) + data.Set("Channel", channel) + requestURL := fmt.Sprintf("%s/v2/Services/%s/Verifications", s.config.TwilioVerifyBaseURL, s.config.TwilioVerifyService) + req, err := http.NewRequest(http.MethodPost, requestURL, strings.NewReader(data.Encode())) + if err != nil { + return err + } + req.Header.Set("User-Agent", "ntfy/"+s.config.Version) + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Authorization", util.BasicAuth(s.config.TwilioAccount, s.config.TwilioAuthToken)) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + response, err := io.ReadAll(resp.Body) + if err != nil { + ev.Err(err).Warn("Error sending Twilio phone verification request") + return err + } + ev.FieldIf("twilio_response", string(response), log.TraceLevel).Debug("Received Twilio phone verification response") + return nil +} + +func (s *Server) verifyPhoneNumberCheck(v *visitor, r *http.Request, phoneNumber, code string) error { + ev := logvr(v, r).Tag(tagTwilio).Field("twilio_to", phoneNumber).Debug("Checking phone verification") + data := url.Values{} + data.Set("To", phoneNumber) + data.Set("Code", code) + requestURL := fmt.Sprintf("%s/v2/Services/%s/VerificationCheck", s.config.TwilioVerifyBaseURL, s.config.TwilioVerifyService) + req, err := http.NewRequest(http.MethodPost, requestURL, strings.NewReader(data.Encode())) + if err != nil { + return err + } + req.Header.Set("User-Agent", "ntfy/"+s.config.Version) + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Authorization", util.BasicAuth(s.config.TwilioAccount, s.config.TwilioAuthToken)) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } else if resp.StatusCode != http.StatusOK { + if ev.IsTrace() { + response, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + ev.Field("twilio_response", string(response)) + } + ev.Warn("Twilio phone verification failed with status code %d", resp.StatusCode) + if resp.StatusCode == http.StatusNotFound { + return errHTTPGonePhoneVerificationExpired + } + return errHTTPInternalError + } + response, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + if ev.IsTrace() { + ev.Field("twilio_response", string(response)).Trace("Received successful Twilio phone verification response") + } else if ev.IsDebug() { + ev.Debug("Received successful Twilio phone verification response") + } + return nil +} + +func xmlEscapeText(text string) string { + var buf bytes.Buffer + _ = xml.EscapeText(&buf, []byte(text)) + return buf.String() +} diff --git a/server/server_twilio_test.go b/server/server_twilio_test.go new file mode 100644 index 00000000..af694a77 --- /dev/null +++ b/server/server_twilio_test.go @@ -0,0 +1,264 @@ +package server + +import ( + "github.com/stretchr/testify/require" + "heckel.io/ntfy/user" + "heckel.io/ntfy/util" + "io" + "net/http" + "net/http/httptest" + "sync/atomic" + "testing" +) + +func TestServer_Twilio_Call_Add_Verify_Call_Delete_Success(t *testing.T) { + var called, verified atomic.Bool + var code atomic.Pointer[string] + twilioVerifyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + require.Nil(t, err) + require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization")) + if r.URL.Path == "/v2/Services/VA1234567890/Verifications" { + if code.Load() != nil { + t.Fatal("Should be only called once") + } + require.Equal(t, "Channel=sms&To=%2B12223334444", string(body)) + code.Store(util.String("123456")) + } else if r.URL.Path == "/v2/Services/VA1234567890/VerificationCheck" { + if verified.Load() { + t.Fatal("Should be only called once") + } + require.Equal(t, "Code=123456&To=%2B12223334444", string(body)) + verified.Store(true) + } else { + t.Fatal("Unexpected path:", r.URL.Path) + } + })) + defer twilioVerifyServer.Close() + twilioCallsServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if called.Load() { + t.Fatal("Should be only called once") + } + body, err := io.ReadAll(r.Body) + require.Nil(t, err) + require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Calls.json", r.URL.Path) + require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization")) + require.Equal(t, "From=%2B1234567890&To=%2B12223334444&Twiml=%0A%3CResponse%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay+loop%3D%223%22%3E%0A%09%09You+have+a+message+from+notify+on+topic+mytopic.+Message%3A%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09hi+there%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09End+of+message.%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09This+message+was+sent+by+user+phil.+It+will+be+repeated+three+times.%0A%09%09To+unsubscribe+from+calls+like+this%2C+remove+your+phone+number+in+the+notify+web+app.%0A%09%09%3Cbreak+time%3D%223s%22%2F%3E%0A%09%3C%2FSay%3E%0A%09%3CSay%3EGoodbye.%3C%2FSay%3E%0A%3C%2FResponse%3E", string(body)) + called.Store(true) + })) + defer twilioCallsServer.Close() + + c := newTestConfigWithAuthFile(t) + c.TwilioVerifyBaseURL = twilioVerifyServer.URL + c.TwilioCallsBaseURL = twilioCallsServer.URL + c.TwilioAccount = "AC1234567890" + c.TwilioAuthToken = "AAEAA1234567890" + c.TwilioPhoneNumber = "+1234567890" + c.TwilioVerifyService = "VA1234567890" + s := newTestServer(t, c) + + // Add tier and user + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "pro", + MessageLimit: 10, + CallLimit: 1, + })) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.ChangeTier("phil", "pro")) + u, err := s.userManager.User("phil") + require.Nil(t, err) + + // Send verification code for phone number + response := request(t, s, "PUT", "/v1/account/phone/verify", `{"number":"+12223334444","channel":"sms"}`, map[string]string{ + "authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, response.Code) + waitFor(t, func() bool { + return *code.Load() == "123456" + }) + + // Add phone number with code + response = request(t, s, "PUT", "/v1/account/phone", `{"number":"+12223334444","code":"123456"}`, map[string]string{ + "authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, response.Code) + waitFor(t, func() bool { + return verified.Load() + }) + phoneNumbers, err := s.userManager.PhoneNumbers(u.ID) + require.Nil(t, err) + require.Equal(t, 1, len(phoneNumbers)) + require.Equal(t, "+12223334444", phoneNumbers[0]) + + // Do the thing + response = request(t, s, "POST", "/mytopic", "hi there", map[string]string{ + "authorization": util.BasicAuth("phil", "phil"), + "x-call": "yes", + }) + require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message) + waitFor(t, func() bool { + return called.Load() + }) + + // Remove the phone number + response = request(t, s, "DELETE", "/v1/account/phone", `{"number":"+12223334444"}`, map[string]string{ + "authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, response.Code) + + // Verify the phone number is gone from the DB + phoneNumbers, err = s.userManager.PhoneNumbers(u.ID) + require.Nil(t, err) + require.Equal(t, 0, len(phoneNumbers)) +} + +func TestServer_Twilio_Call_Success(t *testing.T) { + var called atomic.Bool + twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if called.Load() { + t.Fatal("Should be only called once") + } + body, err := io.ReadAll(r.Body) + require.Nil(t, err) + require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Calls.json", r.URL.Path) + require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization")) + require.Equal(t, "From=%2B1234567890&To=%2B11122233344&Twiml=%0A%3CResponse%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay+loop%3D%223%22%3E%0A%09%09You+have+a+message+from+notify+on+topic+mytopic.+Message%3A%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09hi+there%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09End+of+message.%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09This+message+was+sent+by+user+phil.+It+will+be+repeated+three+times.%0A%09%09To+unsubscribe+from+calls+like+this%2C+remove+your+phone+number+in+the+notify+web+app.%0A%09%09%3Cbreak+time%3D%223s%22%2F%3E%0A%09%3C%2FSay%3E%0A%09%3CSay%3EGoodbye.%3C%2FSay%3E%0A%3C%2FResponse%3E", string(body)) + called.Store(true) + })) + defer twilioServer.Close() + + c := newTestConfigWithAuthFile(t) + c.TwilioCallsBaseURL = twilioServer.URL + c.TwilioAccount = "AC1234567890" + c.TwilioAuthToken = "AAEAA1234567890" + c.TwilioPhoneNumber = "+1234567890" + s := newTestServer(t, c) + + // Add tier and user + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "pro", + MessageLimit: 10, + CallLimit: 1, + })) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.ChangeTier("phil", "pro")) + u, err := s.userManager.User("phil") + require.Nil(t, err) + require.Nil(t, s.userManager.AddPhoneNumber(u.ID, "+11122233344")) + + // Do the thing + response := request(t, s, "POST", "/mytopic", "hi there", map[string]string{ + "authorization": util.BasicAuth("phil", "phil"), + "x-call": "+11122233344", + }) + require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message) + waitFor(t, func() bool { + return called.Load() + }) +} + +func TestServer_Twilio_Call_Success_With_Yes(t *testing.T) { + var called atomic.Bool + twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if called.Load() { + t.Fatal("Should be only called once") + } + body, err := io.ReadAll(r.Body) + require.Nil(t, err) + require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Calls.json", r.URL.Path) + require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization")) + require.Equal(t, "From=%2B1234567890&To=%2B11122233344&Twiml=%0A%3CResponse%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay+loop%3D%223%22%3E%0A%09%09You+have+a+message+from+notify+on+topic+mytopic.+Message%3A%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09hi+there%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09End+of+message.%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09This+message+was+sent+by+user+phil.+It+will+be+repeated+three+times.%0A%09%09To+unsubscribe+from+calls+like+this%2C+remove+your+phone+number+in+the+notify+web+app.%0A%09%09%3Cbreak+time%3D%223s%22%2F%3E%0A%09%3C%2FSay%3E%0A%09%3CSay%3EGoodbye.%3C%2FSay%3E%0A%3C%2FResponse%3E", string(body)) + called.Store(true) + })) + defer twilioServer.Close() + + c := newTestConfigWithAuthFile(t) + c.TwilioCallsBaseURL = twilioServer.URL + c.TwilioAccount = "AC1234567890" + c.TwilioAuthToken = "AAEAA1234567890" + c.TwilioPhoneNumber = "+1234567890" + s := newTestServer(t, c) + + // Add tier and user + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "pro", + MessageLimit: 10, + CallLimit: 1, + })) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.ChangeTier("phil", "pro")) + u, err := s.userManager.User("phil") + require.Nil(t, err) + require.Nil(t, s.userManager.AddPhoneNumber(u.ID, "+11122233344")) + + // Do the thing + response := request(t, s, "POST", "/mytopic", "hi there", map[string]string{ + "authorization": util.BasicAuth("phil", "phil"), + "x-call": "yes", // <<<------ + }) + require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message) + waitFor(t, func() bool { + return called.Load() + }) +} + +func TestServer_Twilio_Call_UnverifiedNumber(t *testing.T) { + c := newTestConfigWithAuthFile(t) + c.TwilioCallsBaseURL = "http://dummy.invalid" + c.TwilioAccount = "AC1234567890" + c.TwilioAuthToken = "AAEAA1234567890" + c.TwilioPhoneNumber = "+1234567890" + s := newTestServer(t, c) + + // Add tier and user + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "pro", + MessageLimit: 10, + CallLimit: 1, + })) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) + require.Nil(t, s.userManager.ChangeTier("phil", "pro")) + + // Do the thing + response := request(t, s, "POST", "/mytopic", "test", map[string]string{ + "authorization": util.BasicAuth("phil", "phil"), + "x-call": "+11122233344", + }) + require.Equal(t, 40034, toHTTPError(t, response.Body.String()).Code) +} + +func TestServer_Twilio_Call_InvalidNumber(t *testing.T) { + c := newTestConfigWithAuthFile(t) + c.TwilioCallsBaseURL = "https://127.0.0.1" + c.TwilioAccount = "AC1234567890" + c.TwilioAuthToken = "AAEAA1234567890" + c.TwilioPhoneNumber = "+1234567890" + s := newTestServer(t, c) + + response := request(t, s, "POST", "/mytopic", "test", map[string]string{ + "x-call": "+invalid", + }) + require.Equal(t, 40033, toHTTPError(t, response.Body.String()).Code) +} + +func TestServer_Twilio_Call_Anonymous(t *testing.T) { + c := newTestConfigWithAuthFile(t) + c.TwilioCallsBaseURL = "https://127.0.0.1" + c.TwilioAccount = "AC1234567890" + c.TwilioAuthToken = "AAEAA1234567890" + c.TwilioPhoneNumber = "+1234567890" + s := newTestServer(t, c) + + response := request(t, s, "POST", "/mytopic", "test", map[string]string{ + "x-call": "+123123", + }) + require.Equal(t, 40035, toHTTPError(t, response.Body.String()).Code) +} + +func TestServer_Twilio_Call_Unconfigured(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "POST", "/mytopic", "test", map[string]string{ + "x-call": "+1234", + }) + require.Equal(t, 40032, toHTTPError(t, response.Body.String()).Code) +} diff --git a/server/server_webpush.go b/server/server_webpush.go new file mode 100644 index 00000000..bb0f5408 --- /dev/null +++ b/server/server_webpush.go @@ -0,0 +1,171 @@ +package server + +import ( + "encoding/json" + "fmt" + "net/http" + "regexp" + "strings" + + "github.com/SherClockHolmes/webpush-go" + "heckel.io/ntfy/log" + "heckel.io/ntfy/user" +) + +const ( + webPushTopicSubscribeLimit = 50 +) + +var ( + webPushAllowedEndpointsPatterns = []string{ + "https://*.google.com/", + "https://*.googleapis.com/", + "https://*.mozilla.com/", + "https://*.mozaws.net/", + "https://*.windows.com/", + "https://*.microsoft.com/", + "https://*.apple.com/", + } + webPushAllowedEndpointsRegex *regexp.Regexp +) + +func init() { + for i, pattern := range webPushAllowedEndpointsPatterns { + webPushAllowedEndpointsPatterns[i] = strings.ReplaceAll(strings.ReplaceAll(pattern, ".", "\\."), "*", ".+") + } + allPatterns := fmt.Sprintf("^(%s)", strings.Join(webPushAllowedEndpointsPatterns, "|")) + webPushAllowedEndpointsRegex = regexp.MustCompile(allPatterns) +} + +func (s *Server) handleWebPushUpdate(w http.ResponseWriter, r *http.Request, v *visitor) error { + req, err := readJSONWithLimit[apiWebPushUpdateSubscriptionRequest](r.Body, jsonBodyBytesLimit, false) + if err != nil || req.Endpoint == "" || req.P256dh == "" || req.Auth == "" { + return errHTTPBadRequestWebPushSubscriptionInvalid + } else if !webPushAllowedEndpointsRegex.MatchString(req.Endpoint) { + return errHTTPBadRequestWebPushEndpointUnknown + } else if len(req.Topics) > webPushTopicSubscribeLimit { + return errHTTPBadRequestWebPushTopicCountTooHigh + } + topics, err := s.topicsFromIDs(req.Topics...) + if err != nil { + return err + } + if s.userManager != nil { + u := v.User() + for _, t := range topics { + if err := s.userManager.Authorize(u, t.ID, user.PermissionRead); err != nil { + logvr(v, r).With(t).Err(err).Debug("Access to topic %s not authorized", t.ID) + return errHTTPForbidden.With(t) + } + } + } + if err := s.webPush.UpsertSubscription(req.Endpoint, req.Auth, req.P256dh, v.MaybeUserID(), v.IP(), req.Topics); err != nil { + return err + } + return s.writeJSON(w, newSuccessResponse()) +} + +func (s *Server) handleWebPushDelete(w http.ResponseWriter, r *http.Request, _ *visitor) error { + req, err := readJSONWithLimit[apiWebPushUpdateSubscriptionRequest](r.Body, jsonBodyBytesLimit, false) + if err != nil || req.Endpoint == "" { + return errHTTPBadRequestWebPushSubscriptionInvalid + } + if err := s.webPush.RemoveSubscriptionsByEndpoint(req.Endpoint); err != nil { + return err + } + return s.writeJSON(w, newSuccessResponse()) +} + +func (s *Server) publishToWebPushEndpoints(v *visitor, m *message) { + subscriptions, err := s.webPush.SubscriptionsForTopic(m.Topic) + if err != nil { + logvm(v, m).Err(err).With(v, m).Warn("Unable to publish web push messages") + return + } + log.Tag(tagWebPush).With(v, m).Debug("Publishing web push message to %d subscribers", len(subscriptions)) + payload, err := json.Marshal(newWebPushPayload(fmt.Sprintf("%s/%s", s.config.BaseURL, m.Topic), m)) + if err != nil { + log.Tag(tagWebPush).Err(err).With(v, m).Warn("Unable to marshal expiring payload") + return + } + for _, subscription := range subscriptions { + if err := s.sendWebPushNotification(subscription, payload, v, m); err != nil { + log.Tag(tagWebPush).Err(err).With(v, m, subscription).Warn("Unable to publish web push message") + } + } +} + +func (s *Server) pruneAndNotifyWebPushSubscriptions() { + if s.config.WebPushPublicKey == "" { + return + } + go func() { + if err := s.pruneAndNotifyWebPushSubscriptionsInternal(); err != nil { + log.Tag(tagWebPush).Err(err).Warn("Unable to prune or notify web push subscriptions") + } + }() +} + +func (s *Server) pruneAndNotifyWebPushSubscriptionsInternal() error { + // Expire old subscriptions + if err := s.webPush.RemoveExpiredSubscriptions(s.config.WebPushExpiryDuration); err != nil { + return err + } + // Notify subscriptions that will expire soon + subscriptions, err := s.webPush.SubscriptionsExpiring(s.config.WebPushExpiryWarningDuration) + if err != nil { + return err + } else if len(subscriptions) == 0 { + return nil + } + payload, err := json.Marshal(newWebPushSubscriptionExpiringPayload()) + if err != nil { + return err + } + warningSent := make([]*webPushSubscription, 0) + for _, subscription := range subscriptions { + if err := s.sendWebPushNotification(subscription, payload); err != nil { + log.Tag(tagWebPush).Err(err).With(subscription).Warn("Unable to publish expiry imminent warning") + continue + } + warningSent = append(warningSent, subscription) + } + if err := s.webPush.MarkExpiryWarningSent(warningSent); err != nil { + return err + } + log.Tag(tagWebPush).Debug("Expired old subscriptions and published %d expiry imminent warnings", len(subscriptions)) + return nil +} + +func (s *Server) sendWebPushNotification(sub *webPushSubscription, message []byte, contexters ...log.Contexter) error { + log.Tag(tagWebPush).With(sub).With(contexters...).Debug("Sending web push message") + payload := &webpush.Subscription{ + Endpoint: sub.Endpoint, + Keys: webpush.Keys{ + Auth: sub.Auth, + P256dh: sub.P256dh, + }, + } + resp, err := webpush.SendNotification(message, payload, &webpush.Options{ + Subscriber: s.config.WebPushEmailAddress, + VAPIDPublicKey: s.config.WebPushPublicKey, + VAPIDPrivateKey: s.config.WebPushPrivateKey, + Urgency: webpush.UrgencyHigh, // iOS requires this to ensure delivery + TTL: int(s.config.CacheDuration.Seconds()), + }) + if err != nil { + log.Tag(tagWebPush).With(sub).With(contexters...).Err(err).Debug("Unable to publish web push message, removing endpoint") + if err := s.webPush.RemoveSubscriptionsByEndpoint(sub.Endpoint); err != nil { + return err + } + return err + } + if (resp.StatusCode < 200 || resp.StatusCode > 299) && resp.StatusCode != 429 { + log.Tag(tagWebPush).With(sub).With(contexters...).Field("response_code", resp.StatusCode).Debug("Unable to publish web push message, unexpected response") + if err := s.webPush.RemoveSubscriptionsByEndpoint(sub.Endpoint); err != nil { + return err + } + return errHTTPInternalErrorWebPushUnableToPublish.With(sub).With(contexters...) + } + return nil +} diff --git a/server/server_webpush_test.go b/server/server_webpush_test.go new file mode 100644 index 00000000..c0db79c6 --- /dev/null +++ b/server/server_webpush_test.go @@ -0,0 +1,256 @@ +package server + +import ( + "encoding/json" + "fmt" + "github.com/stretchr/testify/require" + "heckel.io/ntfy/user" + "heckel.io/ntfy/util" + "io" + "net/http" + "net/http/httptest" + "net/netip" + "strings" + "sync/atomic" + "testing" + "time" +) + +const ( + testWebPushEndpoint = "https://updates.push.services.mozilla.com/wpush/v1/AAABBCCCDDEEEFFF" +) + +func TestServer_WebPush_Disabled(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + + response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), nil) + require.Equal(t, 404, response.Code) +} + +func TestServer_WebPush_TopicAdd(t *testing.T) { + s := newTestServer(t, newTestConfigWithWebPush(t)) + + response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), nil) + require.Equal(t, 200, response.Code) + require.Equal(t, `{"success":true}`+"\n", response.Body.String()) + + subs, err := s.webPush.SubscriptionsForTopic("test-topic") + require.Nil(t, err) + + require.Len(t, subs, 1) + require.Equal(t, subs[0].Endpoint, testWebPushEndpoint) + require.Equal(t, subs[0].P256dh, "p256dh-key") + require.Equal(t, subs[0].Auth, "auth-key") + require.Equal(t, subs[0].UserID, "") +} + +func TestServer_WebPush_TopicAdd_InvalidEndpoint(t *testing.T) { + s := newTestServer(t, newTestConfigWithWebPush(t)) + + response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, "https://ddos-target.example.com/webpush"), nil) + require.Equal(t, 400, response.Code) + require.Equal(t, `{"code":40039,"http":400,"error":"invalid request: web push endpoint unknown"}`+"\n", response.Body.String()) +} + +func TestServer_WebPush_TopicAdd_TooManyTopics(t *testing.T) { + s := newTestServer(t, newTestConfigWithWebPush(t)) + + topicList := make([]string, 51) + for i := range topicList { + topicList[i] = util.RandomString(5) + } + + response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, topicList, testWebPushEndpoint), nil) + require.Equal(t, 400, response.Code) + require.Equal(t, `{"code":40040,"http":400,"error":"invalid request: too many web push topic subscriptions"}`+"\n", response.Body.String()) +} + +func TestServer_WebPush_TopicUnsubscribe(t *testing.T) { + s := newTestServer(t, newTestConfigWithWebPush(t)) + + addSubscription(t, s, testWebPushEndpoint, "test-topic") + requireSubscriptionCount(t, s, "test-topic", 1) + + response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{}, testWebPushEndpoint), nil) + require.Equal(t, 200, response.Code) + require.Equal(t, `{"success":true}`+"\n", response.Body.String()) + + requireSubscriptionCount(t, s, "test-topic", 0) +} + +func TestServer_WebPush_Delete(t *testing.T) { + s := newTestServer(t, newTestConfigWithWebPush(t)) + + addSubscription(t, s, testWebPushEndpoint, "test-topic") + requireSubscriptionCount(t, s, "test-topic", 1) + + response := request(t, s, "DELETE", "/v1/webpush", fmt.Sprintf(`{"endpoint":"%s"}`, testWebPushEndpoint), nil) + require.Equal(t, 200, response.Code) + require.Equal(t, `{"success":true}`+"\n", response.Body.String()) + + requireSubscriptionCount(t, s, "test-topic", 0) +} + +func TestServer_WebPush_TopicSubscribeProtected_Allowed(t *testing.T) { + config := configureAuth(t, newTestConfigWithWebPush(t)) + config.AuthDefault = user.PermissionDenyAll + s := newTestServer(t, config) + + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) + require.Nil(t, s.userManager.AllowAccess("ben", "test-topic", user.PermissionReadWrite)) + + response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 200, response.Code) + require.Equal(t, `{"success":true}`+"\n", response.Body.String()) + + subs, err := s.webPush.SubscriptionsForTopic("test-topic") + require.Nil(t, err) + require.Len(t, subs, 1) + require.True(t, strings.HasPrefix(subs[0].UserID, "u_")) +} + +func TestServer_WebPush_TopicSubscribeProtected_Denied(t *testing.T) { + config := configureAuth(t, newTestConfigWithWebPush(t)) + config.AuthDefault = user.PermissionDenyAll + s := newTestServer(t, config) + + response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), nil) + require.Equal(t, 403, response.Code) + + requireSubscriptionCount(t, s, "test-topic", 0) +} + +func TestServer_WebPush_DeleteAccountUnsubscribe(t *testing.T) { + config := configureAuth(t, newTestConfigWithWebPush(t)) + s := newTestServer(t, config) + + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) + require.Nil(t, s.userManager.AllowAccess("ben", "test-topic", user.PermissionReadWrite)) + + response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + + require.Equal(t, 200, response.Code) + require.Equal(t, `{"success":true}`+"\n", response.Body.String()) + + requireSubscriptionCount(t, s, "test-topic", 1) + + request(t, s, "DELETE", "/v1/account", `{"password":"ben"}`, map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + // should've been deleted with the account + requireSubscriptionCount(t, s, "test-topic", 0) +} + +func TestServer_WebPush_Publish(t *testing.T) { + s := newTestServer(t, newTestConfigWithWebPush(t)) + + var received atomic.Bool + pushService := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := io.ReadAll(r.Body) + require.Nil(t, err) + require.Equal(t, "/push-receive", r.URL.Path) + require.Equal(t, "high", r.Header.Get("Urgency")) + require.Equal(t, "", r.Header.Get("Topic")) + received.Store(true) + })) + defer pushService.Close() + + addSubscription(t, s, pushService.URL+"/push-receive", "test-topic") + request(t, s, "POST", "/test-topic", "web push test", nil) + + waitFor(t, func() bool { + return received.Load() + }) +} + +func TestServer_WebPush_Publish_RemoveOnError(t *testing.T) { + s := newTestServer(t, newTestConfigWithWebPush(t)) + + var received atomic.Bool + pushService := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := io.ReadAll(r.Body) + require.Nil(t, err) + w.WriteHeader(http.StatusGone) + received.Store(true) + })) + defer pushService.Close() + + addSubscription(t, s, pushService.URL+"/push-receive", "test-topic", "test-topic-abc") + requireSubscriptionCount(t, s, "test-topic", 1) + requireSubscriptionCount(t, s, "test-topic-abc", 1) + + request(t, s, "POST", "/test-topic", "web push test", nil) + + waitFor(t, func() bool { + return received.Load() + }) + + // Receiving the 410 should've caused the publisher to expire all subscriptions on the endpoint + + requireSubscriptionCount(t, s, "test-topic", 0) + requireSubscriptionCount(t, s, "test-topic-abc", 0) +} + +func TestServer_WebPush_Expiry(t *testing.T) { + s := newTestServer(t, newTestConfigWithWebPush(t)) + + var received atomic.Bool + + pushService := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := io.ReadAll(r.Body) + require.Nil(t, err) + w.WriteHeader(200) + w.Write([]byte(``)) + received.Store(true) + })) + defer pushService.Close() + + addSubscription(t, s, pushService.URL+"/push-receive", "test-topic") + requireSubscriptionCount(t, s, "test-topic", 1) + + _, err := s.webPush.db.Exec("UPDATE subscription SET updated_at = ?", time.Now().Add(-7*24*time.Hour).Unix()) + require.Nil(t, err) + + s.pruneAndNotifyWebPushSubscriptions() + requireSubscriptionCount(t, s, "test-topic", 1) + + waitFor(t, func() bool { + return received.Load() + }) + + _, err = s.webPush.db.Exec("UPDATE subscription SET updated_at = ?", time.Now().Add(-9*24*time.Hour).Unix()) + require.Nil(t, err) + + s.pruneAndNotifyWebPushSubscriptions() + waitFor(t, func() bool { + subs, err := s.webPush.SubscriptionsForTopic("test-topic") + require.Nil(t, err) + return len(subs) == 0 + }) +} + +func payloadForTopics(t *testing.T, topics []string, endpoint string) string { + topicsJSON, err := json.Marshal(topics) + require.Nil(t, err) + + return fmt.Sprintf(`{ + "topics": %s, + "endpoint": "%s", + "p256dh": "p256dh-key", + "auth": "auth-key" + }`, topicsJSON, endpoint) +} + +func addSubscription(t *testing.T, s *Server, endpoint string, topics ...string) { + require.Nil(t, s.webPush.UpsertSubscription(endpoint, "kSC3T8aN1JCQxxPdrFLrZg", "BMKKbxdUU_xLS7G1Wh5AN8PvWOjCzkCuKZYb8apcqYrDxjOF_2piggBnoJLQYx9IeSD70fNuwawI3e9Y8m3S3PE", "u_123", netip.MustParseAddr("1.2.3.4"), topics)) // Test auth and p256dh +} + +func requireSubscriptionCount(t *testing.T, s *Server, topic string, expectedLength int) { + subs, err := s.webPush.SubscriptionsForTopic(topic) + require.Nil(t, err) + require.Len(t, subs, expectedLength) +} diff --git a/server/smtp_sender.go b/server/smtp_sender.go index 26a0e0e6..9093687e 100644 --- a/server/smtp_sender.go +++ b/server/smtp_sender.go @@ -4,14 +4,15 @@ import ( _ "embed" // required by go:embed "encoding/json" "fmt" - "heckel.io/ntfy/log" - "heckel.io/ntfy/util" "mime" "net" "net/smtp" "strings" "sync" "time" + + "heckel.io/ntfy/log" + "heckel.io/ntfy/util" ) type mailer interface { @@ -131,31 +132,23 @@ This message was sent by {ip} at {time} via {topicURL}` } var ( - //go:embed "mailer_emoji.json" + //go:embed "mailer_emoji_map.json" emojisJSON string ) -type emoji struct { - Emoji string `json:"emoji"` - Aliases []string `json:"aliases"` -} - func toEmojis(tags []string) (emojisOut []string, tagsOut []string, err error) { - var emojis []emoji - if err = json.Unmarshal([]byte(emojisJSON), &emojis); err != nil { + var emojiMap map[string]string + if err = json.Unmarshal([]byte(emojisJSON), &emojiMap); err != nil { return nil, nil, err } tagsOut = make([]string, 0) emojisOut = make([]string, 0) -nextTag: - for _, t := range tags { // TODO Super inefficient; we should just create a .json file with a map - for _, e := range emojis { - if util.Contains(e.Aliases, t) { - emojisOut = append(emojisOut, e.Emoji) - continue nextTag - } + for _, t := range tags { + if emoji, ok := emojiMap[t]; ok { + emojisOut = append(emojisOut, emoji) + } else { + tagsOut = append(tagsOut, t) } - tagsOut = append(tagsOut, t) } return } diff --git a/server/smtp_server.go b/server/smtp_server.go index fe7e3298..76f439e7 100644 --- a/server/smtp_server.go +++ b/server/smtp_server.go @@ -10,6 +10,7 @@ import ( "io" "mime" "mime/multipart" + "mime/quotedprintable" "net" "net/http" "net/http/httptest" @@ -296,6 +297,8 @@ func readTextMailBody(reader io.Reader, contentType, transferEncoding string) (s func readPlainTextMailBody(reader io.Reader, transferEncoding string) (string, error) { if strings.ToLower(transferEncoding) == "base64" { reader = base64.NewDecoder(base64.StdEncoding, reader) + } else if strings.ToLower(transferEncoding) == "quoted-printable" { + reader = quotedprintable.NewReader(reader) } body, err := io.ReadAll(reader) if err != nil { diff --git a/server/smtp_server_test.go b/server/smtp_server_test.go index 1e504521..bdee0785 100644 --- a/server/smtp_server_test.go +++ b/server/smtp_server_test.go @@ -303,6 +303,39 @@ BBBBBBBBBBBBBBBBBBBBBBBBB` writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued") } +func TestSmtpBackend_Plaintext_QuotedPrintable(t *testing.T) { + email := `EHLO example.com +MAIL FROM: phil@example.com +RCPT TO: mytopic@ntfy.sh +DATA +Date: Tue, 28 Dec 2021 00:30:10 +0100 +Message-ID: +Subject: and one more +From: Phil +To: mytopic@ntfy.sh +Content-Type: text/plain; charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + +what's +=C3=A0&=C3=A9"'(-=C3=A8_=C3=A7=C3=A0) +=3D=3D=3D=3D=3D +up +. +` + s, c, conf, scanner := newTestSMTPServer(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 = "" + defer s.Close() + defer c.Close() + writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued") +} + func TestSmtpBackend_Unsupported(t *testing.T) { email := `EHLO example.com MAIL FROM: phil@example.com @@ -390,6 +423,49 @@ L0VOIj4KClRoaXMgaXMgYSB0ZXN0IG1lc3NhZ2UgZnJvbSBUcnVlTkFTIENPUkUuCg== writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued") } +func TestSmtpBackend_MultipartQuotedPrintable(t *testing.T) { + email := `EHLO example.com +MAIL FROM: phil@example.com +RCPT TO: ntfy-mytopic@ntfy.sh +DATA +MIME-Version: 1.0 +Date: Tue, 28 Dec 2021 00:30:10 +0100 +Message-ID: +Subject: and one more +From: Phil +To: ntfy-mytopic@ntfy.sh +Content-Type: multipart/alternative; boundary="000000000000f3320b05d42915c9" + +--000000000000f3320b05d42915c9 +Content-Type: text/html; charset="UTF-8" + +html, ignore me + +--000000000000f3320b05d42915c9 +Content-Type: text/plain; charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + +what's +=C3=A0&=C3=A9"'(-=C3=A8_=C3=A7=C3=A0) +=3D=3D=3D=3D=3D +up + +--000000000000f3320b05d42915c9-- +. +` + s, c, _, scanner := newTestSMTPServer(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)) + }) + defer s.Close() + defer c.Close() + writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued") +} + func TestSmtpBackend_NestedMultipartBase64(t *testing.T) { email := `EHLO example.com MAIL FROM: test@mydomain.me diff --git a/server/topic.go b/server/topic.go index e093a610..5dfafbe3 100644 --- a/server/topic.go +++ b/server/topic.go @@ -1,11 +1,12 @@ package server import ( - "heckel.io/ntfy/log" - "heckel.io/ntfy/util" "math/rand" "sync" "time" + + "heckel.io/ntfy/log" + "heckel.io/ntfy/util" ) const ( @@ -44,10 +45,16 @@ func newTopic(id string) *topic { } // Subscribe subscribes to this topic -func (t *topic) Subscribe(s subscriber, userID string, cancel func()) int { +func (t *topic) Subscribe(s subscriber, userID string, cancel func()) (subscriberID int) { t.mu.Lock() defer t.mu.Unlock() - subscriberID := rand.Int() + for i := 0; i < 5; i++ { // Best effort retry + subscriberID = rand.Int() + _, exists := t.subscribers[subscriberID] + if !exists { + break + } + } t.subscribers[subscriberID] = &topicSubscriber{ userID: userID, // May be empty subscriber: s, @@ -134,24 +141,40 @@ func (t *topic) Keepalive() { t.lastAccess = time.Now() } -// CancelSubscribers calls the cancel function for all subscribers, forcing -func (t *topic) CancelSubscribers(exceptUserID string) { +// CancelSubscribersExceptUser calls the cancel function for all subscribers, forcing +func (t *topic) CancelSubscribersExceptUser(exceptUserID string) { t.mu.Lock() defer t.mu.Unlock() for _, s := range t.subscribers { if s.userID != exceptUserID { - log. - Tag(tagSubscribe). - With(t). - Fields(log.Context{ - "user_id": s.userID, - }). - Debug("Canceling subscriber %s", s.userID) - s.cancel() + t.cancelUserSubscriber(s) } } } +// CancelSubscriberUser kills the subscriber with the given user ID +func (t *topic) CancelSubscriberUser(userID string) { + t.mu.RLock() + defer t.mu.RUnlock() + for _, s := range t.subscribers { + if s.userID == userID { + t.cancelUserSubscriber(s) + return + } + } +} + +func (t *topic) cancelUserSubscriber(s *topicSubscriber) { + log. + Tag(tagSubscribe). + With(t). + Fields(log.Context{ + "user_id": s.userID, + }). + Debug("Canceling subscriber with user ID %s", s.userID) + s.cancel() +} + func (t *topic) Context() log.Context { t.mu.RLock() defer t.mu.RUnlock() diff --git a/server/topic_test.go b/server/topic_test.go index b22bad55..41a29cfd 100644 --- a/server/topic_test.go +++ b/server/topic_test.go @@ -1,13 +1,15 @@ package server import ( - "github.com/stretchr/testify/require" + "math/rand" "sync/atomic" "testing" "time" + + "github.com/stretchr/testify/require" ) -func TestTopic_CancelSubscribers(t *testing.T) { +func TestTopic_CancelSubscribersExceptUser(t *testing.T) { t.Parallel() subFn := func(v *visitor, msg *message) error { @@ -25,11 +27,34 @@ func TestTopic_CancelSubscribers(t *testing.T) { to.Subscribe(subFn, "", cancelFn1) to.Subscribe(subFn, "u_phil", cancelFn2) - to.CancelSubscribers("u_phil") + to.CancelSubscribersExceptUser("u_phil") require.True(t, canceled1.Load()) require.False(t, canceled2.Load()) } +func TestTopic_CancelSubscribersUser(t *testing.T) { + t.Parallel() + + subFn := func(v *visitor, msg *message) error { + return nil + } + canceled1 := atomic.Bool{} + cancelFn1 := func() { + canceled1.Store(true) + } + canceled2 := atomic.Bool{} + cancelFn2 := func() { + canceled2.Store(true) + } + to := newTopic("mytopic") + to.Subscribe(subFn, "u_another", cancelFn1) + to.Subscribe(subFn, "u_phil", cancelFn2) + + to.CancelSubscriberUser("u_phil") + require.False(t, canceled1.Load()) + require.True(t, canceled2.Load()) +} + func TestTopic_Keepalive(t *testing.T) { t.Parallel() @@ -39,3 +64,29 @@ func TestTopic_Keepalive(t *testing.T) { require.True(t, to.LastAccess().Unix() >= time.Now().Unix()-2) require.True(t, to.LastAccess().Unix() <= time.Now().Unix()+2) } + +func TestTopic_Subscribe_DuplicateID(t *testing.T) { + t.Parallel() + to := newTopic("mytopic") + + // Fix random seed to force same number generation + rand.Seed(1) + a := rand.Int() + to.subscribers[a] = &topicSubscriber{ + userID: "a", + subscriber: nil, + cancel: func() {}, + } + + subFn := func(v *visitor, msg *message) error { + return nil + } + + // Force rand.Int to generate the same id once more + rand.Seed(1) + id := to.Subscribe(subFn, "b", func() {}) + res := to.subscribers[id] + + require.NotEqual(t, id, a) + require.Equal(t, "b", res.userID, "b") +} diff --git a/server/types.go b/server/types.go index b11424f2..eeb566fc 100644 --- a/server/types.go +++ b/server/types.go @@ -1,12 +1,13 @@ package server import ( - "heckel.io/ntfy/log" - "heckel.io/ntfy/user" "net/http" "net/netip" "time" + "heckel.io/ntfy/log" + "heckel.io/ntfy/user" + "heckel.io/ntfy/util" ) @@ -24,23 +25,24 @@ const ( // message represents a message published to a topic type message struct { - ID string `json:"id"` // Random message ID - Time int64 `json:"time"` // Unix time in seconds - Expires int64 `json:"expires,omitempty"` // Unix time in seconds (not required for open/keepalive) - Event string `json:"event"` // One of the above - Topic string `json:"topic"` - Title string `json:"title,omitempty"` - Message string `json:"message,omitempty"` - Priority int `json:"priority,omitempty"` - Tags []string `json:"tags,omitempty"` - Click string `json:"click,omitempty"` - Icon string `json:"icon,omitempty"` - Actions []*action `json:"actions,omitempty"` - Attachment *attachment `json:"attachment,omitempty"` - PollID string `json:"poll_id,omitempty"` - Encoding string `json:"encoding,omitempty"` // empty for raw UTF-8, or "base64" for encoded bytes - Sender netip.Addr `json:"-"` // IP address of uploader, used for rate limiting - User string `json:"-"` // Username of the uploader, used to associated attachments + ID string `json:"id"` // Random message ID + Time int64 `json:"time"` // Unix time in seconds + Expires int64 `json:"expires,omitempty"` // Unix time in seconds (not required for open/keepalive) + Event string `json:"event"` // One of the above + Topic string `json:"topic"` + Title string `json:"title,omitempty"` + Message string `json:"message,omitempty"` + Priority int `json:"priority,omitempty"` + Tags []string `json:"tags,omitempty"` + Click string `json:"click,omitempty"` + Icon string `json:"icon,omitempty"` + Actions []*action `json:"actions,omitempty"` + Attachment *attachment `json:"attachment,omitempty"` + PollID string `json:"poll_id,omitempty"` + ContentType string `json:"content_type,omitempty"` // text/plain by default (if empty), or text/markdown + Encoding string `json:"encoding,omitempty"` // empty for raw UTF-8, or "base64" for encoded bytes + Sender netip.Addr `json:"-"` // IP address of uploader, used for rate limiting + User string `json:"-"` // UserID of the uploader, used to associated attachments } func (m *message) Context() log.Context { @@ -99,8 +101,10 @@ type publishMessage struct { Icon string `json:"icon"` Actions []action `json:"actions"` Attach string `json:"attach"` + Markdown bool `json:"markdown"` Filename string `json:"filename"` Email string `json:"email"` + Call string `json:"call"` Delay string `json:"delay"` } @@ -239,6 +243,45 @@ type apiHealthResponse struct { Healthy bool `json:"healthy"` } +type apiStatsResponse struct { + Messages int64 `json:"messages"` + MessagesRate float64 `json:"messages_rate"` // Average number of messages per second +} + +type apiUserAddRequest struct { + Username string `json:"username"` + Password string `json:"password"` + Tier string `json:"tier"` + // Do not add 'role' here. We don't want to add admins via the API. +} + +type apiUserResponse struct { + Username string `json:"username"` + Role string `json:"role"` + Tier string `json:"tier,omitempty"` + Grants []*apiUserGrantResponse `json:"grants,omitempty"` +} + +type apiUserGrantResponse struct { + Topic string `json:"topic"` // This may be a pattern + Permission string `json:"permission"` +} + +type apiUserDeleteRequest struct { + Username string `json:"username"` +} + +type apiAccessAllowRequest struct { + Username string `json:"username"` + Topic string `json:"topic"` // This may be a pattern + Permission string `json:"permission"` +} + +type apiAccessResetRequest struct { + Username string `json:"username"` + Topic string `json:"topic"` +} + type apiAccountCreateRequest struct { Username string `json:"username"` Password string `json:"password"` @@ -272,6 +315,16 @@ type apiAccountTokenResponse struct { Expires int64 `json:"expires,omitempty"` // Unix timestamp } +type apiAccountPhoneNumberVerifyRequest struct { + Number string `json:"number"` + Channel string `json:"channel"` +} + +type apiAccountPhoneNumberAddRequest struct { + Number string `json:"number"` + Code string `json:"code"` // Only set when adding a phone number +} + type apiAccountTier struct { Code string `json:"code"` Name string `json:"name"` @@ -282,6 +335,7 @@ type apiAccountLimits struct { Messages int64 `json:"messages"` MessagesExpiryDuration int64 `json:"messages_expiry_duration"` Emails int64 `json:"emails"` + Calls int64 `json:"calls"` Reservations int64 `json:"reservations"` AttachmentTotalSize int64 `json:"attachment_total_size"` AttachmentFileSize int64 `json:"attachment_file_size"` @@ -294,6 +348,8 @@ type apiAccountStats struct { MessagesRemaining int64 `json:"messages_remaining"` Emails int64 `json:"emails"` EmailsRemaining int64 `json:"emails_remaining"` + Calls int64 `json:"calls"` + CallsRemaining int64 `json:"calls_remaining"` Reservations int64 `json:"reservations"` ReservationsRemaining int64 `json:"reservations_remaining"` AttachmentTotalSize int64 `json:"attachment_total_size"` @@ -323,6 +379,7 @@ type apiAccountResponse struct { Subscriptions []*user.Subscription `json:"subscriptions,omitempty"` Reservations []*apiAccountReservation `json:"reservations,omitempty"` Tokens []*apiAccountTokenResponse `json:"tokens,omitempty"` + PhoneNumbers []string `json:"phone_numbers,omitempty"` Tier *apiAccountTier `json:"tier,omitempty"` Limits *apiAccountLimits `json:"limits,omitempty"` Stats *apiAccountStats `json:"stats,omitempty"` @@ -340,8 +397,12 @@ type apiConfigResponse struct { EnableLogin bool `json:"enable_login"` EnableSignup bool `json:"enable_signup"` EnablePayments bool `json:"enable_payments"` + EnableCalls bool `json:"enable_calls"` + EnableEmails bool `json:"enable_emails"` EnableReservations bool `json:"enable_reservations"` + EnableWebPush bool `json:"enable_web_push"` BillingContact string `json:"billing_contact"` + WebPushPublicKey string `json:"web_push_public_key"` DisallowedTopics []string `json:"disallowed_topics"` } @@ -406,3 +467,75 @@ type apiStripeSubscriptionDeletedEvent struct { ID string `json:"id"` Customer string `json:"customer"` } + +type apiWebPushUpdateSubscriptionRequest struct { + Endpoint string `json:"endpoint"` + Auth string `json:"auth"` + P256dh string `json:"p256dh"` + Topics []string `json:"topics"` +} + +// List of possible Web Push events (see sw.js) +const ( + webPushMessageEvent = "message" + webPushExpiringEvent = "subscription_expiring" +) + +type webPushPayload struct { + Event string `json:"event"` + SubscriptionID string `json:"subscription_id"` + Message *message `json:"message"` +} + +func newWebPushPayload(subscriptionID string, message *message) *webPushPayload { + return &webPushPayload{ + Event: webPushMessageEvent, + SubscriptionID: subscriptionID, + Message: message, + } +} + +type webPushControlMessagePayload struct { + Event string `json:"event"` +} + +func newWebPushSubscriptionExpiringPayload() *webPushControlMessagePayload { + return &webPushControlMessagePayload{ + Event: webPushExpiringEvent, + } +} + +type webPushSubscription struct { + ID string + Endpoint string + Auth string + P256dh string + UserID string +} + +func (w *webPushSubscription) Context() log.Context { + return map[string]any{ + "web_push_subscription_id": w.ID, + "web_push_subscription_user_id": w.UserID, + "web_push_subscription_endpoint": w.Endpoint, + } +} + +// https://developer.mozilla.org/en-US/docs/Web/Manifest +type webManifestResponse struct { + Name string `json:"name"` + Description string `json:"description"` + ShortName string `json:"short_name"` + Scope string `json:"scope"` + StartURL string `json:"start_url"` + Display string `json:"display"` + BackgroundColor string `json:"background_color"` + ThemeColor string `json:"theme_color"` + Icons []*webManifestIcon `json:"icons"` +} + +type webManifestIcon struct { + SRC string `json:"src"` + Sizes string `json:"sizes"` + Type string `json:"type"` +} diff --git a/server/util.go b/server/util.go index 390e7fb1..09536765 100644 --- a/server/util.go +++ b/server/util.go @@ -5,16 +5,31 @@ import ( "fmt" "heckel.io/ntfy/util" "io" + "mime" "net/http" "net/netip" + "regexp" "strings" ) +var ( + mimeDecoder mime.WordDecoder + priorityHeaderIgnoreRegex = regexp.MustCompile(`^u=\d,\s*(i|\d)$|^u=\d$`) +) + func readBoolParam(r *http.Request, defaultValue bool, names ...string) bool { value := strings.ToLower(readParam(r, names...)) if value == "" { return defaultValue } + return toBool(value) +} + +func isBoolValue(value string) bool { + return value == "1" || value == "yes" || value == "true" || value == "0" || value == "no" || value == "false" +} + +func toBool(value string) bool { return value == "1" || value == "yes" || value == "true" } @@ -39,9 +54,9 @@ func readParam(r *http.Request, names ...string) string { func readHeaderParam(r *http.Request, names ...string) string { for _, name := range names { - value := r.Header.Get(name) + value := strings.TrimSpace(maybeDecodeHeader(name, r.Header.Get(name))) if value != "" { - return strings.TrimSpace(value) + return value } } return "" @@ -114,3 +129,27 @@ func fromContext[T any](r *http.Request, key contextKey) (T, error) { } return t, nil } + +// maybeDecodeHeader decodes the given header value if it is MIME encoded, e.g. "=?utf-8?q?Hello_World?=", +// or returns the original header value if it is not MIME encoded. It also calls maybeIgnoreSpecialHeader +// to ignore new HTTP "Priority" header. +func maybeDecodeHeader(name, value string) string { + decoded, err := mimeDecoder.DecodeHeader(value) + if err != nil { + return maybeIgnoreSpecialHeader(name, value) + } + return maybeIgnoreSpecialHeader(name, decoded) +} + +// maybeIgnoreSpecialHeader ignores new HTTP "Priority" header (see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-priority) +// +// Cloudflare (and potentially other providers) add this to requests when forwarding to the backend (ntfy), +// so we just ignore it. If the "Priority" header is set to "u=*, i" or "u=*" (by Cloudflare), the header will be ignored. +// Returning an empty string will allow the rest of the logic to continue searching for another header (x-priority, prio, p), +// or in the Query parameters. +func maybeIgnoreSpecialHeader(name, value string) string { + if strings.ToLower(name) == "priority" && priorityHeaderIgnoreRegex.MatchString(strings.TrimSpace(value)) { + return "" + } + return value +} diff --git a/server/util_test.go b/server/util_test.go index 3d062b4d..6555a81b 100644 --- a/server/util_test.go +++ b/server/util_test.go @@ -2,9 +2,9 @@ package server import ( "bytes" + "crypto/rand" "fmt" "github.com/stretchr/testify/require" - "math/rand" "net/http" "strings" "testing" @@ -75,3 +75,16 @@ Accept: */* (peeked bytes not UTF-8, peek limit of 4096 bytes reached, hex: ` + fmt.Sprintf("%x", body[:4096]) + ` ...)` require.Equal(t, expected, renderHTTPRequest(r)) } + +func TestMaybeIgnoreSpecialHeader(t *testing.T) { + require.Empty(t, maybeIgnoreSpecialHeader("priority", "u=1")) + require.Empty(t, maybeIgnoreSpecialHeader("Priority", "u=1")) + require.Empty(t, maybeIgnoreSpecialHeader("Priority", "u=1, i")) +} + +func TestMaybeDecodeHeaders(t *testing.T) { + r, _ := http.NewRequest("GET", "http://ntfy.sh/mytopic/json?since=all", nil) + r.Header.Set("Priority", "u=1") // Cloudflare priority header + r.Header.Set("X-Priority", "5") // ntfy priority header + require.Equal(t, "5", readHeaderParam(r, "x-priority", "priority", "p")) +} diff --git a/server/visitor.go b/server/visitor.go index 63a3ac60..e4c06f66 100644 --- a/server/visitor.go +++ b/server/visitor.go @@ -24,6 +24,10 @@ const ( // visitorDefaultReservationsLimit is the amount of topic names a user without a tier is allowed to reserve. // This number is zero, and changing it may have unintended consequences in the web app, or otherwise visitorDefaultReservationsLimit = int64(0) + + // visitorDefaultCallsLimit is the amount of calls a user without a tier is allowed to make. + // This number is zero, because phone numbers have to be verified first. + visitorDefaultCallsLimit = int64(0) ) // Constants used to convert a tier-user's MessageLimit (see user.Tier) into adequate request limiter @@ -56,6 +60,7 @@ type visitor struct { requestLimiter *rate.Limiter // Rate limiter for (almost) all requests (including messages) messagesLimiter *util.FixedLimiter // Rate limiter for messages emailsLimiter *util.RateLimiter // Rate limiter for emails + callsLimiter *util.FixedLimiter // Rate limiter for calls subscriptionLimiter *util.FixedLimiter // Fixed limiter for active subscriptions (ongoing connections) bandwidthLimiter *util.RateLimiter // Limiter for attachment bandwidth downloads accountLimiter *rate.Limiter // Rate limiter for account creation, may be nil @@ -79,6 +84,7 @@ type visitorLimits struct { EmailLimit int64 EmailLimitBurst int EmailLimitReplenish rate.Limit + CallLimit int64 ReservationsLimit int64 AttachmentTotalSizeLimit int64 AttachmentFileSizeLimit int64 @@ -91,6 +97,8 @@ type visitorStats struct { MessagesRemaining int64 Emails int64 EmailsRemaining int64 + Calls int64 + CallsRemaining int64 Reservations int64 ReservationsRemaining int64 AttachmentTotalSize int64 @@ -107,10 +115,11 @@ const ( ) func newVisitor(conf *Config, messageCache *messageCache, userManager *user.Manager, ip netip.Addr, user *user.User) *visitor { - var messages, emails int64 + var messages, emails, calls int64 if user != nil { messages = user.Stats.Messages emails = user.Stats.Emails + calls = user.Stats.Calls } v := &visitor{ config: conf, @@ -124,11 +133,12 @@ func newVisitor(conf *Config, messageCache *messageCache, userManager *user.Mana requestLimiter: nil, // Set in resetLimiters messagesLimiter: nil, // Set in resetLimiters, may be nil emailsLimiter: nil, // Set in resetLimiters + callsLimiter: nil, // Set in resetLimiters, may be nil bandwidthLimiter: nil, // Set in resetLimiters accountLimiter: nil, // Set in resetLimiters, may be nil authLimiter: nil, // Set in resetLimiters, may be nil } - v.resetLimitersNoLock(messages, emails, false) + v.resetLimitersNoLock(messages, emails, calls, false) return v } @@ -147,12 +157,19 @@ func (v *visitor) contextNoLock() log.Context { "visitor_messages": info.Stats.Messages, "visitor_messages_limit": info.Limits.MessageLimit, "visitor_messages_remaining": info.Stats.MessagesRemaining, - "visitor_emails": info.Stats.Emails, - "visitor_emails_limit": info.Limits.EmailLimit, - "visitor_emails_remaining": info.Stats.EmailsRemaining, "visitor_request_limiter_limit": v.requestLimiter.Limit(), "visitor_request_limiter_tokens": v.requestLimiter.Tokens(), } + if v.config.SMTPSenderFrom != "" { + fields["visitor_emails"] = info.Stats.Emails + fields["visitor_emails_limit"] = info.Limits.EmailLimit + fields["visitor_emails_remaining"] = info.Stats.EmailsRemaining + } + if v.config.TwilioAccount != "" { + fields["visitor_calls"] = info.Stats.Calls + fields["visitor_calls_limit"] = info.Limits.CallLimit + fields["visitor_calls_remaining"] = info.Stats.CallsRemaining + } if v.authLimiter != nil { fields["visitor_auth_limiter_limit"] = v.authLimiter.Limit() fields["visitor_auth_limiter_tokens"] = v.authLimiter.Tokens() @@ -216,6 +233,12 @@ func (v *visitor) EmailAllowed() bool { return v.emailsLimiter.Allow() } +func (v *visitor) CallAllowed() bool { + v.mu.RLock() // limiters could be replaced! + defer v.mu.RUnlock() + return v.callsLimiter.Allow() +} + func (v *visitor) SubscriptionAllowed() bool { v.mu.RLock() // limiters could be replaced! defer v.mu.RUnlock() @@ -296,6 +319,7 @@ func (v *visitor) Stats() *user.Stats { return &user.Stats{ Messages: v.messagesLimiter.Value(), Emails: v.emailsLimiter.Value(), + Calls: v.callsLimiter.Value(), } } @@ -304,6 +328,7 @@ func (v *visitor) ResetStats() { defer v.mu.RUnlock() v.emailsLimiter.Reset() v.messagesLimiter.Reset() + v.callsLimiter.Reset() } // User returns the visitor user, or nil if there is none @@ -334,11 +359,11 @@ func (v *visitor) SetUser(u *user.User) { shouldResetLimiters := v.user.TierID() != u.TierID() // TierID works with nil receiver v.user = u // u may be nil! if shouldResetLimiters { - var messages, emails int64 + var messages, emails, calls int64 if u != nil { - messages, emails = u.Stats.Messages, u.Stats.Emails + messages, emails, calls = u.Stats.Messages, u.Stats.Emails, u.Stats.Calls } - v.resetLimitersNoLock(messages, emails, true) + v.resetLimitersNoLock(messages, emails, calls, true) } } @@ -353,11 +378,12 @@ func (v *visitor) MaybeUserID() string { return "" } -func (v *visitor) resetLimitersNoLock(messages, emails int64, enqueueUpdate bool) { +func (v *visitor) resetLimitersNoLock(messages, emails, calls int64, enqueueUpdate bool) { limits := v.limitsNoLock() v.requestLimiter = rate.NewLimiter(limits.RequestLimitReplenish, limits.RequestLimitBurst) v.messagesLimiter = util.NewFixedLimiterWithValue(limits.MessageLimit, messages) v.emailsLimiter = util.NewRateLimiterWithValue(limits.EmailLimitReplenish, limits.EmailLimitBurst, emails) + v.callsLimiter = util.NewFixedLimiterWithValue(limits.CallLimit, calls) v.bandwidthLimiter = util.NewBytesLimiter(int(limits.AttachmentBandwidthLimit), oneDay) if v.user == nil { v.accountLimiter = rate.NewLimiter(rate.Every(v.config.VisitorAccountCreationLimitReplenish), v.config.VisitorAccountCreationLimitBurst) @@ -370,6 +396,7 @@ func (v *visitor) resetLimitersNoLock(messages, emails int64, enqueueUpdate bool go v.userManager.EnqueueUserStats(v.user.ID, &user.Stats{ Messages: messages, Emails: emails, + Calls: calls, }) } log.Fields(v.contextNoLock()).Debug("Rate limiters reset for visitor") // Must be after function, because contextNoLock() describes rate limiters @@ -398,6 +425,7 @@ func tierBasedVisitorLimits(conf *Config, tier *user.Tier) *visitorLimits { EmailLimit: tier.EmailLimit, EmailLimitBurst: util.MinMax(int(float64(tier.EmailLimit)*visitorEmailLimitBurstRate), conf.VisitorEmailLimitBurst, visitorEmailLimitBurstMax), EmailLimitReplenish: dailyLimitToRate(tier.EmailLimit), + CallLimit: tier.CallLimit, ReservationsLimit: tier.ReservationLimit, AttachmentTotalSizeLimit: tier.AttachmentTotalSizeLimit, AttachmentFileSizeLimit: tier.AttachmentFileSizeLimit, @@ -420,6 +448,7 @@ func configBasedVisitorLimits(conf *Config) *visitorLimits { EmailLimit: replenishDurationToDailyLimit(conf.VisitorEmailLimitReplenish), // Approximation! EmailLimitBurst: conf.VisitorEmailLimitBurst, EmailLimitReplenish: rate.Every(conf.VisitorEmailLimitReplenish), + CallLimit: visitorDefaultCallsLimit, ReservationsLimit: visitorDefaultReservationsLimit, AttachmentTotalSizeLimit: conf.VisitorAttachmentTotalSizeLimit, AttachmentFileSizeLimit: conf.AttachmentFileSizeLimit, @@ -465,12 +494,15 @@ func (v *visitor) Info() (*visitorInfo, error) { func (v *visitor) infoLightNoLock() *visitorInfo { messages := v.messagesLimiter.Value() emails := v.emailsLimiter.Value() + calls := v.callsLimiter.Value() limits := v.limitsNoLock() stats := &visitorStats{ Messages: messages, MessagesRemaining: zeroIfNegative(limits.MessageLimit - messages), Emails: emails, EmailsRemaining: zeroIfNegative(limits.EmailLimit - emails), + Calls: calls, + CallsRemaining: zeroIfNegative(limits.CallLimit - calls), } return &visitorInfo{ Limits: limits, diff --git a/server/webpush_store.go b/server/webpush_store.go new file mode 100644 index 00000000..b2ab0d11 --- /dev/null +++ b/server/webpush_store.go @@ -0,0 +1,280 @@ +package server + +import ( + "database/sql" + "errors" + "heckel.io/ntfy/util" + "net/netip" + "time" + + _ "github.com/mattn/go-sqlite3" // SQLite driver +) + +const ( + subscriptionIDPrefix = "wps_" + subscriptionIDLength = 10 + subscriptionEndpointLimitPerSubscriberIP = 10 +) + +var ( + errWebPushNoRows = errors.New("no rows found") + errWebPushTooManySubscriptions = errors.New("too many subscriptions") + errWebPushUserIDCannotBeEmpty = errors.New("user ID cannot be empty") +) + +const ( + createWebPushSubscriptionsTableQuery = ` + BEGIN; + CREATE TABLE IF NOT EXISTS subscription ( + id TEXT PRIMARY KEY, + endpoint TEXT NOT NULL, + key_auth TEXT NOT NULL, + key_p256dh TEXT NOT NULL, + user_id TEXT NOT NULL, + subscriber_ip TEXT NOT NULL, + updated_at INT NOT NULL, + warned_at INT NOT NULL DEFAULT 0 + ); + CREATE UNIQUE INDEX IF NOT EXISTS idx_endpoint ON subscription (endpoint); + CREATE INDEX IF NOT EXISTS idx_subscriber_ip ON subscription (subscriber_ip); + CREATE TABLE IF NOT EXISTS subscription_topic ( + subscription_id TEXT NOT NULL, + topic TEXT NOT NULL, + PRIMARY KEY (subscription_id, topic), + FOREIGN KEY (subscription_id) REFERENCES subscription (id) ON DELETE CASCADE + ); + CREATE INDEX IF NOT EXISTS idx_topic ON subscription_topic (topic); + CREATE TABLE IF NOT EXISTS schemaVersion ( + id INT PRIMARY KEY, + version INT NOT NULL + ); + COMMIT; + ` + builtinStartupQueries = ` + PRAGMA foreign_keys = ON; + ` + + selectWebPushSubscriptionIDByEndpoint = `SELECT id FROM subscription WHERE endpoint = ?` + selectWebPushSubscriptionCountBySubscriberIP = `SELECT COUNT(*) FROM subscription WHERE subscriber_ip = ?` + selectWebPushSubscriptionsForTopicQuery = ` + SELECT id, endpoint, key_auth, key_p256dh, user_id + FROM subscription_topic st + JOIN subscription s ON s.id = st.subscription_id + WHERE st.topic = ? + ORDER BY endpoint + ` + selectWebPushSubscriptionsExpiringSoonQuery = ` + SELECT id, endpoint, key_auth, key_p256dh, user_id + FROM subscription + WHERE warned_at = 0 AND updated_at <= ? + ` + insertWebPushSubscriptionQuery = ` + INSERT INTO subscription (id, endpoint, key_auth, key_p256dh, user_id, subscriber_ip, updated_at, warned_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT (endpoint) + DO UPDATE SET key_auth = excluded.key_auth, key_p256dh = excluded.key_p256dh, user_id = excluded.user_id, subscriber_ip = excluded.subscriber_ip, updated_at = excluded.updated_at, warned_at = excluded.warned_at + ` + updateWebPushSubscriptionWarningSentQuery = `UPDATE subscription SET warned_at = ? WHERE id = ?` + deleteWebPushSubscriptionByEndpointQuery = `DELETE FROM subscription WHERE endpoint = ?` + deleteWebPushSubscriptionByUserIDQuery = `DELETE FROM subscription WHERE user_id = ?` + deleteWebPushSubscriptionByAgeQuery = `DELETE FROM subscription WHERE updated_at <= ?` // Full table scan! + + insertWebPushSubscriptionTopicQuery = `INSERT INTO subscription_topic (subscription_id, topic) VALUES (?, ?)` + deleteWebPushSubscriptionTopicAllQuery = `DELETE FROM subscription_topic WHERE subscription_id = ?` +) + +// Schema management queries +const ( + currentWebPushSchemaVersion = 1 + insertWebPushSchemaVersion = `INSERT INTO schemaVersion VALUES (1, ?)` + selectWebPushSchemaVersionQuery = `SELECT version FROM schemaVersion WHERE id = 1` +) + +type webPushStore struct { + db *sql.DB +} + +func newWebPushStore(filename, startupQueries string) (*webPushStore, error) { + db, err := sql.Open("sqlite3", filename) + if err != nil { + return nil, err + } + if err := setupWebPushDB(db); err != nil { + return nil, err + } + if err := runWebPushStartupQueries(db, startupQueries); err != nil { + return nil, err + } + return &webPushStore{ + db: db, + }, nil +} + +func setupWebPushDB(db *sql.DB) error { + // If 'schemaVersion' table does not exist, this must be a new database + rows, err := db.Query(selectWebPushSchemaVersionQuery) + if err != nil { + return setupNewWebPushDB(db) + } + return rows.Close() +} + +func setupNewWebPushDB(db *sql.DB) error { + if _, err := db.Exec(createWebPushSubscriptionsTableQuery); err != nil { + return err + } + if _, err := db.Exec(insertWebPushSchemaVersion, currentWebPushSchemaVersion); err != nil { + return err + } + return nil +} + +func runWebPushStartupQueries(db *sql.DB, startupQueries string) error { + if _, err := db.Exec(startupQueries); err != nil { + return err + } + if _, err := db.Exec(builtinStartupQueries); err != nil { + return err + } + return nil +} + +// UpsertSubscription adds or updates Web Push subscriptions for the given topics and user ID. It always first deletes all +// existing entries for a given endpoint. +func (c *webPushStore) UpsertSubscription(endpoint string, auth, p256dh, userID string, subscriberIP netip.Addr, topics []string) error { + tx, err := c.db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + // Read number of subscriptions for subscriber IP address + rowsCount, err := tx.Query(selectWebPushSubscriptionCountBySubscriberIP, subscriberIP.String()) + if err != nil { + return err + } + defer rowsCount.Close() + var subscriptionCount int + if !rowsCount.Next() { + return errWebPushNoRows + } + if err := rowsCount.Scan(&subscriptionCount); err != nil { + return err + } + if err := rowsCount.Close(); err != nil { + return err + } + // Read existing subscription ID for endpoint (or create new ID) + rows, err := tx.Query(selectWebPushSubscriptionIDByEndpoint, endpoint) + if err != nil { + return err + } + defer rows.Close() + var subscriptionID string + if rows.Next() { + if err := rows.Scan(&subscriptionID); err != nil { + return err + } + } else { + if subscriptionCount >= subscriptionEndpointLimitPerSubscriberIP { + return errWebPushTooManySubscriptions + } + subscriptionID = util.RandomStringPrefix(subscriptionIDPrefix, subscriptionIDLength) + } + if err := rows.Close(); err != nil { + return err + } + // Insert or update subscription + updatedAt, warnedAt := time.Now().Unix(), 0 + if _, err = tx.Exec(insertWebPushSubscriptionQuery, subscriptionID, endpoint, auth, p256dh, userID, subscriberIP.String(), updatedAt, warnedAt); err != nil { + return err + } + // Replace all subscription topics + if _, err := tx.Exec(deleteWebPushSubscriptionTopicAllQuery, subscriptionID); err != nil { + return err + } + for _, topic := range topics { + if _, err = tx.Exec(insertWebPushSubscriptionTopicQuery, subscriptionID, topic); err != nil { + return err + } + } + return tx.Commit() +} + +// SubscriptionsForTopic returns all subscriptions for the given topic +func (c *webPushStore) SubscriptionsForTopic(topic string) ([]*webPushSubscription, error) { + rows, err := c.db.Query(selectWebPushSubscriptionsForTopicQuery, topic) + if err != nil { + return nil, err + } + defer rows.Close() + return c.subscriptionsFromRows(rows) +} + +// SubscriptionsExpiring returns all subscriptions that have not been updated for a given time period +func (c *webPushStore) SubscriptionsExpiring(warnAfter time.Duration) ([]*webPushSubscription, error) { + rows, err := c.db.Query(selectWebPushSubscriptionsExpiringSoonQuery, time.Now().Add(-warnAfter).Unix()) + if err != nil { + return nil, err + } + defer rows.Close() + return c.subscriptionsFromRows(rows) +} + +// MarkExpiryWarningSent marks the given subscriptions as having received a warning about expiring soon +func (c *webPushStore) MarkExpiryWarningSent(subscriptions []*webPushSubscription) error { + tx, err := c.db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + for _, subscription := range subscriptions { + if _, err := tx.Exec(updateWebPushSubscriptionWarningSentQuery, time.Now().Unix(), subscription.ID); err != nil { + return err + } + } + return tx.Commit() +} + +func (c *webPushStore) subscriptionsFromRows(rows *sql.Rows) ([]*webPushSubscription, error) { + subscriptions := make([]*webPushSubscription, 0) + for rows.Next() { + var id, endpoint, auth, p256dh, userID string + if err := rows.Scan(&id, &endpoint, &auth, &p256dh, &userID); err != nil { + return nil, err + } + subscriptions = append(subscriptions, &webPushSubscription{ + ID: id, + Endpoint: endpoint, + Auth: auth, + P256dh: p256dh, + UserID: userID, + }) + } + return subscriptions, nil +} + +// RemoveSubscriptionsByEndpoint removes the subscription for the given endpoint +func (c *webPushStore) RemoveSubscriptionsByEndpoint(endpoint string) error { + _, err := c.db.Exec(deleteWebPushSubscriptionByEndpointQuery, endpoint) + return err +} + +// RemoveSubscriptionsByUserID removes all subscriptions for the given user ID +func (c *webPushStore) RemoveSubscriptionsByUserID(userID string) error { + if userID == "" { + return errWebPushUserIDCannotBeEmpty + } + _, err := c.db.Exec(deleteWebPushSubscriptionByUserIDQuery, userID) + return err +} + +// RemoveExpiredSubscriptions removes all subscriptions that have not been updated for a given time period +func (c *webPushStore) RemoveExpiredSubscriptions(expireAfter time.Duration) error { + _, err := c.db.Exec(deleteWebPushSubscriptionByAgeQuery, time.Now().Add(-expireAfter).Unix()) + return err +} + +// Close closes the underlying database connection +func (c *webPushStore) Close() error { + return c.db.Close() +} diff --git a/server/webpush_store_test.go b/server/webpush_store_test.go new file mode 100644 index 00000000..ab5bc424 --- /dev/null +++ b/server/webpush_store_test.go @@ -0,0 +1,199 @@ +package server + +import ( + "fmt" + "github.com/stretchr/testify/require" + "net/netip" + "path/filepath" + "testing" + "time" +) + +func TestWebPushStore_UpsertSubscription_SubscriptionsForTopic(t *testing.T) { + webPush := newTestWebPushStore(t) + defer webPush.Close() + + require.Nil(t, webPush.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"test-topic", "mytopic"})) + + subs, err := webPush.SubscriptionsForTopic("test-topic") + require.Nil(t, err) + require.Len(t, subs, 1) + require.Equal(t, subs[0].Endpoint, testWebPushEndpoint) + require.Equal(t, subs[0].P256dh, "p256dh-key") + require.Equal(t, subs[0].Auth, "auth-key") + require.Equal(t, subs[0].UserID, "u_1234") + + subs2, err := webPush.SubscriptionsForTopic("mytopic") + require.Nil(t, err) + require.Len(t, subs2, 1) + require.Equal(t, subs[0].Endpoint, subs2[0].Endpoint) +} + +func TestWebPushStore_UpsertSubscription_SubscriberIPLimitReached(t *testing.T) { + webPush := newTestWebPushStore(t) + defer webPush.Close() + + // Insert 10 subscriptions with the same IP address + for i := 0; i < 10; i++ { + endpoint := fmt.Sprintf(testWebPushEndpoint+"%d", i) + require.Nil(t, webPush.UpsertSubscription(endpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"test-topic", "mytopic"})) + } + + // Another one for the same endpoint should be fine + require.Nil(t, webPush.UpsertSubscription(testWebPushEndpoint+"0", "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"test-topic", "mytopic"})) + + // But with a different endpoint it should fail + require.Equal(t, errWebPushTooManySubscriptions, webPush.UpsertSubscription(testWebPushEndpoint+"11", "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"test-topic", "mytopic"})) + + // But with a different IP address it should be fine again + require.Nil(t, webPush.UpsertSubscription(testWebPushEndpoint+"99", "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("9.9.9.9"), []string{"test-topic", "mytopic"})) +} + +func TestWebPushStore_UpsertSubscription_UpdateTopics(t *testing.T) { + webPush := newTestWebPushStore(t) + defer webPush.Close() + + // Insert subscription with two topics, and another with one topic + require.Nil(t, webPush.UpsertSubscription(testWebPushEndpoint+"0", "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1", "topic2"})) + require.Nil(t, webPush.UpsertSubscription(testWebPushEndpoint+"1", "auth-key", "p256dh-key", "", netip.MustParseAddr("9.9.9.9"), []string{"topic1"})) + + subs, err := webPush.SubscriptionsForTopic("topic1") + require.Nil(t, err) + require.Len(t, subs, 2) + require.Equal(t, testWebPushEndpoint+"0", subs[0].Endpoint) + require.Equal(t, testWebPushEndpoint+"1", subs[1].Endpoint) + + subs, err = webPush.SubscriptionsForTopic("topic2") + require.Nil(t, err) + require.Len(t, subs, 1) + require.Equal(t, testWebPushEndpoint+"0", subs[0].Endpoint) + + // Update the first subscription to have only one topic + require.Nil(t, webPush.UpsertSubscription(testWebPushEndpoint+"0", "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1"})) + + subs, err = webPush.SubscriptionsForTopic("topic1") + require.Nil(t, err) + require.Len(t, subs, 2) + require.Equal(t, testWebPushEndpoint+"0", subs[0].Endpoint) + + subs, err = webPush.SubscriptionsForTopic("topic2") + require.Nil(t, err) + require.Len(t, subs, 0) +} + +func TestWebPushStore_RemoveSubscriptionsByEndpoint(t *testing.T) { + webPush := newTestWebPushStore(t) + defer webPush.Close() + + // Insert subscription with two topics + require.Nil(t, webPush.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1", "topic2"})) + subs, err := webPush.SubscriptionsForTopic("topic1") + require.Nil(t, err) + require.Len(t, subs, 1) + + // And remove it again + require.Nil(t, webPush.RemoveSubscriptionsByEndpoint(testWebPushEndpoint)) + subs, err = webPush.SubscriptionsForTopic("topic1") + require.Nil(t, err) + require.Len(t, subs, 0) +} + +func TestWebPushStore_RemoveSubscriptionsByUserID(t *testing.T) { + webPush := newTestWebPushStore(t) + defer webPush.Close() + + // Insert subscription with two topics + require.Nil(t, webPush.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1", "topic2"})) + subs, err := webPush.SubscriptionsForTopic("topic1") + require.Nil(t, err) + require.Len(t, subs, 1) + + // And remove it again + require.Nil(t, webPush.RemoveSubscriptionsByUserID("u_1234")) + subs, err = webPush.SubscriptionsForTopic("topic1") + require.Nil(t, err) + require.Len(t, subs, 0) +} + +func TestWebPushStore_RemoveSubscriptionsByUserID_Empty(t *testing.T) { + webPush := newTestWebPushStore(t) + defer webPush.Close() + require.Equal(t, errWebPushUserIDCannotBeEmpty, webPush.RemoveSubscriptionsByUserID("")) +} + +func TestWebPushStore_MarkExpiryWarningSent(t *testing.T) { + webPush := newTestWebPushStore(t) + defer webPush.Close() + + // Insert subscription with two topics + require.Nil(t, webPush.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1", "topic2"})) + subs, err := webPush.SubscriptionsForTopic("topic1") + require.Nil(t, err) + require.Len(t, subs, 1) + + // Mark them as warning sent + require.Nil(t, webPush.MarkExpiryWarningSent(subs)) + + rows, err := webPush.db.Query("SELECT endpoint FROM subscription WHERE warned_at > 0") + require.Nil(t, err) + defer rows.Close() + var endpoint string + require.True(t, rows.Next()) + require.Nil(t, rows.Scan(&endpoint)) + require.Nil(t, err) + require.Equal(t, testWebPushEndpoint, endpoint) + require.False(t, rows.Next()) +} + +func TestWebPushStore_SubscriptionsExpiring(t *testing.T) { + webPush := newTestWebPushStore(t) + defer webPush.Close() + + // Insert subscription with two topics + require.Nil(t, webPush.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1", "topic2"})) + subs, err := webPush.SubscriptionsForTopic("topic1") + require.Nil(t, err) + require.Len(t, subs, 1) + + // Fake-mark them as soon-to-expire + _, err = webPush.db.Exec("UPDATE subscription SET updated_at = ? WHERE endpoint = ?", time.Now().Add(-8*24*time.Hour).Unix(), testWebPushEndpoint) + require.Nil(t, err) + + // Should not be cleaned up yet + require.Nil(t, webPush.RemoveExpiredSubscriptions(9*24*time.Hour)) + + // Run expiration + subs, err = webPush.SubscriptionsExpiring(7 * 24 * time.Hour) + require.Nil(t, err) + require.Len(t, subs, 1) + require.Equal(t, testWebPushEndpoint, subs[0].Endpoint) +} + +func TestWebPushStore_RemoveExpiredSubscriptions(t *testing.T) { + webPush := newTestWebPushStore(t) + defer webPush.Close() + + // Insert subscription with two topics + require.Nil(t, webPush.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1", "topic2"})) + subs, err := webPush.SubscriptionsForTopic("topic1") + require.Nil(t, err) + require.Len(t, subs, 1) + + // Fake-mark them as expired + _, err = webPush.db.Exec("UPDATE subscription SET updated_at = ? WHERE endpoint = ?", time.Now().Add(-10*24*time.Hour).Unix(), testWebPushEndpoint) + require.Nil(t, err) + + // Run expiration + require.Nil(t, webPush.RemoveExpiredSubscriptions(9*24*time.Hour)) + + // List again, should be 0 + subs, err = webPush.SubscriptionsForTopic("topic1") + require.Nil(t, err) + require.Len(t, subs, 0) +} + +func newTestWebPushStore(t *testing.T) *webPushStore { + webPush, err := newWebPushStore(filepath.Join(t.TempDir(), "webpush.db"), "") + require.Nil(t, err) + return webPush +} diff --git a/user/manager.go b/user/manager.go index b2898ae8..324b7684 100644 --- a/user/manager.go +++ b/user/manager.go @@ -6,7 +6,7 @@ import ( "encoding/json" "errors" "fmt" - _ "github.com/mattn/go-sqlite3" // SQLite driver + "github.com/mattn/go-sqlite3" "github.com/stripe/stripe-go/v74" "golang.org/x/crypto/bcrypt" "heckel.io/ntfy/log" @@ -55,6 +55,7 @@ const ( messages_limit INT NOT NULL, messages_expiry_duration INT NOT NULL, emails_limit INT NOT NULL, + calls_limit INT NOT NULL, reservations_limit INT NOT NULL, attachment_file_size_limit INT NOT NULL, attachment_total_size_limit INT NOT NULL, @@ -76,6 +77,7 @@ const ( sync_topic TEXT NOT NULL, stats_messages INT NOT NULL DEFAULT (0), stats_emails INT NOT NULL DEFAULT (0), + stats_calls INT NOT NULL DEFAULT (0), stripe_customer_id TEXT, stripe_subscription_id TEXT, stripe_subscription_status TEXT, @@ -109,6 +111,12 @@ const ( PRIMARY KEY (user_id, token), FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE ); + CREATE TABLE IF NOT EXISTS user_phone ( + user_id TEXT NOT NULL, + phone_number TEXT NOT NULL, + PRIMARY KEY (user_id, phone_number), + FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE + ); CREATE TABLE IF NOT EXISTS schemaVersion ( id INT PRIMARY KEY, version INT NOT NULL @@ -118,31 +126,32 @@ const ( ON CONFLICT (id) DO NOTHING; COMMIT; ` + builtinStartupQueries = ` PRAGMA foreign_keys = ON; ` selectUserByIDQuery = ` - SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id + SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id FROM user u LEFT JOIN tier t on t.id = u.tier_id WHERE u.id = ? ` selectUserByNameQuery = ` - SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id + SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id FROM user u LEFT JOIN tier t on t.id = u.tier_id WHERE user = ? ` selectUserByTokenQuery = ` - SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id + SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id FROM user u JOIN user_token tk on u.id = tk.user_id LEFT JOIN tier t on t.id = u.tier_id WHERE tk.token = ? AND (tk.expires = 0 OR tk.expires >= ?) ` selectUserByStripeCustomerIDQuery = ` - SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id + SELECT u.id, u.user, u.pass, u.role, u.prefs, u.sync_topic, u.stats_messages, u.stats_emails, u.stats_calls, u.stripe_customer_id, u.stripe_subscription_id, u.stripe_subscription_status, u.stripe_subscription_interval, u.stripe_subscription_paid_until, u.stripe_subscription_cancel_at, deleted, t.id, t.code, t.name, t.messages_limit, t.messages_expiry_duration, t.emails_limit, t.calls_limit, t.reservations_limit, t.attachment_file_size_limit, t.attachment_total_size_limit, t.attachment_expiry_duration, t.attachment_bandwidth_limit, t.stripe_monthly_price_id, t.stripe_yearly_price_id FROM user u LEFT JOIN tier t on t.id = u.tier_id WHERE u.stripe_customer_id = ? @@ -151,7 +160,7 @@ const ( SELECT read, write FROM user_access a JOIN user u ON u.id = a.user_id - WHERE (u.user = ? OR u.user = ?) AND ? LIKE a.topic + WHERE (u.user = ? OR u.user = ?) AND ? LIKE a.topic ESCAPE '\' ORDER BY u.user DESC ` @@ -173,8 +182,8 @@ const ( updateUserPassQuery = `UPDATE user SET pass = ? WHERE user = ?` updateUserRoleQuery = `UPDATE user SET role = ? WHERE user = ?` updateUserPrefsQuery = `UPDATE user SET prefs = ? WHERE id = ?` - updateUserStatsQuery = `UPDATE user SET stats_messages = ?, stats_emails = ? WHERE id = ?` - updateUserStatsResetAllQuery = `UPDATE user SET stats_messages = 0, stats_emails = 0` + updateUserStatsQuery = `UPDATE user SET stats_messages = ?, stats_emails = ?, stats_calls = ? WHERE id = ?` + updateUserStatsResetAllQuery = `UPDATE user SET stats_messages = 0, stats_emails = 0, stats_calls = 0` updateUserDeletedQuery = `UPDATE user SET deleted = ? WHERE id = ?` deleteUsersMarkedQuery = `DELETE FROM user WHERE deleted < ?` deleteUserQuery = `DELETE FROM user WHERE user = ?` @@ -185,6 +194,11 @@ const ( ON CONFLICT (user_id, topic) DO UPDATE SET read=excluded.read, write=excluded.write, owner_user_id=excluded.owner_user_id ` + selectUserAllAccessQuery = ` + SELECT user_id, topic, read, write + FROM user_access + ORDER BY write DESC, read DESC, topic + ` selectUserAccessQuery = ` SELECT topic, read, write FROM user_access @@ -221,7 +235,7 @@ const ( selectOtherAccessCountQuery = ` SELECT COUNT(*) FROM user_access - WHERE (topic = ? OR ? LIKE topic) + WHERE (topic = ? OR ? LIKE topic ESCAPE '\') AND (owner_user_id IS NULL OR owner_user_id != (SELECT id FROM user WHERE user = ?)) ` deleteAllAccessQuery = `DELETE FROM user_access` @@ -248,7 +262,8 @@ const ( deleteExpiredTokensQuery = `DELETE FROM user_token WHERE expires > 0 AND expires < ?` deleteExcessTokensQuery = ` DELETE FROM user_token - WHERE (user_id, token) NOT IN ( + WHERE user_id = ? + AND (user_id, token) NOT IN ( SELECT user_id, token FROM user_token WHERE user_id = ? @@ -257,26 +272,30 @@ const ( ) ` + selectPhoneNumbersQuery = `SELECT phone_number FROM user_phone WHERE user_id = ?` + insertPhoneNumberQuery = `INSERT INTO user_phone (user_id, phone_number) VALUES (?, ?)` + deletePhoneNumberQuery = `DELETE FROM user_phone WHERE user_id = ? AND phone_number = ?` + insertTierQuery = ` - INSERT INTO tier (id, code, name, messages_limit, messages_expiry_duration, emails_limit, reservations_limit, attachment_file_size_limit, attachment_total_size_limit, attachment_expiry_duration, attachment_bandwidth_limit, stripe_monthly_price_id, stripe_yearly_price_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO tier (id, code, name, messages_limit, messages_expiry_duration, emails_limit, calls_limit, reservations_limit, attachment_file_size_limit, attachment_total_size_limit, attachment_expiry_duration, attachment_bandwidth_limit, stripe_monthly_price_id, stripe_yearly_price_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` updateTierQuery = ` UPDATE tier - SET name = ?, messages_limit = ?, messages_expiry_duration = ?, emails_limit = ?, reservations_limit = ?, attachment_file_size_limit = ?, attachment_total_size_limit = ?, attachment_expiry_duration = ?, attachment_bandwidth_limit = ?, stripe_monthly_price_id = ?, stripe_yearly_price_id = ? + SET name = ?, messages_limit = ?, messages_expiry_duration = ?, emails_limit = ?, calls_limit = ?, reservations_limit = ?, attachment_file_size_limit = ?, attachment_total_size_limit = ?, attachment_expiry_duration = ?, attachment_bandwidth_limit = ?, stripe_monthly_price_id = ?, stripe_yearly_price_id = ? WHERE code = ? ` selectTiersQuery = ` - SELECT id, code, name, messages_limit, messages_expiry_duration, emails_limit, reservations_limit, attachment_file_size_limit, attachment_total_size_limit, attachment_expiry_duration, attachment_bandwidth_limit, stripe_monthly_price_id, stripe_yearly_price_id + SELECT id, code, name, messages_limit, messages_expiry_duration, emails_limit, calls_limit, reservations_limit, attachment_file_size_limit, attachment_total_size_limit, attachment_expiry_duration, attachment_bandwidth_limit, stripe_monthly_price_id, stripe_yearly_price_id FROM tier ` selectTierByCodeQuery = ` - SELECT id, code, name, messages_limit, messages_expiry_duration, emails_limit, reservations_limit, attachment_file_size_limit, attachment_total_size_limit, attachment_expiry_duration, attachment_bandwidth_limit, stripe_monthly_price_id, stripe_yearly_price_id + SELECT id, code, name, messages_limit, messages_expiry_duration, emails_limit, calls_limit, reservations_limit, attachment_file_size_limit, attachment_total_size_limit, attachment_expiry_duration, attachment_bandwidth_limit, stripe_monthly_price_id, stripe_yearly_price_id FROM tier WHERE code = ? ` selectTierByPriceIDQuery = ` - SELECT id, code, name, messages_limit, messages_expiry_duration, emails_limit, reservations_limit, attachment_file_size_limit, attachment_total_size_limit, attachment_expiry_duration, attachment_bandwidth_limit, stripe_monthly_price_id, stripe_yearly_price_id + SELECT id, code, name, messages_limit, messages_expiry_duration, emails_limit, calls_limit, reservations_limit, attachment_file_size_limit, attachment_total_size_limit, attachment_expiry_duration, attachment_bandwidth_limit, stripe_monthly_price_id, stripe_yearly_price_id FROM tier WHERE (stripe_monthly_price_id = ? OR stripe_yearly_price_id = ?) ` @@ -293,7 +312,7 @@ const ( // Schema management queries const ( - currentSchemaVersion = 3 + currentSchemaVersion = 5 insertSchemaVersion = `INSERT INTO schemaVersion VALUES (1, ?)` updateSchemaVersion = `UPDATE schemaVersion SET version = ? WHERE id = 1` selectSchemaVersionQuery = `SELECT version FROM schemaVersion WHERE id = 1` @@ -391,12 +410,31 @@ const ( CREATE UNIQUE INDEX idx_tier_stripe_monthly_price_id ON tier (stripe_monthly_price_id); CREATE UNIQUE INDEX idx_tier_stripe_yearly_price_id ON tier (stripe_yearly_price_id); ` + + // 3 -> 4 + migrate3To4UpdateQueries = ` + ALTER TABLE tier ADD COLUMN calls_limit INT NOT NULL DEFAULT (0); + ALTER TABLE user ADD COLUMN stats_calls INT NOT NULL DEFAULT (0); + CREATE TABLE IF NOT EXISTS user_phone ( + user_id TEXT NOT NULL, + phone_number TEXT NOT NULL, + PRIMARY KEY (user_id, phone_number), + FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE + ); + ` + + // 4 -> 5 + migrate4To5UpdateQueries = ` + UPDATE user_access SET topic = REPLACE(topic, '_', '\_'); + ` ) var ( migrations = map[int]func(db *sql.DB) error{ 1: migrateFrom1, 2: migrateFrom2, + 3: migrateFrom3, + 4: migrateFrom4, } ) @@ -478,7 +516,7 @@ func (a *Manager) AuthenticateToken(token string) (*User, error) { // after a fixed duration unless ChangeToken is called. This function also prunes tokens for the // given user, if there are too many of them. func (a *Manager) CreateToken(userID, label string, expires time.Time, origin netip.Addr) (*Token, error) { - token := util.RandomStringPrefix(tokenPrefix, tokenLength) + token := util.RandomLowerStringPrefix(tokenPrefix, tokenLength) // Lowercase only to support "+@" email addresses tx, err := a.db.Begin() if err != nil { return nil, err @@ -503,7 +541,7 @@ func (a *Manager) CreateToken(userID, label string, expires time.Time, origin ne if tokenCount >= tokenMaxCount { // This pruning logic is done in two queries for efficiency. The SELECT above is a lookup // on two indices, whereas the query below is a full table scan. - if _, err := tx.Exec(deleteExcessTokensQuery, userID, tokenMaxCount); err != nil { + if _, err := tx.Exec(deleteExcessTokensQuery, userID, userID, tokenMaxCount); err != nil { return nil, err } } @@ -618,6 +656,56 @@ func (a *Manager) RemoveExpiredTokens() error { return nil } +// PhoneNumbers returns all phone numbers for the user with the given user ID +func (a *Manager) PhoneNumbers(userID string) ([]string, error) { + rows, err := a.db.Query(selectPhoneNumbersQuery, userID) + if err != nil { + return nil, err + } + defer rows.Close() + phoneNumbers := make([]string, 0) + for { + phoneNumber, err := a.readPhoneNumber(rows) + if err == ErrPhoneNumberNotFound { + break + } else if err != nil { + return nil, err + } + phoneNumbers = append(phoneNumbers, phoneNumber) + } + return phoneNumbers, nil +} + +func (a *Manager) readPhoneNumber(rows *sql.Rows) (string, error) { + var phoneNumber string + if !rows.Next() { + return "", ErrPhoneNumberNotFound + } + if err := rows.Scan(&phoneNumber); err != nil { + return "", err + } else if err := rows.Err(); err != nil { + return "", err + } + return phoneNumber, nil +} + +// AddPhoneNumber adds a phone number to the user with the given user ID +func (a *Manager) AddPhoneNumber(userID string, phoneNumber string) error { + if _, err := a.db.Exec(insertPhoneNumberQuery, userID, phoneNumber); err != nil { + if sqliteErr, ok := err.(sqlite3.Error); ok && sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique { + return ErrPhoneNumberExists + } + return err + } + return nil +} + +// RemovePhoneNumber deletes a phone number from the user with the given user ID +func (a *Manager) RemovePhoneNumber(userID string, phoneNumber string) error { + _, err := a.db.Exec(deletePhoneNumberQuery, userID, phoneNumber) + return err +} + // RemoveDeletedUsers deletes all users that have been marked deleted for func (a *Manager) RemoveDeletedUsers() error { if _, err := a.db.Exec(deleteUsersMarkedQuery, time.Now().Unix()); err != nil { @@ -700,9 +788,10 @@ func (a *Manager) writeUserStatsQueue() error { "user_id": userID, "messages_count": update.Messages, "emails_count": update.Emails, + "calls_count": update.Calls, }). Trace("Updating stats for user %s", userID) - if _, err := tx.Exec(updateUserStatsQuery, update.Messages, update.Emails, userID); err != nil { + if _, err := tx.Exec(updateUserStatsQuery, update.Messages, update.Emails, update.Calls, userID); err != nil { return err } } @@ -784,6 +873,9 @@ func (a *Manager) AddUser(username, password string, role Role) error { userID := util.RandomStringPrefix(userIDPrefix, userIDLength) syncTopic, now := util.RandomStringPrefix(syncTopicPrefix, syncTopicLength), time.Now().Unix() if _, err = a.db.Exec(insertUserQuery, userID, username, hash, role, syncTopic, now); err != nil { + if sqliteErr, ok := err.(sqlite3.Error); ok && sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique { + return ErrUserExists + } return err } return nil @@ -911,12 +1003,12 @@ func (a *Manager) readUser(rows *sql.Rows) (*User, error) { defer rows.Close() var id, username, hash, role, prefs, syncTopic string var stripeCustomerID, stripeSubscriptionID, stripeSubscriptionStatus, stripeSubscriptionInterval, stripeMonthlyPriceID, stripeYearlyPriceID, tierID, tierCode, tierName sql.NullString - var messages, emails int64 - var messagesLimit, messagesExpiryDuration, emailsLimit, reservationsLimit, attachmentFileSizeLimit, attachmentTotalSizeLimit, attachmentExpiryDuration, attachmentBandwidthLimit, stripeSubscriptionPaidUntil, stripeSubscriptionCancelAt, deleted sql.NullInt64 + var messages, emails, calls int64 + var messagesLimit, messagesExpiryDuration, emailsLimit, callsLimit, reservationsLimit, attachmentFileSizeLimit, attachmentTotalSizeLimit, attachmentExpiryDuration, attachmentBandwidthLimit, stripeSubscriptionPaidUntil, stripeSubscriptionCancelAt, deleted sql.NullInt64 if !rows.Next() { return nil, ErrUserNotFound } - if err := rows.Scan(&id, &username, &hash, &role, &prefs, &syncTopic, &messages, &emails, &stripeCustomerID, &stripeSubscriptionID, &stripeSubscriptionStatus, &stripeSubscriptionInterval, &stripeSubscriptionPaidUntil, &stripeSubscriptionCancelAt, &deleted, &tierID, &tierCode, &tierName, &messagesLimit, &messagesExpiryDuration, &emailsLimit, &reservationsLimit, &attachmentFileSizeLimit, &attachmentTotalSizeLimit, &attachmentExpiryDuration, &attachmentBandwidthLimit, &stripeMonthlyPriceID, &stripeYearlyPriceID); err != nil { + if err := rows.Scan(&id, &username, &hash, &role, &prefs, &syncTopic, &messages, &emails, &calls, &stripeCustomerID, &stripeSubscriptionID, &stripeSubscriptionStatus, &stripeSubscriptionInterval, &stripeSubscriptionPaidUntil, &stripeSubscriptionCancelAt, &deleted, &tierID, &tierCode, &tierName, &messagesLimit, &messagesExpiryDuration, &emailsLimit, &callsLimit, &reservationsLimit, &attachmentFileSizeLimit, &attachmentTotalSizeLimit, &attachmentExpiryDuration, &attachmentBandwidthLimit, &stripeMonthlyPriceID, &stripeYearlyPriceID); err != nil { return nil, err } else if err := rows.Err(); err != nil { return nil, err @@ -931,6 +1023,7 @@ func (a *Manager) readUser(rows *sql.Rows) (*User, error) { Stats: &Stats{ Messages: messages, Emails: emails, + Calls: calls, }, Billing: &Billing{ StripeCustomerID: stripeCustomerID.String, // May be empty @@ -954,6 +1047,7 @@ func (a *Manager) readUser(rows *sql.Rows) (*User, error) { MessageLimit: messagesLimit.Int64, MessageExpiryDuration: time.Duration(messagesExpiryDuration.Int64) * time.Second, EmailLimit: emailsLimit.Int64, + CallLimit: callsLimit.Int64, ReservationLimit: reservationsLimit.Int64, AttachmentFileSizeLimit: attachmentFileSizeLimit.Int64, AttachmentTotalSizeLimit: attachmentTotalSizeLimit.Int64, @@ -966,6 +1060,33 @@ func (a *Manager) readUser(rows *sql.Rows) (*User, error) { return user, nil } +// AllGrants returns all user-specific access control entries, mapped to their respective user IDs +func (a *Manager) AllGrants() (map[string][]Grant, error) { + rows, err := a.db.Query(selectUserAllAccessQuery) + if err != nil { + return nil, err + } + defer rows.Close() + grants := make(map[string][]Grant, 0) + for rows.Next() { + var userID, topic string + var read, write bool + if err := rows.Scan(&userID, &topic, &read, &write); err != nil { + return nil, err + } else if err := rows.Err(); err != nil { + return nil, err + } + if _, ok := grants[userID]; !ok { + grants[userID] = make([]Grant, 0) + } + grants[userID] = append(grants[userID], Grant{ + TopicPattern: fromSQLWildcard(topic), + Allow: NewPermission(read, write), + }) + } + return grants, nil +} + // Grants returns all user-specific access control entries func (a *Manager) Grants(username string) ([]Grant, error) { rows, err := a.db.Query(selectUserAccessQuery, username) @@ -1008,7 +1129,7 @@ func (a *Manager) Reservations(username string) ([]Reservation, error) { return nil, err } reservations = append(reservations, Reservation{ - Topic: topic, + Topic: unescapeUnderscore(topic), Owner: NewPermission(ownerRead, ownerWrite), Everyone: NewPermission(everyoneRead.Bool, everyoneWrite.Bool), // false if null }) @@ -1018,7 +1139,7 @@ func (a *Manager) Reservations(username string) ([]Reservation, error) { // HasReservation returns true if the given topic access is owned by the user func (a *Manager) HasReservation(username, topic string) (bool, error) { - rows, err := a.db.Query(selectUserHasReservationQuery, username, topic) + rows, err := a.db.Query(selectUserHasReservationQuery, username, escapeUnderscore(topic)) if err != nil { return false, err } @@ -1053,7 +1174,7 @@ func (a *Manager) ReservationsCount(username string) (int64, error) { // ReservationOwner returns user ID of the user that owns this topic, or an // empty string if it's not owned by anyone func (a *Manager) ReservationOwner(topic string) (string, error) { - rows, err := a.db.Query(selectUserReservationsOwnerQuery, topic) + rows, err := a.db.Query(selectUserReservationsOwnerQuery, escapeUnderscore(topic)) if err != nil { return "", err } @@ -1148,7 +1269,7 @@ func (a *Manager) AllowReservation(username string, topic string) error { if (!AllowedUsername(username) && username != Everyone) || !AllowedTopic(topic) { return ErrInvalidArgument } - rows, err := a.db.Query(selectOtherAccessCountQuery, topic, topic, username) + rows, err := a.db.Query(selectOtherAccessCountQuery, escapeUnderscore(topic), escapeUnderscore(topic), username) if err != nil { return err } @@ -1213,10 +1334,10 @@ func (a *Manager) AddReservation(username string, topic string, everyone Permiss return err } defer tx.Rollback() - if _, err := tx.Exec(upsertUserAccessQuery, username, topic, true, true, username, username); err != nil { + if _, err := tx.Exec(upsertUserAccessQuery, username, escapeUnderscore(topic), true, true, username, username); err != nil { return err } - if _, err := tx.Exec(upsertUserAccessQuery, Everyone, topic, everyone.IsRead(), everyone.IsWrite(), username, username); err != nil { + if _, err := tx.Exec(upsertUserAccessQuery, Everyone, escapeUnderscore(topic), everyone.IsRead(), everyone.IsWrite(), username, username); err != nil { return err } return tx.Commit() @@ -1239,10 +1360,10 @@ func (a *Manager) RemoveReservations(username string, topics ...string) error { } defer tx.Rollback() for _, topic := range topics { - if _, err := tx.Exec(deleteTopicAccessQuery, username, username, topic); err != nil { + if _, err := tx.Exec(deleteTopicAccessQuery, username, username, escapeUnderscore(topic)); err != nil { return err } - if _, err := tx.Exec(deleteTopicAccessQuery, Everyone, Everyone, topic); err != nil { + if _, err := tx.Exec(deleteTopicAccessQuery, Everyone, Everyone, escapeUnderscore(topic)); err != nil { return err } } @@ -1259,7 +1380,7 @@ func (a *Manager) AddTier(tier *Tier) error { if tier.ID == "" { tier.ID = util.RandomStringPrefix(tierIDPrefix, tierIDLength) } - if _, err := a.db.Exec(insertTierQuery, tier.ID, tier.Code, tier.Name, tier.MessageLimit, int64(tier.MessageExpiryDuration.Seconds()), tier.EmailLimit, tier.ReservationLimit, tier.AttachmentFileSizeLimit, tier.AttachmentTotalSizeLimit, int64(tier.AttachmentExpiryDuration.Seconds()), tier.AttachmentBandwidthLimit, nullString(tier.StripeMonthlyPriceID), nullString(tier.StripeYearlyPriceID)); err != nil { + if _, err := a.db.Exec(insertTierQuery, tier.ID, tier.Code, tier.Name, tier.MessageLimit, int64(tier.MessageExpiryDuration.Seconds()), tier.EmailLimit, tier.CallLimit, tier.ReservationLimit, tier.AttachmentFileSizeLimit, tier.AttachmentTotalSizeLimit, int64(tier.AttachmentExpiryDuration.Seconds()), tier.AttachmentBandwidthLimit, nullString(tier.StripeMonthlyPriceID), nullString(tier.StripeYearlyPriceID)); err != nil { return err } return nil @@ -1267,7 +1388,7 @@ func (a *Manager) AddTier(tier *Tier) error { // UpdateTier updates a tier's properties in the database func (a *Manager) UpdateTier(tier *Tier) error { - if _, err := a.db.Exec(updateTierQuery, tier.Name, tier.MessageLimit, int64(tier.MessageExpiryDuration.Seconds()), tier.EmailLimit, tier.ReservationLimit, tier.AttachmentFileSizeLimit, tier.AttachmentTotalSizeLimit, int64(tier.AttachmentExpiryDuration.Seconds()), tier.AttachmentBandwidthLimit, nullString(tier.StripeMonthlyPriceID), nullString(tier.StripeYearlyPriceID), tier.Code); err != nil { + if _, err := a.db.Exec(updateTierQuery, tier.Name, tier.MessageLimit, int64(tier.MessageExpiryDuration.Seconds()), tier.EmailLimit, tier.CallLimit, tier.ReservationLimit, tier.AttachmentFileSizeLimit, tier.AttachmentTotalSizeLimit, int64(tier.AttachmentExpiryDuration.Seconds()), tier.AttachmentBandwidthLimit, nullString(tier.StripeMonthlyPriceID), nullString(tier.StripeYearlyPriceID), tier.Code); err != nil { return err } return nil @@ -1336,11 +1457,11 @@ func (a *Manager) TierByStripePrice(priceID string) (*Tier, error) { func (a *Manager) readTier(rows *sql.Rows) (*Tier, error) { var id, code, name string var stripeMonthlyPriceID, stripeYearlyPriceID sql.NullString - var messagesLimit, messagesExpiryDuration, emailsLimit, reservationsLimit, attachmentFileSizeLimit, attachmentTotalSizeLimit, attachmentExpiryDuration, attachmentBandwidthLimit sql.NullInt64 + var messagesLimit, messagesExpiryDuration, emailsLimit, callsLimit, reservationsLimit, attachmentFileSizeLimit, attachmentTotalSizeLimit, attachmentExpiryDuration, attachmentBandwidthLimit sql.NullInt64 if !rows.Next() { return nil, ErrTierNotFound } - if err := rows.Scan(&id, &code, &name, &messagesLimit, &messagesExpiryDuration, &emailsLimit, &reservationsLimit, &attachmentFileSizeLimit, &attachmentTotalSizeLimit, &attachmentExpiryDuration, &attachmentBandwidthLimit, &stripeMonthlyPriceID, &stripeYearlyPriceID); err != nil { + if err := rows.Scan(&id, &code, &name, &messagesLimit, &messagesExpiryDuration, &emailsLimit, &callsLimit, &reservationsLimit, &attachmentFileSizeLimit, &attachmentTotalSizeLimit, &attachmentExpiryDuration, &attachmentBandwidthLimit, &stripeMonthlyPriceID, &stripeYearlyPriceID); err != nil { return nil, err } else if err := rows.Err(); err != nil { return nil, err @@ -1353,6 +1474,7 @@ func (a *Manager) readTier(rows *sql.Rows) (*Tier, error) { MessageLimit: messagesLimit.Int64, MessageExpiryDuration: time.Duration(messagesExpiryDuration.Int64) * time.Second, EmailLimit: emailsLimit.Int64, + CallLimit: callsLimit.Int64, ReservationLimit: reservationsLimit.Int64, AttachmentFileSizeLimit: attachmentFileSizeLimit.Int64, AttachmentTotalSizeLimit: attachmentTotalSizeLimit.Int64, @@ -1368,12 +1490,24 @@ func (a *Manager) Close() error { return a.db.Close() } +// toSQLWildcard converts a wildcard string to a SQL wildcard string. It only allows '*' as wildcards, +// and escapes '_', assuming '\' as escape character. func toSQLWildcard(s string) string { - return strings.ReplaceAll(s, "*", "%") + return escapeUnderscore(strings.ReplaceAll(s, "*", "%")) } +// fromSQLWildcard converts a SQL wildcard string to a wildcard string. It converts '%' to '*', +// and removes the '\_' escape character. func fromSQLWildcard(s string) string { - return strings.ReplaceAll(s, "%", "*") + return strings.ReplaceAll(unescapeUnderscore(s), "%", "*") +} + +func escapeUnderscore(s string) string { + return strings.ReplaceAll(s, "_", "\\_") +} + +func unescapeUnderscore(s string) string { + return strings.ReplaceAll(s, "\\_", "_") } func runStartupQueries(db *sql.DB, startupQueries string) error { @@ -1495,6 +1629,38 @@ func migrateFrom2(db *sql.DB) error { return tx.Commit() } +func migrateFrom3(db *sql.DB) error { + log.Tag(tag).Info("Migrating user database schema: from 3 to 4") + tx, err := db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + if _, err := tx.Exec(migrate3To4UpdateQueries); err != nil { + return err + } + if _, err := tx.Exec(updateSchemaVersion, 4); err != nil { + return err + } + return tx.Commit() +} + +func migrateFrom4(db *sql.DB) error { + log.Tag(tag).Info("Migrating user database schema: from 4 to 5") + tx, err := db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + if _, err := tx.Exec(migrate4To5UpdateQueries); err != nil { + return err + } + if _, err := tx.Exec(updateSchemaVersion, 5); err != nil { + return err + } + return tx.Commit() +} + func nullString(s string) sql.NullString { if s == "" { return sql.NullString{} diff --git a/user/manager_test.go b/user/manager_test.go index cd2e1032..468dc36a 100644 --- a/user/manager_test.go +++ b/user/manager_test.go @@ -183,6 +183,19 @@ func TestManager_MarkUserRemoved_RemoveDeletedUsers(t *testing.T) { require.Equal(t, ErrUserNotFound, err) } +func TestManager_CreateToken_Only_Lower(t *testing.T) { + a := newTestManager(t, PermissionDenyAll) + + // Create user, add reservations and token + require.Nil(t, a.AddUser("user", "pass", RoleAdmin)) + u, err := a.User("user") + require.Nil(t, err) + + token, err := a.CreateToken(u.ID, "", time.Now().Add(time.Hour), netip.IPv4Unspecified()) + require.Nil(t, err) + require.Equal(t, token.Value, strings.ToLower(token.Value)) +} + func TestManager_UserManagement(t *testing.T) { a := newTestManager(t, PermissionDenyAll) require.Nil(t, a.AddUser("phil", "phil", RoleAdmin)) @@ -317,7 +330,7 @@ func TestManager_Reservations(t *testing.T) { a := newTestManager(t, PermissionDenyAll) require.Nil(t, a.AddUser("phil", "phil", RoleUser)) require.Nil(t, a.AddUser("ben", "ben", RoleUser)) - require.Nil(t, a.AddReservation("ben", "ztopic", PermissionDenyAll)) + require.Nil(t, a.AddReservation("ben", "ztopic_", PermissionDenyAll)) require.Nil(t, a.AddReservation("ben", "readme", PermissionRead)) require.Nil(t, a.AllowAccess("ben", "something-else", PermissionRead)) @@ -330,7 +343,7 @@ func TestManager_Reservations(t *testing.T) { Everyone: PermissionRead, }, reservations[0]) require.Equal(t, Reservation{ - Topic: "ztopic", + Topic: "ztopic_", Owner: PermissionReadWrite, Everyone: PermissionDenyAll, }, reservations[1]) @@ -339,6 +352,14 @@ func TestManager_Reservations(t *testing.T) { require.Nil(t, err) require.True(t, b) + b, err = a.HasReservation("ben", "ztopic_") + require.Nil(t, err) + require.True(t, b) + + b, err = a.HasReservation("ben", "ztopicX") // _ != X (used to be a SQL wildcard issue) + require.Nil(t, err) + require.False(t, b) + b, err = a.HasReservation("notben", "readme") require.Nil(t, err) require.False(t, b) @@ -358,11 +379,17 @@ func TestManager_Reservations(t *testing.T) { err = a.AllowReservation("phil", "readme") require.Equal(t, errTopicOwnedByOthers, err) + err = a.AllowReservation("phil", "ztopic_") + require.Equal(t, errTopicOwnedByOthers, err) + + err = a.AllowReservation("phil", "ztopicX") + require.Nil(t, err) + err = a.AllowReservation("phil", "not-reserved") require.Nil(t, err) // Now remove them again - require.Nil(t, a.RemoveReservations("ben", "ztopic", "readme")) + require.Nil(t, a.RemoveReservations("ben", "ztopic_", "readme")) count, err = a.ReservationsCount("ben") require.Nil(t, err) @@ -567,46 +594,80 @@ func TestManager_Token_Extend(t *testing.T) { } func TestManager_Token_MaxCount_AutoDelete(t *testing.T) { + // Tests that tokens are automatically deleted when the maximum number of tokens is reached + a := newTestManager(t, PermissionDenyAll) require.Nil(t, a.AddUser("ben", "ben", RoleUser)) + require.Nil(t, a.AddUser("phil", "phil", RoleUser)) - // Try to extend token for user without token - u, err := a.User("ben") + ben, err := a.User("ben") require.Nil(t, err) - // Tokens + phil, err := a.User("phil") + require.Nil(t, err) + + // Create 2 tokens for phil + philTokens := make([]string, 0) + token, err := a.CreateToken(phil.ID, "", time.Now().Add(72*time.Hour), netip.IPv4Unspecified()) + require.Nil(t, err) + require.NotEmpty(t, token.Value) + philTokens = append(philTokens, token.Value) + + token, err = a.CreateToken(phil.ID, "", time.Unix(0, 0), netip.IPv4Unspecified()) + require.Nil(t, err) + require.NotEmpty(t, token.Value) + philTokens = append(philTokens, token.Value) + + // Create 22 tokens for ben (only 20 allowed!) baseTime := time.Now().Add(24 * time.Hour) - tokens := make([]string, 0) - for i := 0; i < 22; i++ { - token, err := a.CreateToken(u.ID, "", time.Now().Add(72*time.Hour), netip.IPv4Unspecified()) + benTokens := make([]string, 0) + for i := 0; i < 22; i++ { // + token, err := a.CreateToken(ben.ID, "", time.Now().Add(72*time.Hour), netip.IPv4Unspecified()) require.Nil(t, err) require.NotEmpty(t, token.Value) - tokens = append(tokens, token.Value) + benTokens = append(benTokens, token.Value) // Manually modify expiry date to avoid sorting issues (this is a hack) _, err = a.db.Exec(`UPDATE user_token SET expires=? WHERE token=?`, baseTime.Add(time.Duration(i)*time.Minute).Unix(), token.Value) require.Nil(t, err) } - _, err = a.AuthenticateToken(tokens[0]) + // Ben: The first 2 tokens should have been wiped and should not work anymore! + _, err = a.AuthenticateToken(benTokens[0]) require.Equal(t, ErrUnauthenticated, err) - _, err = a.AuthenticateToken(tokens[1]) + _, err = a.AuthenticateToken(benTokens[1]) require.Equal(t, ErrUnauthenticated, err) + // Ben: The other tokens should still work for i := 2; i < 22; i++ { - userWithToken, err := a.AuthenticateToken(tokens[i]) - require.Nil(t, err, "token[%d]=%s failed", i, tokens[i]) + userWithToken, err := a.AuthenticateToken(benTokens[i]) + require.Nil(t, err, "token[%d]=%s failed", i, benTokens[i]) require.Equal(t, "ben", userWithToken.Name) - require.Equal(t, tokens[i], userWithToken.Token) + require.Equal(t, benTokens[i], userWithToken.Token) } - var count int - rows, err := a.db.Query(`SELECT COUNT(*) FROM user_token`) + // Phil: All tokens should still work + for i := 0; i < 2; i++ { + userWithToken, err := a.AuthenticateToken(philTokens[i]) + require.Nil(t, err, "token[%d]=%s failed", i, philTokens[i]) + require.Equal(t, "phil", userWithToken.Name) + require.Equal(t, philTokens[i], userWithToken.Token) + } + + var benCount int + rows, err := a.db.Query(`SELECT COUNT(*) FROM user_token WHERE user_id=?`, ben.ID) require.Nil(t, err) require.True(t, rows.Next()) - require.Nil(t, rows.Scan(&count)) - require.Equal(t, 20, count) + require.Nil(t, rows.Scan(&benCount)) + require.Equal(t, 20, benCount) + + var philCount int + rows, err = a.db.Query(`SELECT COUNT(*) FROM user_token WHERE user_id=?`, phil.ID) + require.Nil(t, err) + require.True(t, rows.Next()) + require.Nil(t, rows.Scan(&philCount)) + require.Equal(t, 2, philCount) } func TestManager_EnqueueStats_ResetStats(t *testing.T) { @@ -893,7 +954,82 @@ func TestManager_Tier_Change_And_Reset(t *testing.T) { require.Nil(t, a.ResetTier("phil")) } -func TestSqliteCache_Migration_From1(t *testing.T) { +func TestUser_PhoneNumberAddListRemove(t *testing.T) { + a := newTestManager(t, PermissionDenyAll) + + require.Nil(t, a.AddUser("phil", "phil", RoleUser)) + phil, err := a.User("phil") + require.Nil(t, err) + require.Nil(t, a.AddPhoneNumber(phil.ID, "+1234567890")) + + phoneNumbers, err := a.PhoneNumbers(phil.ID) + require.Nil(t, err) + require.Equal(t, 1, len(phoneNumbers)) + require.Equal(t, "+1234567890", phoneNumbers[0]) + + require.Nil(t, a.RemovePhoneNumber(phil.ID, "+1234567890")) + phoneNumbers, err = a.PhoneNumbers(phil.ID) + require.Nil(t, err) + require.Equal(t, 0, len(phoneNumbers)) + + // Paranoia check: We do NOT want to keep phone numbers in there + rows, err := a.db.Query(`SELECT * FROM user_phone`) + require.Nil(t, err) + require.False(t, rows.Next()) + require.Nil(t, rows.Close()) +} + +func TestUser_PhoneNumberAdd_Multiple_Users_Same_Number(t *testing.T) { + a := newTestManager(t, PermissionDenyAll) + + require.Nil(t, a.AddUser("phil", "phil", RoleUser)) + require.Nil(t, a.AddUser("ben", "ben", RoleUser)) + phil, err := a.User("phil") + require.Nil(t, err) + ben, err := a.User("ben") + require.Nil(t, err) + require.Nil(t, a.AddPhoneNumber(phil.ID, "+1234567890")) + require.Nil(t, a.AddPhoneNumber(ben.ID, "+1234567890")) +} + +func TestManager_Topic_Wildcard_With_Asterisk_Underscore(t *testing.T) { + f := filepath.Join(t.TempDir(), "user.db") + a := newTestManagerFromFile(t, f, "", PermissionDenyAll, DefaultUserPasswordBcryptCost, DefaultUserStatsQueueWriterInterval) + require.Nil(t, a.AllowAccess(Everyone, "*_", PermissionRead)) + require.Nil(t, a.AllowAccess(Everyone, "__*_", PermissionRead)) + require.Nil(t, a.Authorize(nil, "allowed_", PermissionRead)) + require.Nil(t, a.Authorize(nil, "__allowed_", PermissionRead)) + require.Nil(t, a.Authorize(nil, "_allowed_", PermissionRead)) // The "%" in "%\_" matches the first "_" + require.Equal(t, ErrUnauthorized, a.Authorize(nil, "notallowed", PermissionRead)) + require.Equal(t, ErrUnauthorized, a.Authorize(nil, "_notallowed", PermissionRead)) + require.Equal(t, ErrUnauthorized, a.Authorize(nil, "__notallowed", PermissionRead)) +} + +func TestManager_Topic_Wildcard_With_Underscore(t *testing.T) { + f := filepath.Join(t.TempDir(), "user.db") + a := newTestManagerFromFile(t, f, "", PermissionDenyAll, DefaultUserPasswordBcryptCost, DefaultUserStatsQueueWriterInterval) + require.Nil(t, a.AllowAccess(Everyone, "mytopic_", PermissionReadWrite)) + require.Nil(t, a.Authorize(nil, "mytopic_", PermissionRead)) + require.Nil(t, a.Authorize(nil, "mytopic_", PermissionWrite)) + require.Equal(t, ErrUnauthorized, a.Authorize(nil, "mytopicX", PermissionRead)) + require.Equal(t, ErrUnauthorized, a.Authorize(nil, "mytopicX", PermissionWrite)) +} + +func TestToFromSQLWildcard(t *testing.T) { + require.Equal(t, "up%", toSQLWildcard("up*")) + require.Equal(t, "up\\_%", toSQLWildcard("up_*")) + require.Equal(t, "foo", toSQLWildcard("foo")) + + require.Equal(t, "up*", fromSQLWildcard("up%")) + require.Equal(t, "up_*", fromSQLWildcard("up\\_%")) + require.Equal(t, "foo", fromSQLWildcard("foo")) + + require.Equal(t, "up*", fromSQLWildcard(toSQLWildcard("up*"))) + require.Equal(t, "up_*", fromSQLWildcard(toSQLWildcard("up_*"))) + require.Equal(t, "foo", fromSQLWildcard(toSQLWildcard("foo"))) +} + +func TestMigrationFrom1(t *testing.T) { filename := filepath.Join(t.TempDir(), "user.db") db, err := sql.Open("sqlite3", filename) require.Nil(t, err) @@ -978,6 +1114,152 @@ func TestSqliteCache_Migration_From1(t *testing.T) { require.Equal(t, PermissionRead, everyoneGrants[0].Allow) } +func TestMigrationFrom4(t *testing.T) { + filename := filepath.Join(t.TempDir(), "user.db") + db, err := sql.Open("sqlite3", filename) + require.Nil(t, err) + + // Create "version 4" schema + _, err = db.Exec(` + BEGIN; + CREATE TABLE IF NOT EXISTS tier ( + id TEXT PRIMARY KEY, + code TEXT NOT NULL, + name TEXT NOT NULL, + messages_limit INT NOT NULL, + messages_expiry_duration INT NOT NULL, + emails_limit INT NOT NULL, + calls_limit INT NOT NULL, + reservations_limit INT NOT NULL, + attachment_file_size_limit INT NOT NULL, + attachment_total_size_limit INT NOT NULL, + attachment_expiry_duration INT NOT NULL, + attachment_bandwidth_limit INT NOT NULL, + stripe_monthly_price_id TEXT, + stripe_yearly_price_id TEXT + ); + CREATE UNIQUE INDEX idx_tier_code ON tier (code); + CREATE UNIQUE INDEX idx_tier_stripe_monthly_price_id ON tier (stripe_monthly_price_id); + CREATE UNIQUE INDEX idx_tier_stripe_yearly_price_id ON tier (stripe_yearly_price_id); + CREATE TABLE IF NOT EXISTS user ( + id TEXT PRIMARY KEY, + tier_id TEXT, + user TEXT NOT NULL, + pass TEXT NOT NULL, + role TEXT CHECK (role IN ('anonymous', 'admin', 'user')) NOT NULL, + prefs JSON NOT NULL DEFAULT '{}', + sync_topic TEXT NOT NULL, + stats_messages INT NOT NULL DEFAULT (0), + stats_emails INT NOT NULL DEFAULT (0), + stats_calls INT NOT NULL DEFAULT (0), + stripe_customer_id TEXT, + stripe_subscription_id TEXT, + stripe_subscription_status TEXT, + stripe_subscription_interval TEXT, + stripe_subscription_paid_until INT, + stripe_subscription_cancel_at INT, + created INT NOT NULL, + deleted INT, + FOREIGN KEY (tier_id) REFERENCES tier (id) + ); + CREATE UNIQUE INDEX idx_user ON user (user); + CREATE UNIQUE INDEX idx_user_stripe_customer_id ON user (stripe_customer_id); + CREATE UNIQUE INDEX idx_user_stripe_subscription_id ON user (stripe_subscription_id); + CREATE TABLE IF NOT EXISTS user_access ( + user_id TEXT NOT NULL, + topic TEXT NOT NULL, + read INT NOT NULL, + write INT NOT NULL, + owner_user_id INT, + PRIMARY KEY (user_id, topic), + FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE, + FOREIGN KEY (owner_user_id) REFERENCES user (id) ON DELETE CASCADE + ); + CREATE TABLE IF NOT EXISTS user_token ( + user_id TEXT NOT NULL, + token TEXT NOT NULL, + label TEXT NOT NULL, + last_access INT NOT NULL, + last_origin TEXT NOT NULL, + expires INT NOT NULL, + PRIMARY KEY (user_id, token), + FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE + ); + CREATE TABLE IF NOT EXISTS user_phone ( + user_id TEXT NOT NULL, + phone_number TEXT NOT NULL, + PRIMARY KEY (user_id, phone_number), + FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE + ); + CREATE TABLE IF NOT EXISTS schemaVersion ( + id INT PRIMARY KEY, + version INT NOT NULL + ); + INSERT INTO user (id, user, pass, role, sync_topic, created) + VALUES ('u_everyone', '*', '', 'anonymous', '', UNIXEPOCH()) + ON CONFLICT (id) DO NOTHING; + INSERT INTO schemaVersion (id, version) VALUES (1, 4); + COMMIT; + `) + require.Nil(t, err) + + // Insert a few ACL entries + _, err = db.Exec(` + BEGIN; + INSERT INTO user_access (user_id, topic, read, write) values ('u_everyone', 'mytopic_', 1, 1); + INSERT INTO user_access (user_id, topic, read, write) values ('u_everyone', 'up%', 1, 1); + INSERT INTO user_access (user_id, topic, read, write) values ('u_everyone', 'down_%', 1, 1); + COMMIT; + `) + require.Nil(t, err) + + // Create manager to trigger migration + a := newTestManagerFromFile(t, filename, "", PermissionDenyAll, bcrypt.MinCost, DefaultUserStatsQueueWriterInterval) + checkSchemaVersion(t, a.db) + + // Add another + require.Nil(t, a.AllowAccess(Everyone, "left_*", PermissionReadWrite)) + + // Check "external view" of grants + everyoneGrants, err := a.Grants(Everyone) + require.Nil(t, err) + + require.Equal(t, 4, len(everyoneGrants)) + require.Equal(t, "down_*", everyoneGrants[0].TopicPattern) + require.Equal(t, "left_*", everyoneGrants[1].TopicPattern) + require.Equal(t, "mytopic_", everyoneGrants[2].TopicPattern) + require.Equal(t, "up*", everyoneGrants[3].TopicPattern) + + // Check they are stored correctly in the database + rows, err := db.Query(`SELECT topic FROM user_access WHERE user_id = 'u_everyone' ORDER BY topic`) + require.Nil(t, err) + topicPatterns := make([]string, 0) + for rows.Next() { + var topicPattern string + require.Nil(t, rows.Scan(&topicPattern)) + topicPatterns = append(topicPatterns, topicPattern) + } + require.Nil(t, rows.Close()) + require.Equal(t, 4, len(topicPatterns)) + require.Equal(t, "down\\_%", topicPatterns[0]) + require.Equal(t, "left\\_%", topicPatterns[1]) + require.Equal(t, "mytopic\\_", topicPatterns[2]) + require.Equal(t, "up%", topicPatterns[3]) + + // Check that ACL works as excepted + require.Nil(t, a.Authorize(nil, "down_123", PermissionRead)) + require.Equal(t, ErrUnauthorized, a.Authorize(nil, "downX123", PermissionRead)) + + require.Nil(t, a.Authorize(nil, "left_abc", PermissionRead)) + require.Equal(t, ErrUnauthorized, a.Authorize(nil, "leftX123", PermissionRead)) + + require.Nil(t, a.Authorize(nil, "mytopic_", PermissionRead)) + require.Equal(t, ErrUnauthorized, a.Authorize(nil, "mytopicX", PermissionRead)) + + require.Nil(t, a.Authorize(nil, "up123", PermissionRead)) + require.Nil(t, a.Authorize(nil, "up", PermissionRead)) // % matches 0 or more characters +} + func checkSchemaVersion(t *testing.T, db *sql.DB) { rows, err := db.Query(`SELECT version FROM schemaVersion`) require.Nil(t, err) diff --git a/user/types.go b/user/types.go index 2486f110..11895785 100644 --- a/user/types.go +++ b/user/types.go @@ -86,6 +86,7 @@ type Tier struct { MessageLimit int64 // Daily message limit MessageExpiryDuration time.Duration // Cache duration for messages EmailLimit int64 // Daily email limit + CallLimit int64 // Daily phone call limit ReservationLimit int64 // Number of topic reservations allowed by user AttachmentFileSizeLimit int64 // Max file size per file (bytes) AttachmentTotalSizeLimit int64 // Total file size for all files of this user (bytes) @@ -131,6 +132,7 @@ type NotificationPrefs struct { type Stats struct { Messages int64 Emails int64 + Calls int64 } // Billing is a struct holding a user's billing information @@ -276,7 +278,10 @@ var ( ErrUnauthorized = errors.New("unauthorized") ErrInvalidArgument = errors.New("invalid argument") ErrUserNotFound = errors.New("user not found") + ErrUserExists = errors.New("user already exists") ErrTierNotFound = errors.New("tier not found") ErrTokenNotFound = errors.New("token not found") + ErrPhoneNumberNotFound = errors.New("phone number not found") ErrTooManyReservations = errors.New("new tier has lower reservation limit") + ErrPhoneNumberExists = errors.New("phone number already exists") ) diff --git a/util/util.go b/util/util.go index 33fa34ee..d48487df 100644 --- a/util/util.go +++ b/util/util.go @@ -6,7 +6,6 @@ import ( "encoding/json" "errors" "fmt" - "golang.org/x/time/rate" "io" "math/rand" "net/netip" @@ -17,12 +16,15 @@ import ( "sync" "time" + "golang.org/x/time/rate" + "github.com/gabriel-vasile/mimetype" "golang.org/x/term" ) const ( - randomStringCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + randomStringCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + randomStringLowerCaseCharset = "abcdefghijklmnopqrstuvwxyz0123456789" ) var ( @@ -67,15 +69,12 @@ func ContainsIP(haystack []netip.Prefix, needle netip.Addr) bool { // ContainsAll returns true if all needles are contained in haystack func ContainsAll[T comparable](haystack []T, needles []T) bool { - matches := 0 - for _, s := range haystack { - for _, needle := range needles { - if s == needle { - matches++ - } + for _, needle := range needles { + if !Contains(haystack, needle) { + return false } } - return matches == len(needles) + return true } // SplitNoEmpty splits a string using strings.Split, but filters out empty strings @@ -114,11 +113,20 @@ func RandomString(length int) string { // RandomStringPrefix returns a random string with a given length, with a prefix func RandomStringPrefix(prefix string, length int) string { + return randomStringPrefixWithCharset(prefix, length, randomStringCharset) +} + +// RandomLowerStringPrefix returns a random lowercase-only string with a given length, with a prefix +func RandomLowerStringPrefix(prefix string, length int) string { + return randomStringPrefixWithCharset(prefix, length, randomStringLowerCaseCharset) +} + +func randomStringPrefixWithCharset(prefix string, length int, charset string) string { randomMutex.Lock() // Who would have thought that random.Intn() is not thread-safe?! defer randomMutex.Unlock() b := make([]byte, length-len(prefix)) for i := range b { - b[i] = randomStringCharset[random.Intn(len(randomStringCharset))] + b[i] = charset[random.Intn(len(charset))] } return prefix + string(b) } @@ -153,11 +161,6 @@ func ParsePriority(priority string) (int, error) { case "5", "max", "urgent": return 5, nil default: - // Ignore new HTTP Priority header (see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-priority) - // Cloudflare adds this to requests when forwarding to the backend (ntfy), so we just ignore it. - if strings.HasPrefix(p, "u=") { - return 3, nil - } return 0, errInvalidPriority } } diff --git a/util/util_test.go b/util/util_test.go index 5717c5cc..f0f45c28 100644 --- a/util/util_test.go +++ b/util/util_test.go @@ -2,7 +2,6 @@ package util import ( "errors" - "golang.org/x/time/rate" "io" "net/netip" "os" @@ -11,6 +10,8 @@ import ( "testing" "time" + "golang.org/x/time/rate" + "github.com/stretchr/testify/require" ) @@ -49,6 +50,11 @@ func TestContains(t *testing.T) { require.False(t, Contains(s, 3)) } +func TestContainsAll(t *testing.T) { + require.True(t, ContainsAll([]int{1, 2, 3}, []int{2, 3})) + require.False(t, ContainsAll([]int{1, 1}, []int{1, 2})) +} + func TestContainsIP(t *testing.T) { require.True(t, ContainsIP([]netip.Prefix{netip.MustParsePrefix("fd00::/8"), netip.MustParsePrefix("1.1.0.0/16")}, netip.MustParseAddr("1.1.1.1"))) require.True(t, ContainsIP([]netip.Prefix{netip.MustParsePrefix("fd00::/8"), netip.MustParsePrefix("1.1.0.0/16")}, netip.MustParseAddr("fd12:1234:5678::9876"))) @@ -81,15 +87,6 @@ func TestParsePriority_Invalid(t *testing.T) { } } -func TestParsePriority_HTTPSpecPriority(t *testing.T) { - priorities := []string{"u=1", "u=3", "u=7, i"} // see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-priority - for _, priority := range priorities { - actual, err := ParsePriority(priority) - require.Nil(t, err) - require.Equal(t, 3, actual) // Always expect 3! - } -} - func TestPriorityString(t *testing.T) { priorities := []int{0, 1, 2, 3, 4, 5} expected := []string{"default", "min", "low", "default", "high", "max"} diff --git a/web/.eslintignore b/web/.eslintignore new file mode 100644 index 00000000..29c9584b --- /dev/null +++ b/web/.eslintignore @@ -0,0 +1 @@ +src/app/emojis.js \ No newline at end of file diff --git a/web/.eslintrc b/web/.eslintrc new file mode 100644 index 00000000..a21221fc --- /dev/null +++ b/web/.eslintrc @@ -0,0 +1,38 @@ +{ + "extends": ["airbnb", "prettier"], + "env": { + "browser": true + }, + "globals": { + "config": "readonly" + }, + "parserOptions": { + "ecmaVersion": 2023 + }, + "rules": { + "no-console": "off", + "class-methods-use-this": "off", + "func-style": ["error", "expression"], + "no-restricted-syntax": ["error", "ForInStatement", "LabeledStatement", "WithStatement"], + "no-await-in-loop": "error", + "import/no-cycle": "warn", + "react/prop-types": "off", + "react/destructuring-assignment": "off", + "react/jsx-no-useless-fragment": "off", + "react/jsx-props-no-spreading": "off", + "react/jsx-no-duplicate-props": [ + "error", + { + "ignoreCase": false // For 's [iI]nputProps + } + ], + "react/function-component-definition": [ + "error", + { + "namedComponents": "arrow-function", + "unnamedComponents": "arrow-function" + } + ] + }, + "overrides": [{ "files": ["./public/sw.js"], "rules": { "no-restricted-globals": "off" } }] +} diff --git a/web/.prettierignore b/web/.prettierignore new file mode 100644 index 00000000..802cdb8d --- /dev/null +++ b/web/.prettierignore @@ -0,0 +1,4 @@ +build/ +dist/ +public/static/langs/ +src/app/emojis.js diff --git a/web/index.html b/web/index.html new file mode 100644 index 00000000..462bbc1f --- /dev/null +++ b/web/index.html @@ -0,0 +1,58 @@ + + + + + ntfy web + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/web/package-lock.json b/web/package-lock.json index 3955ff78..7e3fcfdb 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -8,8 +8,9 @@ "name": "ntfy", "version": "1.0.0", "dependencies": { - "@emotion/react": "^11.8.2", - "@emotion/styled": "^11.8.1", + "@emotion/cache": "^11.11.0", + "@emotion/react": "^11.11.0", + "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.4.2", "@mui/material": "latest", "dexie": "^3.2.1", @@ -23,18 +24,43 @@ "react-dom": "latest", "react-i18next": "^11.16.2", "react-infinite-scroll-component": "^6.1.0", + "react-remark": "^2.1.0", "react-router-dom": "^6.2.2", - "react-scripts": "^5.0.0", "stacktrace-gps": "^3.0.4", - "stacktrace-js": "^2.0.2" + "stacktrace-js": "^2.0.2", + "stylis": "^4.3.0", + "stylis-plugin-rtl": "^2.1.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.0.0", + "eslint": "^8.41.0", + "eslint-config-airbnb": "^19.0.4", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", + "prettier": "^2.8.8", + "vite": "^4.3.9", + "vite-plugin-pwa": "^0.15.0" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" } }, "node_modules/@ampproject/remapping": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", - "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, "dependencies": { - "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" }, "engines": { @@ -42,44 +68,47 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dependencies": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.0.tgz", - "integrity": "sha512-gMuZsmsgxk/ENC3O/fRw5QY8A9/uxQbbCEypnLIiYYc/qVJtEV7ouxC3EllIIwNzMqAQee5tanFabWsUOutS7g==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.2.tgz", + "integrity": "sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==", + "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.3.tgz", - "integrity": "sha512-qIJONzoa/qiHghnm0l1n4i/6IIziDpzqc36FBs4pzMhDUraHqponwJLiAKm1hGLP3OSB/TVNz6rMwVGpwxxySw==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz", + "integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==", + "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.21.3", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-module-transforms": "^7.21.2", - "@babel/helpers": "^7.21.0", - "@babel/parser": "^7.21.3", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.3", - "@babel/types": "^7.21.3", - "convert-source-map": "^1.7.0", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.23.0", + "@babel/helpers": "^7.23.2", + "@babel/parser": "^7.23.0", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0", + "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "json5": "^2.2.2", - "semver": "^6.3.0" + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -89,53 +118,19 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/eslint-parser": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.21.3.tgz", - "integrity": "sha512-kfhmPimwo6k4P8zxNs8+T7yR44q1LdpsZdE1NkCsVlfiuTPRfnGgjaF8Qgug9q9Pou17u6wneYF0lDCZJATMFg==", - "dependencies": { - "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", - "eslint-visitor-keys": "^2.1.0", - "semver": "^6.3.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || >=14.0.0" - }, - "peerDependencies": { - "@babel/core": ">=7.11.0", - "eslint": "^7.5.0 || ^8.0.0" - } - }, - "node_modules/@babel/eslint-parser/node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "engines": { - "node": ">=10" - } - }, - "node_modules/@babel/eslint-parser/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "bin": { - "semver": "bin/semver.js" - } + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true }, "node_modules/@babel/generator": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.3.tgz", - "integrity": "sha512-QS3iR1GYC/YGUnW7IdggFeN5c1poPUurnGttOV/bZgPGV+izC/D8HnD6DLwod0fsatNyVn1G3EVWMYIF0nHbeA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "dev": true, "dependencies": { - "@babel/types": "^7.21.3", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -144,81 +139,61 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", - "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz", - "integrity": "sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", + "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", + "dev": true, "dependencies": { - "@babel/helper-explode-assignable-expression": "^7.18.6", - "@babel/types": "^7.18.9" + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz", - "integrity": "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "dev": true, "dependencies": { - "@babel/compat-data": "^7.20.5", - "@babel/helper-validator-option": "^7.18.6", - "browserslist": "^4.21.3", + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.15", + "browserslist": "^4.21.9", "lru-cache": "^5.1.1", - "semver": "^6.3.0" + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "bin": { - "semver": "bin/semver.js" } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.0.tgz", - "integrity": "sha512-Q8wNiMIdwsv5la5SPxNYzzkPnjgC0Sy0i7jLkVOCdllu/xcVNkr3TeZzbHBJrj+XXRqzX5uCyCoV9eu6xUG7KQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", + "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==", + "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-member-expression-to-functions": "^7.21.0", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-replace-supers": "^7.20.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/helper-split-export-declaration": "^7.18.6" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -228,12 +203,14 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.21.0.tgz", - "integrity": "sha512-N+LaFW/auRSWdx7SHD/HiARwXQju1vXTW4fKr4u5SgBUTm51OKEjKgj+cs00ggW3kEvNqwErnlwuq7Y3xBe4eg==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", + "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", + "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "regexpu-core": "^5.3.1" + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -243,139 +220,127 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", - "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.3.tgz", + "integrity": "sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==", + "dev": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.17.7", - "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2", - "semver": "^6.1.2" + "resolve": "^1.14.2" }, "peerDependencies": { - "@babel/core": "^7.4.0-0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "bin": { - "semver": "bin/semver.js" + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-explode-assignable-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz", - "integrity": "sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==", - "dependencies": { - "@babel/types": "^7.18.6" - }, + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", - "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, "dependencies": { - "@babel/template": "^7.20.7", - "@babel/types": "^7.21.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz", - "integrity": "sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", + "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", + "dev": true, "dependencies": { - "@babel/types": "^7.21.0" + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", - "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.21.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz", - "integrity": "sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz", + "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==", + "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.20.2", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.19.1", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.2", - "@babel/types": "^7.21.2" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", - "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", - "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", - "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", + "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", + "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-wrap-function": "^7.18.9", - "@babel/types": "^7.18.9" + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-wrap-function": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -385,112 +350,118 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz", - "integrity": "sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", + "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", + "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-member-expression-to-functions": "^7.20.7", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.20.7", - "@babel/types": "^7.20.7" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-optimise-call-expression": "^7.22.5" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-simple-access": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", - "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, "dependencies": { - "@babel/types": "^7.20.2" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", - "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "dev": true, "dependencies": { - "@babel/types": "^7.20.0" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz", - "integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", + "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz", - "integrity": "sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", + "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", + "dev": true, "dependencies": { - "@babel/helper-function-name": "^7.19.0", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.5", - "@babel/types": "^7.20.5" + "@babel/helper-function-name": "^7.22.5", + "@babel/template": "^7.22.15", + "@babel/types": "^7.22.19" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.0.tgz", - "integrity": "sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", + "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", + "dev": true, "dependencies": { - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.0", - "@babel/types": "^7.21.0" + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -498,9 +469,10 @@ } }, "node_modules/@babel/parser": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.3.tgz", - "integrity": "sha512-lobG0d7aOfQRXh8AyklEAgZGvA4FShxo6xQbUrrT/cNBPUdIDojlokwJsQyCC/eKia7ifqM0yP+2DRZ4WKw2RQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "dev": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -509,11 +481,12 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", - "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz", + "integrity": "sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==", + "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -523,13 +496,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz", - "integrity": "sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz", + "integrity": "sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==", + "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/plugin-proposal-optional-chaining": "^7.20.7" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.15" }, "engines": { "node": ">=6.9.0" @@ -538,236 +512,11 @@ "@babel/core": "^7.13.0" } }, - "node_modules/@babel/plugin-proposal-async-generator-functions": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", - "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", - "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-remap-async-to-generator": "^7.18.9", - "@babel/plugin-syntax-async-generators": "^7.8.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-class-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", - "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-class-static-block": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz", - "integrity": "sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.21.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-proposal-decorators": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.21.0.tgz", - "integrity": "sha512-MfgX49uRrFUTL/HvWtmx3zmpyzMMr4MTj3d527MLlr/4RTT9G/ytFFP7qet2uM2Ve03b+BkpWUpK+lRXnQ+v9w==", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.21.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-replace-supers": "^7.20.7", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/plugin-syntax-decorators": "^7.21.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-dynamic-import": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", - "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-export-namespace-from": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", - "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-json-strings": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", - "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-json-strings": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-logical-assignment-operators": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", - "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", - "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-numeric-separator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", - "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-object-rest-spread": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", - "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", - "dependencies": { - "@babel/compat-data": "^7.20.5", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.20.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-catch-binding": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", - "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-chaining": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", - "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-methods": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", - "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0.tgz", - "integrity": "sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-create-class-features-plugin": "^7.21.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - }, + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, "engines": { "node": ">=6.9.0" }, @@ -775,36 +524,11 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-proposal-unicode-property-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", - "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-async-generators": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -816,6 +540,7 @@ "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" }, @@ -827,6 +552,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -837,24 +563,11 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-decorators": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.21.0.tgz", - "integrity": "sha512-tIoPpGBR8UuM4++ccWN3gifhVvQu7ZizuR1fklhRJrd5ewgbkUS+0KVFeWWxELtn18NTLoW32XV7zyOgIAiz+w==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-dynamic-import": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -866,6 +579,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.3" }, @@ -873,12 +587,13 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-flow": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.18.6.tgz", - "integrity": "sha512-LUbR+KNTBWCUAqRG9ex5Gnzu2IOkt8jRJbHHXFT9q+L9zm7M/QQbEqXyw1n1pohYvOyWC8CjeyjrSaIwiYjK7A==", + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", + "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", + "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -887,12 +602,13 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", - "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==", + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", + "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", + "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.19.0" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -905,6 +621,7 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -916,6 +633,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -923,24 +641,11 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", - "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-logical-assignment-operators": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -952,6 +657,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -963,6 +669,7 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -974,6 +681,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -985,6 +693,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -996,6 +705,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -1007,6 +717,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -1021,6 +732,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -1031,12 +743,29 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz", - "integrity": "sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==", + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.19.0" + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", + "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1045,12 +774,16 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz", - "integrity": "sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ==", + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.2.tgz", + "integrity": "sha512-BBYVGxbDVHfoeXbOwcagAkOQAm9NxoTdMGfTqghu1GrvadSaw6iW3Je6IcL5PNOw8VwjxqBECXy50/iCQSY/lQ==", + "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.20", + "@babel/plugin-syntax-async-generators": "^7.8.4" }, "engines": { "node": ">=6.9.0" @@ -1060,13 +793,14 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz", - "integrity": "sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", + "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", + "dev": true, "dependencies": { - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-remap-async-to-generator": "^7.18.9" + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1076,11 +810,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", - "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", + "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", + "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1090,11 +825,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.21.0.tgz", - "integrity": "sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.0.tgz", + "integrity": "sha512-cOsrbmIOXmf+5YbL99/S49Y3j46k/T16b9ml8bm9lP6N9US5iQ2yBK7gpui1pg0V/WMcXdkfKbTb7HXq9u+v4g==", + "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1103,19 +839,53 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.21.0.tgz", - "integrity": "sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ==", + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", + "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", + "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-replace-supers": "^7.20.7", - "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz", + "integrity": "sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz", + "integrity": "sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-split-export-declaration": "^7.22.6", "globals": "^11.1.0" }, "engines": { @@ -1126,12 +896,13 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz", - "integrity": "sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", + "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", + "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/template": "^7.20.7" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/template": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1141,11 +912,12 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.21.3.tgz", - "integrity": "sha512-bp6hwMFzuiE4HqYEyoGJ/V2LeIWn+hLVKc4pnj++E5XQptwhtcGmSayM029d/j2X1bPKGTlsyPwAubuU22KhMA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.0.tgz", + "integrity": "sha512-vaMdgNXFkYrB+8lbgniSYWHsgqK5gjaMNcc84bMIOMRLH0L9AqYq3hwMdvnyqj1OPqea8UtjPEuS/DCenah1wg==", + "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1155,12 +927,13 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", - "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", + "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", + "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1170,11 +943,28 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", - "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", + "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", + "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz", + "integrity": "sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" }, "engines": { "node": ">=6.9.0" @@ -1184,12 +974,13 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", - "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", + "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", + "dev": true, "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1198,13 +989,14 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-flow-strip-types": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.21.0.tgz", - "integrity": "sha512-FlFA2Mj87a6sDkW4gfGrQQqwY/dLlBAyJa2dJEZ+FHXUVHBflO2wyKvg+OOEzXfrKYIa4HWl0mgmbCzt0cMb7w==", + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz", + "integrity": "sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==", + "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-flow": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" }, "engines": { "node": ">=6.9.0" @@ -1214,11 +1006,12 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.0.tgz", - "integrity": "sha512-LlUYlydgDkKpIY7mcBWvyPPmMcOphEyYA27Ef4xpbh1IiDNLr0kZsos2nf92vz3IccvJI25QUwp86Eo5s6HmBQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz", + "integrity": "sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==", + "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1228,13 +1021,30 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", - "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", + "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", + "dev": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.18.9", - "@babel/helper-function-name": "^7.18.9", - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz", + "integrity": "sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" }, "engines": { "node": ">=6.9.0" @@ -1244,11 +1054,28 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", - "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", + "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", + "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz", + "integrity": "sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" }, "engines": { "node": ">=6.9.0" @@ -1258,11 +1085,12 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", - "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", + "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", + "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1272,12 +1100,13 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.20.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz", - "integrity": "sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.0.tgz", + "integrity": "sha512-xWT5gefv2HGSm4QHtgc1sYPbseOyf+FFDo2JbpE25GWl5BqTGO9IMwTYJRoIdjsF85GE+VegHxSCUt5EvoYTAw==", + "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.20.11", - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-module-transforms": "^7.23.0", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1287,13 +1116,14 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.21.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.2.tgz", - "integrity": "sha512-Cln+Yy04Gxua7iPdj6nOV96smLGjpElir5YwzF0LBPKoPlLDNJePNlrGGaybAJkd0zKRnOVXOgizSqPYMNYkzA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.0.tgz", + "integrity": "sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ==", + "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.21.2", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-simple-access": "^7.20.2" + "@babel/helper-module-transforms": "^7.23.0", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1303,14 +1133,15 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.20.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz", - "integrity": "sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.0.tgz", + "integrity": "sha512-qBej6ctXZD2f+DhlOC9yO47yEYgUh5CZNz/aBoH4j/3NOlRfJXJbY7xDQCqQVf9KbrqGzIWER1f23doHGrIHFg==", + "dev": true, "dependencies": { - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-module-transforms": "^7.20.11", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-validator-identifier": "^7.19.1" + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.0", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -1320,12 +1151,13 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", - "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", + "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", + "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1335,12 +1167,13 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz", - "integrity": "sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.20.5", - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1350,11 +1183,63 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", - "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", + "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", + "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz", + "integrity": "sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz", + "integrity": "sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz", + "integrity": "sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.22.15" }, "engines": { "node": ">=6.9.0" @@ -1364,12 +1249,46 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", - "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", + "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", + "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-replace-supers": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz", + "integrity": "sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.0.tgz", + "integrity": "sha512-sBBGXbLJjxTzLBF5rFWaikMnOGOk/BmK6vVByIdEggZ7Vn6CvWXZyRkkLFK6WE0IF8jSliyOkUN6SScFgzCM0g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, "engines": { "node": ">=6.9.0" @@ -1379,11 +1298,46 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.21.3.tgz", - "integrity": "sha512-Wxc+TvppQG9xWFYatvCGPvZ6+SIUxQ2ZdiBP+PHYMIjnPXD+uThCshaz4NZOnODAtBjjcVQQ/3OKs9LW28purQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz", + "integrity": "sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==", + "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", + "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz", + "integrity": "sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, "engines": { "node": ">=6.9.0" @@ -1393,11 +1347,12 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", - "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", + "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", + "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1406,12 +1361,13 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-react-constant-elements": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.21.3.tgz", - "integrity": "sha512-4DVcFeWe/yDYBLp0kBmOGFJ6N2UYg7coGid1gdxb4co62dy/xISDMaYBXBVXEDhfgMk7qkbcYiGtwd5Q/hwDDQ==", + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.22.5.tgz", + "integrity": "sha512-nTh2ogNUtxbiSbxaT4Ds6aXnXEipHweN9YRgOX/oNXdf0cCrGn/+2LozFa3lnPV5D90MkjhgckCPBrsoSc1a7g==", + "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1420,59 +1376,13 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.18.6.tgz", - "integrity": "sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA==", + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.22.5.tgz", + "integrity": "sha512-yIiRO6yobeEIaI0RTbIr8iAK9FcBHLtZq0S89ZPjDLQXBA4xvghaKqI0etp/tF3htTM0sazJKKLz9oEiGRtu7w==", + "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.21.0.tgz", - "integrity": "sha512-6OAWljMvQrZjR2DaNhVfRz6dkCAVV+ymcLUmaf8bccGOHn2v5rHJK3tTpij0BuhdYWP4LLaqj5lwcdlpAAPuvg==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-jsx": "^7.18.6", - "@babel/types": "^7.21.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-development": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.18.6.tgz", - "integrity": "sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==", - "dependencies": { - "@babel/plugin-transform-react-jsx": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-pure-annotations": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.18.6.tgz", - "integrity": "sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1482,12 +1392,13 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz", - "integrity": "sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ==", + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz", + "integrity": "sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==", + "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "regenerator-transform": "^0.15.1" + "@babel/helper-plugin-utils": "^7.22.5", + "regenerator-transform": "^0.15.2" }, "engines": { "node": ">=6.9.0" @@ -1497,11 +1408,12 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", - "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", + "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", + "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1510,39 +1422,13 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-runtime": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.21.0.tgz", - "integrity": "sha512-ReY6pxwSzEU0b3r2/T/VhqMKg/AkceBT19X0UptA3/tYi5Pe2eXgEUH+NNMC5nok6c6XQz5tyVTUpuezRfSMSg==", - "dependencies": { - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.20.2", - "babel-plugin-polyfill-corejs2": "^0.3.3", - "babel-plugin-polyfill-corejs3": "^0.6.0", - "babel-plugin-polyfill-regenerator": "^0.4.1", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", - "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", + "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", + "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1552,12 +1438,13 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz", - "integrity": "sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", + "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", + "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1567,11 +1454,12 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", - "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", + "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", + "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1581,11 +1469,12 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", - "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", + "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", + "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1595,28 +1484,12 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", - "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", + "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", + "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typescript": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.21.3.tgz", - "integrity": "sha512-RQxPz6Iqt8T0uw/WsJNReuBpWpBqs/n7mNo18sKLoTbMp+UrEekhH+pKSVC7gWz+DNjo9gryfV8YzCiT45RgMw==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-create-class-features-plugin": "^7.21.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-typescript": "^7.20.0" + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1626,11 +1499,28 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", - "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz", + "integrity": "sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==", + "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", + "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1640,12 +1530,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", - "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", + "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", + "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1654,38 +1545,43 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/preset-env": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.20.2.tgz", - "integrity": "sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==", + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", + "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", + "dev": true, "dependencies": { - "@babel/compat-data": "^7.20.1", - "@babel/helper-compilation-targets": "^7.20.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-validator-option": "^7.18.6", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", - "@babel/plugin-proposal-async-generator-functions": "^7.20.1", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-class-static-block": "^7.18.6", - "@babel/plugin-proposal-dynamic-import": "^7.18.6", - "@babel/plugin-proposal-export-namespace-from": "^7.18.9", - "@babel/plugin-proposal-json-strings": "^7.18.6", - "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", - "@babel/plugin-proposal-numeric-separator": "^7.18.6", - "@babel/plugin-proposal-object-rest-spread": "^7.20.2", - "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", - "@babel/plugin-proposal-optional-chaining": "^7.18.9", - "@babel/plugin-proposal-private-methods": "^7.18.6", - "@babel/plugin-proposal-private-property-in-object": "^7.18.6", - "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.2.tgz", + "integrity": "sha512-BW3gsuDD+rvHL2VO2SjAUNTBe5YrjsTiDyqamPDWY723na3/yPQ65X5oQkFVJZ0o50/2d+svm1rkPoJeR1KxVQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.23.2", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.15", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.15", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.15", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.20.0", + "@babel/plugin-syntax-import-assertions": "^7.22.5", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", @@ -1695,45 +1591,62 @@ "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-transform-arrow-functions": "^7.18.6", - "@babel/plugin-transform-async-to-generator": "^7.18.6", - "@babel/plugin-transform-block-scoped-functions": "^7.18.6", - "@babel/plugin-transform-block-scoping": "^7.20.2", - "@babel/plugin-transform-classes": "^7.20.2", - "@babel/plugin-transform-computed-properties": "^7.18.9", - "@babel/plugin-transform-destructuring": "^7.20.2", - "@babel/plugin-transform-dotall-regex": "^7.18.6", - "@babel/plugin-transform-duplicate-keys": "^7.18.9", - "@babel/plugin-transform-exponentiation-operator": "^7.18.6", - "@babel/plugin-transform-for-of": "^7.18.8", - "@babel/plugin-transform-function-name": "^7.18.9", - "@babel/plugin-transform-literals": "^7.18.9", - "@babel/plugin-transform-member-expression-literals": "^7.18.6", - "@babel/plugin-transform-modules-amd": "^7.19.6", - "@babel/plugin-transform-modules-commonjs": "^7.19.6", - "@babel/plugin-transform-modules-systemjs": "^7.19.6", - "@babel/plugin-transform-modules-umd": "^7.18.6", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.19.1", - "@babel/plugin-transform-new-target": "^7.18.6", - "@babel/plugin-transform-object-super": "^7.18.6", - "@babel/plugin-transform-parameters": "^7.20.1", - "@babel/plugin-transform-property-literals": "^7.18.6", - "@babel/plugin-transform-regenerator": "^7.18.6", - "@babel/plugin-transform-reserved-words": "^7.18.6", - "@babel/plugin-transform-shorthand-properties": "^7.18.6", - "@babel/plugin-transform-spread": "^7.19.0", - "@babel/plugin-transform-sticky-regex": "^7.18.6", - "@babel/plugin-transform-template-literals": "^7.18.9", - "@babel/plugin-transform-typeof-symbol": "^7.18.9", - "@babel/plugin-transform-unicode-escapes": "^7.18.10", - "@babel/plugin-transform-unicode-regex": "^7.18.6", - "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.20.2", - "babel-plugin-polyfill-corejs2": "^0.3.3", - "babel-plugin-polyfill-corejs3": "^0.6.0", - "babel-plugin-polyfill-regenerator": "^0.4.1", - "core-js-compat": "^3.25.1", - "semver": "^6.3.0" + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.22.5", + "@babel/plugin-transform-async-generator-functions": "^7.23.2", + "@babel/plugin-transform-async-to-generator": "^7.22.5", + "@babel/plugin-transform-block-scoped-functions": "^7.22.5", + "@babel/plugin-transform-block-scoping": "^7.23.0", + "@babel/plugin-transform-class-properties": "^7.22.5", + "@babel/plugin-transform-class-static-block": "^7.22.11", + "@babel/plugin-transform-classes": "^7.22.15", + "@babel/plugin-transform-computed-properties": "^7.22.5", + "@babel/plugin-transform-destructuring": "^7.23.0", + "@babel/plugin-transform-dotall-regex": "^7.22.5", + "@babel/plugin-transform-duplicate-keys": "^7.22.5", + "@babel/plugin-transform-dynamic-import": "^7.22.11", + "@babel/plugin-transform-exponentiation-operator": "^7.22.5", + "@babel/plugin-transform-export-namespace-from": "^7.22.11", + "@babel/plugin-transform-for-of": "^7.22.15", + "@babel/plugin-transform-function-name": "^7.22.5", + "@babel/plugin-transform-json-strings": "^7.22.11", + "@babel/plugin-transform-literals": "^7.22.5", + "@babel/plugin-transform-logical-assignment-operators": "^7.22.11", + "@babel/plugin-transform-member-expression-literals": "^7.22.5", + "@babel/plugin-transform-modules-amd": "^7.23.0", + "@babel/plugin-transform-modules-commonjs": "^7.23.0", + "@babel/plugin-transform-modules-systemjs": "^7.23.0", + "@babel/plugin-transform-modules-umd": "^7.22.5", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.22.5", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11", + "@babel/plugin-transform-numeric-separator": "^7.22.11", + "@babel/plugin-transform-object-rest-spread": "^7.22.15", + "@babel/plugin-transform-object-super": "^7.22.5", + "@babel/plugin-transform-optional-catch-binding": "^7.22.11", + "@babel/plugin-transform-optional-chaining": "^7.23.0", + "@babel/plugin-transform-parameters": "^7.22.15", + "@babel/plugin-transform-private-methods": "^7.22.5", + "@babel/plugin-transform-private-property-in-object": "^7.22.11", + "@babel/plugin-transform-property-literals": "^7.22.5", + "@babel/plugin-transform-regenerator": "^7.22.10", + "@babel/plugin-transform-reserved-words": "^7.22.5", + "@babel/plugin-transform-shorthand-properties": "^7.22.5", + "@babel/plugin-transform-spread": "^7.22.5", + "@babel/plugin-transform-sticky-regex": "^7.22.5", + "@babel/plugin-transform-template-literals": "^7.22.5", + "@babel/plugin-transform-typeof-symbol": "^7.22.5", + "@babel/plugin-transform-unicode-escapes": "^7.22.10", + "@babel/plugin-transform-unicode-property-regex": "^7.22.5", + "@babel/plugin-transform-unicode-regex": "^7.22.5", + "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "@babel/types": "^7.23.0", + "babel-plugin-polyfill-corejs2": "^0.4.6", + "babel-plugin-polyfill-corejs3": "^0.8.5", + "babel-plugin-polyfill-regenerator": "^0.5.3", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -1742,106 +1655,65 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/preset-env/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/preset-modules": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", - "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", "@babel/types": "^7.4.4", "esutils": "^2.0.2" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-react": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.18.6.tgz", - "integrity": "sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-validator-option": "^7.18.6", - "@babel/plugin-transform-react-display-name": "^7.18.6", - "@babel/plugin-transform-react-jsx": "^7.18.6", - "@babel/plugin-transform-react-jsx-development": "^7.18.6", - "@babel/plugin-transform-react-pure-annotations": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-typescript": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.21.0.tgz", - "integrity": "sha512-myc9mpoVA5m1rF8K8DgLEatOYFDpwC+RkMkjZ0Du6uI62YvDe8uxIEYVs/VCdSJ097nlALiU/yBC7//3nI+hNg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-validator-option": "^7.21.0", - "@babel/plugin-transform-typescript": "^7.21.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, "node_modules/@babel/regjsgen": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true }, "node_modules/@babel/runtime": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", - "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", + "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", "dependencies": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", - "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.3.tgz", - "integrity": "sha512-XLyopNeaTancVitYZe2MlUEvgKb6YVVPXzofHgqHijCImG33b/uTurMS488ht/Hbsb2XK3U2BnSTxKVNGV3nGQ==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.21.3", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.21.3", - "@babel/types": "^7.21.3", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -1850,353 +1722,88 @@ } }, "node_modules/@babel/types": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.3.tgz", - "integrity": "sha512-sBGdETxC+/M4o/zKC0sl6sjWv62WFR/uzxrJ6uYyMLZOUlPnwzw0tKgVHOXxaAd5l2g8pEDM5RZ495GPQI77kg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dependencies": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" - }, - "node_modules/@csstools/normalize.css": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.0.0.tgz", - "integrity": "sha512-M0qqxAcwCsIVfpFQSlGN5XjXWu8l5JDZN+fPt1LeW5SZexQTgnaEvgXAY+CeygRw0EeppWHi12JxESWiWrB0Sg==" - }, - "node_modules/@csstools/postcss-cascade-layers": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.1.tgz", - "integrity": "sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA==", - "dependencies": { - "@csstools/selector-specificity": "^2.0.2", - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-color-function": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-1.1.1.tgz", - "integrity": "sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw==", - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-font-format-keywords": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.1.tgz", - "integrity": "sha512-ZgrlzuUAjXIOc2JueK0X5sZDjCtgimVp/O5CEqTcs5ShWBa6smhWYbS0x5cVc/+rycTDbjjzoP0KTDnUneZGOg==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-hwb-function": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.2.tgz", - "integrity": "sha512-YHdEru4o3Rsbjmu6vHy4UKOXZD+Rn2zmkAmLRfPet6+Jz4Ojw8cbWxe1n42VaXQhD3CQUXXTooIy8OkVbUcL+w==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-ic-unit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.1.tgz", - "integrity": "sha512-Ot1rcwRAaRHNKC9tAqoqNZhjdYBzKk1POgWfhN4uCOE47ebGcLRqXjKkApVDpjifL6u2/55ekkpnFcp+s/OZUw==", - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-is-pseudo-class": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.7.tgz", - "integrity": "sha512-7JPeVVZHd+jxYdULl87lvjgvWldYu+Bc62s9vD/ED6/QTGjy0jy0US/f6BG53sVMTBJ1lzKZFpYmofBN9eaRiA==", - "dependencies": { - "@csstools/selector-specificity": "^2.0.0", - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-nested-calc": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-1.0.0.tgz", - "integrity": "sha512-JCsQsw1wjYwv1bJmgjKSoZNvf7R6+wuHDAbi5f/7MbFhl2d/+v+TvBTU4BJH3G1X1H87dHl0mh6TfYogbT/dJQ==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-normalize-display-values": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.1.tgz", - "integrity": "sha512-jcOanIbv55OFKQ3sYeFD/T0Ti7AMXc9nM1hZWu8m/2722gOTxFg7xYu4RDLJLeZmPUVQlGzo4jhzvTUq3x4ZUw==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-oklab-function": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.1.1.tgz", - "integrity": "sha512-nJpJgsdA3dA9y5pgyb/UfEzE7W5Ka7u0CX0/HIMVBNWzWemdcTH3XwANECU6anWv/ao4vVNLTMxhiPNZsTK6iA==", - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-progressive-custom-properties": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz", - "integrity": "sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.3" - } - }, - "node_modules/@csstools/postcss-stepped-value-functions": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-1.0.1.tgz", - "integrity": "sha512-dz0LNoo3ijpTOQqEJLY8nyaapl6umbmDcgj4AD0lgVQ572b2eqA1iGZYTTWhrcrHztWDDRAX2DGYyw2VBjvCvQ==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-text-decoration-shorthand": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-1.0.0.tgz", - "integrity": "sha512-c1XwKJ2eMIWrzQenN0XbcfzckOLLJiczqy+YvfGmzoVXd7pT9FfObiSEfzs84bpE/VqfpEuAZ9tCRbZkZxxbdw==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-trigonometric-functions": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-1.0.2.tgz", - "integrity": "sha512-woKaLO///4bb+zZC2s80l+7cm07M7268MsyG3M0ActXXEFi6SuhvriQYcb58iiKGbjwwIU7n45iRLEHypB47Og==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-unset-value": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.2.tgz", - "integrity": "sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==", - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/selector-specificity": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz", - "integrity": "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss-selector-parser": "^6.0.10" - } - }, "node_modules/@emotion/babel-plugin": { - "version": "11.10.6", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.6.tgz", - "integrity": "sha512-p2dAqtVrkhSa7xz1u/m9eHYdLi+en8NowrmXeF/dKtJpU8lCWli8RUAati7NcSl0afsBott48pdnANuD0wh9QQ==", + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.0", - "@emotion/memoize": "^0.8.0", - "@emotion/serialize": "^1.1.1", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", "source-map": "^0.5.7", - "stylis": "4.1.3" + "stylis": "4.2.0" } }, + "node_modules/@emotion/babel-plugin/node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, "node_modules/@emotion/cache": { - "version": "11.10.5", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.5.tgz", - "integrity": "sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA==", + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", "dependencies": { - "@emotion/memoize": "^0.8.0", - "@emotion/sheet": "^1.2.1", - "@emotion/utils": "^1.2.0", - "@emotion/weak-memoize": "^0.3.0", - "stylis": "4.1.3" + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" } }, + "node_modules/@emotion/cache/node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, "node_modules/@emotion/hash": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz", - "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==" + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" }, "node_modules/@emotion/is-prop-valid": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz", - "integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", + "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", "dependencies": { - "@emotion/memoize": "^0.8.0" + "@emotion/memoize": "^0.8.1" } }, "node_modules/@emotion/memoize": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", - "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" }, "node_modules/@emotion/react": { - "version": "11.10.6", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.10.6.tgz", - "integrity": "sha512-6HT8jBmcSkfzO7mc+N1L9uwvOnlcGoix8Zn7srt+9ga0MjREo6lRpuVX0kzo6Jp6oTqDhREOFsygN6Ew4fEQbw==", + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.1.tgz", + "integrity": "sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==", "dependencies": { "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.10.6", - "@emotion/cache": "^11.10.5", - "@emotion/serialize": "^1.1.1", - "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", - "@emotion/utils": "^1.2.0", - "@emotion/weak-memoize": "^0.3.0", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.2", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", "hoist-non-react-statics": "^3.3.1" }, "peerDependencies": { @@ -2209,33 +1816,33 @@ } }, "node_modules/@emotion/serialize": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.1.tgz", - "integrity": "sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.2.tgz", + "integrity": "sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==", "dependencies": { - "@emotion/hash": "^0.9.0", - "@emotion/memoize": "^0.8.0", - "@emotion/unitless": "^0.8.0", - "@emotion/utils": "^1.2.0", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", "csstype": "^3.0.2" } }, "node_modules/@emotion/sheet": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.1.tgz", - "integrity": "sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA==" + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" }, "node_modules/@emotion/styled": { - "version": "11.10.6", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.10.6.tgz", - "integrity": "sha512-OXtBzOmDSJo5Q0AFemHCfl+bUueT8BIcPSxu0EGTpGk6DmI5dnhSzQANm1e1ze0YZL7TDyAyy6s/b/zmGOS3Og==", + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.0.tgz", + "integrity": "sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==", "dependencies": { "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.10.6", - "@emotion/is-prop-valid": "^1.2.0", - "@emotion/serialize": "^1.1.1", - "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", - "@emotion/utils": "^1.2.0" + "@emotion/babel-plugin": "^11.11.0", + "@emotion/is-prop-valid": "^1.2.1", + "@emotion/serialize": "^1.1.2", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1" }, "peerDependencies": { "@emotion/react": "^11.0.0-rc.0", @@ -2248,32 +1855,385 @@ } }, "node_modules/@emotion/unitless": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", - "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz", - "integrity": "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", "peerDependencies": { "react": ">=16.8.0" } }, "node_modules/@emotion/utils": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz", - "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" }, "node_modules/@emotion/weak-memoize": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz", - "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==" + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" + }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, "dependencies": { "eslint-visitor-keys": "^3.3.0" }, @@ -2285,21 +2245,23 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.0.tgz", - "integrity": "sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/eslintrc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz", - "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", + "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", + "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.5.1", + "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -2314,15 +2276,11 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "dev": true, "dependencies": { "type-fest": "^0.20.2" }, @@ -2333,21 +2291,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/@eslint/eslintrc/node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, "engines": { "node": ">=10" }, @@ -2356,19 +2304,55 @@ } }, "node_modules/@eslint/js": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.37.0.tgz", - "integrity": "sha512-x5vzdtOOGgFVDCUs81QRB2+liax8rFg3+7hqM+QhBG0/G3F1ZsoYl97UrqgHgQ9KKT7G6c4V+aTUCgu/n22v1A==", + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz", + "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==", + "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", - "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "node_modules/@floating-ui/core": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz", + "integrity": "sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==", "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", + "@floating-ui/utils": "^0.1.3" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz", + "integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==", + "dependencies": { + "@floating-ui/core": "^1.4.2", + "@floating-ui/utils": "^0.1.3" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.2.tgz", + "integrity": "sha512-5qhlDvjaLmAst/rKb3VdlCinwTF4EYMiVxuuc/HVUjs46W0zgtbMmAZ1UTsDrRTxRmUEzl92mOtWbeeXL26lSQ==", + "dependencies": { + "@floating-ui/dom": "^1.5.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", + "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==" + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", "minimatch": "^3.0.5" }, @@ -2380,6 +2364,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, "engines": { "node": ">=12.22" }, @@ -2389,722 +2374,16 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==" - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz", - "integrity": "sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg==", - "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^27.5.1", - "jest-util": "^27.5.1", - "slash": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/console/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/console/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/console/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/console/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/@jest/console/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/core": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-27.5.1.tgz", - "integrity": "sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ==", - "dependencies": { - "@jest/console": "^27.5.1", - "@jest/reporters": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.8.1", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^27.5.1", - "jest-config": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-resolve-dependencies": "^27.5.1", - "jest-runner": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "jest-watcher": "^27.5.1", - "micromatch": "^4.0.4", - "rimraf": "^3.0.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/core/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/core/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/core/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/core/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/@jest/core/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/core/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/environment": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.5.1.tgz", - "integrity": "sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==", - "dependencies": { - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz", - "integrity": "sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==", - "dependencies": { - "@jest/types": "^27.5.1", - "@sinonjs/fake-timers": "^8.0.1", - "@types/node": "*", - "jest-message-util": "^27.5.1", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-27.5.1.tgz", - "integrity": "sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q==", - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/types": "^27.5.1", - "expect": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-27.5.1.tgz", - "integrity": "sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw==", - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.2", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^5.1.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-haste-map": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", - "slash": "^3.0.0", - "source-map": "^0.6.0", - "string-length": "^4.0.1", - "terminal-link": "^2.0.0", - "v8-to-istanbul": "^8.1.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/reporters/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/reporters/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/reporters/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/reporters/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/@jest/reporters/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/reporters/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/@jest/reporters/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/schemas": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz", - "integrity": "sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==", - "dependencies": { - "@sinclair/typebox": "^0.24.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/source-map": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-27.5.1.tgz", - "integrity": "sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg==", - "dependencies": { - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9", - "source-map": "^0.6.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/source-map/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/@jest/test-result": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-27.5.1.tgz", - "integrity": "sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag==", - "dependencies": { - "@jest/console": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz", - "integrity": "sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ==", - "dependencies": { - "@jest/test-result": "^27.5.1", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-runtime": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-27.5.1.tgz", - "integrity": "sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw==", - "dependencies": { - "@babel/core": "^7.1.0", - "@jest/types": "^27.5.1", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-util": "^27.5.1", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/transform/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/transform/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/transform/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/transform/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/@jest/transform/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/transform/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/@jest/transform/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/types/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/types/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/types/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/types/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/@jest/types/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/types/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "dev": true }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", - "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", - "dependencies": { - "@jridgewell/set-array": "^1.0.0", - "@jridgewell/sourcemap-codec": "^1.4.10" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "engines": { - "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.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -3114,38 +2393,73 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", - "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" } }, - "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", - "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==" + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mapbox/hast-util-table-cell-style": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@mapbox/hast-util-table-cell-style/-/hast-util-table-cell-style-0.2.1.tgz", + "integrity": "sha512-LyQz4XJIdCdY/+temIhD/Ed0x/p4GAOUycpFSEK2Ads1CPKZy6b7V/2ROEtQiLLQ8soIs0xe/QAoR6kwpyW/yw==", + "dependencies": { + "unist-util-visit": "^1.4.1" + }, + "engines": { + "node": ">=12" + } }, "node_modules/@mui/base": { - "version": "5.0.0-alpha.123", - "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.123.tgz", - "integrity": "sha512-pxzcAfET3I6jvWqS4kijiLMn1OmdMw+mGmDa0SqmDZo3bXXdvLhpCCPqCkULG3UykhvFCOcU5HclOX3JCA+Zhg==", + "version": "5.0.0-beta.22", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.22.tgz", + "integrity": "sha512-l4asGID5tmyerx9emJfXOKLyXzaBtdXNIFE3M+IrSZaFtGFvaQKHhc3+nxxSxPf1+G44psjczM0ekRQCdXx9HA==", "dependencies": { - "@babel/runtime": "^7.21.0", - "@emotion/is-prop-valid": "^1.2.0", - "@mui/types": "^7.2.3", - "@mui/utils": "^5.11.13", - "@popperjs/core": "^2.11.7", - "clsx": "^1.2.1", - "prop-types": "^15.8.1", - "react-is": "^18.2.0" + "@babel/runtime": "^7.23.2", + "@floating-ui/react-dom": "^2.0.2", + "@mui/types": "^7.2.8", + "@mui/utils": "^5.14.16", + "@popperjs/core": "^2.11.8", + "clsx": "^2.0.0", + "prop-types": "^15.8.1" }, "engines": { "node": ">=12.0.0" @@ -3166,20 +2480,20 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "5.11.15", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.11.15.tgz", - "integrity": "sha512-Q0e2oBsjHyIWWj1wLzl14btunvBYC0yl+px7zL9R69tF87uenj6q72ieS369BJ6jxYpJwvXfR6/f+TC+ZUsKKg==", + "version": "5.14.16", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.14.16.tgz", + "integrity": "sha512-97isBjzH2v1K7oB4UH2f4NOkBShOynY6dhnoR2XlUk/g6bb7ZBv2I3D1hvvqPtpEigKu93e7f/jAYr5d9LOc5w==", "funding": { "type": "opencollective", "url": "https://opencollective.com/mui" } }, "node_modules/@mui/icons-material": { - "version": "5.11.11", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.11.11.tgz", - "integrity": "sha512-Eell3ADmQVE8HOpt/LZ3zIma8JSvPh3XgnhwZLT0k5HRqZcd6F/QDHc7xsWtgz09t+UEFvOYJXjtrwKmLdwwpw==", + "version": "5.14.16", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.14.16.tgz", + "integrity": "sha512-wmOgslMEGvbHZjFLru8uH5E+pif/ciXAvKNw16q6joK6EWVWU5rDYWFknDaZhCvz8ZE/K8ZnJQ+lMG6GgHzXbg==", "dependencies": { - "@babel/runtime": "^7.21.0" + "@babel/runtime": "^7.23.2" }, "engines": { "node": ">=12.0.0" @@ -3200,19 +2514,19 @@ } }, "node_modules/@mui/material": { - "version": "5.11.15", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.11.15.tgz", - "integrity": "sha512-E5RbLq9/OvRKmGyeZawdnmFBCvhKkI/Zqgr0xFqW27TGwKLxObq/BreJc6Uu5Sbv8Fjj34vEAbRx6otfOyxn5w==", + "version": "5.14.16", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.14.16.tgz", + "integrity": "sha512-W4zZ4vnxgGk6/HqBwgsDHKU7x2l2NhX+r8gAwfg58Rhu3ikfY7NkIS6y8Gl3NkATc4GG1FNaGjjpQKfJx3U6Jw==", "dependencies": { - "@babel/runtime": "^7.21.0", - "@mui/base": "5.0.0-alpha.123", - "@mui/core-downloads-tracker": "^5.11.15", - "@mui/system": "^5.11.15", - "@mui/types": "^7.2.3", - "@mui/utils": "^5.11.13", - "@types/react-transition-group": "^4.4.5", - "clsx": "^1.2.1", - "csstype": "^3.1.1", + "@babel/runtime": "^7.23.2", + "@mui/base": "5.0.0-beta.22", + "@mui/core-downloads-tracker": "^5.14.16", + "@mui/system": "^5.14.16", + "@mui/types": "^7.2.8", + "@mui/utils": "^5.14.16", + "@types/react-transition-group": "^4.4.8", + "clsx": "^2.0.0", + "csstype": "^3.1.2", "prop-types": "^15.8.1", "react-is": "^18.2.0", "react-transition-group": "^4.4.5" @@ -3244,12 +2558,12 @@ } }, "node_modules/@mui/private-theming": { - "version": "5.11.13", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.11.13.tgz", - "integrity": "sha512-PJnYNKzW5LIx3R+Zsp6WZVPs6w5sEKJ7mgLNnUXuYB1zo5aX71FVLtV7geyPXRcaN2tsoRNK7h444ED0t7cIjA==", + "version": "5.14.16", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.14.16.tgz", + "integrity": "sha512-FNlL0pTSEBh8nXsVWreCHDSHk+jG8cBx1sxRbT8JVtL+PYbYPi802zfV4B00Kkf0LNRVRvAVQwojMWSR/MYGng==", "dependencies": { - "@babel/runtime": "^7.21.0", - "@mui/utils": "^5.11.13", + "@babel/runtime": "^7.23.2", + "@mui/utils": "^5.14.16", "prop-types": "^15.8.1" }, "engines": { @@ -3270,13 +2584,13 @@ } }, "node_modules/@mui/styled-engine": { - "version": "5.11.11", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.11.11.tgz", - "integrity": "sha512-wV0UgW4lN5FkDBXefN8eTYeuE9sjyQdg5h94vtwZCUamGQEzmCOtir4AakgmbWMy0x8OLjdEUESn9wnf5J9MOg==", + "version": "5.14.16", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.14.16.tgz", + "integrity": "sha512-FfvYvTG/Zd+KXMMImbcMYEeQAbONGuX5Vx3gBmmtB6KyA7Mvm9Pma1ly3R0gc44yeoFd+2wBjn1feS8h42HW5w==", "dependencies": { - "@babel/runtime": "^7.21.0", - "@emotion/cache": "^11.10.5", - "csstype": "^3.1.1", + "@babel/runtime": "^7.23.2", + "@emotion/cache": "^11.11.0", + "csstype": "^3.1.2", "prop-types": "^15.8.1" }, "engines": { @@ -3301,17 +2615,17 @@ } }, "node_modules/@mui/system": { - "version": "5.11.15", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.11.15.tgz", - "integrity": "sha512-vCatoWCTnAPquoNifHbqMCMnOElEbLosVUeW0FQDyjCq+8yMABD9E6iY0s14O7iq1wD+qqU7rFAuDIVvJ/AzzA==", + "version": "5.14.16", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.14.16.tgz", + "integrity": "sha512-uKnPfsDqDs8bbN54TviAuoGWOmFiQLwNZ3Wvj+OBkJCzwA6QnLb/sSeCB7Pk3ilH4h4jQ0BHtbR+Xpjy9wlOuA==", "dependencies": { - "@babel/runtime": "^7.21.0", - "@mui/private-theming": "^5.11.13", - "@mui/styled-engine": "^5.11.11", - "@mui/types": "^7.2.3", - "@mui/utils": "^5.11.13", - "clsx": "^1.2.1", - "csstype": "^3.1.1", + "@babel/runtime": "^7.23.2", + "@mui/private-theming": "^5.14.16", + "@mui/styled-engine": "^5.14.16", + "@mui/types": "^7.2.8", + "@mui/utils": "^5.14.16", + "clsx": "^2.0.0", + "csstype": "^3.1.2", "prop-types": "^15.8.1" }, "engines": { @@ -3340,11 +2654,11 @@ } }, "node_modules/@mui/types": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.3.tgz", - "integrity": "sha512-tZ+CQggbe9Ol7e/Fs5RcKwg/woU+o8DCtOnccX6KmbBc7YrfqMYEYuaIcXHuhpT880QwNkZZ3wQwvtlDFA2yOw==", + "version": "7.2.8", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.8.tgz", + "integrity": "sha512-9u0ji+xspl96WPqvrYJF/iO+1tQ1L5GTaDOeG3vCR893yy7VcWwRNiVMmPdPNpMDqx0WV1wtEW9OMwK9acWJzQ==", "peerDependencies": { - "@types/react": "*" + "@types/react": "^17.0.0 || ^18.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -3353,13 +2667,12 @@ } }, "node_modules/@mui/utils": { - "version": "5.11.13", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.11.13.tgz", - "integrity": "sha512-5ltA58MM9euOuUcnvwFJqpLdEugc9XFsRR8Gt4zZNb31XzMfSKJPR4eumulyhsOTK1rWf7K4D63NKFPfX0AxqA==", + "version": "5.14.16", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.16.tgz", + "integrity": "sha512-3xV31GposHkwRbQzwJJuooWpK2ybWdEdeUPtRjv/6vjomyi97F3+68l+QVj9tPTvmfSbr2sx5c/NuvDulrdRmA==", "dependencies": { - "@babel/runtime": "^7.21.0", - "@types/prop-types": "^15.7.5", - "@types/react-is": "^16.7.1 || ^17.0.0", + "@babel/runtime": "^7.23.2", + "@types/prop-types": "^15.7.9", "prop-types": "^15.8.1", "react-is": "^18.2.0" }, @@ -3371,41 +2684,20 @@ "url": "https://opencollective.com/mui" }, "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", "react": "^17.0.0 || ^18.0.0" - } - }, - "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { - "version": "5.1.1-v1", - "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", - "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", - "dependencies": { - "eslint-scope": "5.1.1" - } - }, - "node_modules/@nicolo-ribaudo/eslint-scope-5-internals/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@nicolo-ribaudo/eslint-scope-5-internals/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "engines": { - "node": ">=4.0" + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -3418,6 +2710,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, "engines": { "node": ">= 8" } @@ -3426,6 +2719,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -3434,184 +2728,28 @@ "node": ">= 8" } }, - "node_modules/@pmmmwh/react-refresh-webpack-plugin": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz", - "integrity": "sha512-j0Ya0hCFZPd4x40qLzbhGsh9TMtdb+CJQiso+WxLOPNasohq9cc5SNUcwsZaRH6++Xh91Xkm/xHCkuIiIu0LUA==", - "dependencies": { - "ansi-html-community": "^0.0.8", - "common-path-prefix": "^3.0.0", - "core-js-pure": "^3.23.3", - "error-stack-parser": "^2.0.6", - "find-up": "^5.0.0", - "html-entities": "^2.1.0", - "loader-utils": "^2.0.4", - "schema-utils": "^3.0.0", - "source-map": "^0.7.3" - }, - "engines": { - "node": ">= 10.13" - }, - "peerDependencies": { - "@types/webpack": "4.x || 5.x", - "react-refresh": ">=0.10.0 <1.0.0", - "sockjs-client": "^1.4.0", - "type-fest": ">=0.17.0 <4.0.0", - "webpack": ">=4.43.0 <6.0.0", - "webpack-dev-server": "3.x || 4.x", - "webpack-hot-middleware": "2.x", - "webpack-plugin-serve": "0.x || 1.x" - }, - "peerDependenciesMeta": { - "@types/webpack": { - "optional": true - }, - "sockjs-client": { - "optional": true - }, - "type-fest": { - "optional": true - }, - "webpack-dev-server": { - "optional": true - }, - "webpack-hot-middleware": { - "optional": true - }, - "webpack-plugin-serve": { - "optional": true - } - } - }, - "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "engines": { - "node": ">= 8" - } - }, "node_modules/@popperjs/core": { - "version": "2.11.7", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz", - "integrity": "sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==", + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" } }, "node_modules/@remix-run/router": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.5.0.tgz", - "integrity": "sha512-bkUDCp8o1MvFO+qxkODcbhSqRa6P2GXgrGZVpt0dCXNW2HCSCqYI0ZoAqEOSAjRWmmlKcYgFvN4B4S+zo/f8kg==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.11.0.tgz", + "integrity": "sha512-BHdhcWgeiudl91HvVa2wxqZjSHbheSgIiDvxrF1VjFzBzpTtuDPkOdOi3Iqvc08kXtFkLjhbS+ML9aM8mJS+wQ==", "engines": { - "node": ">=14" - } - }, - "node_modules/@rollup/plugin-babel": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", - "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", - "dependencies": { - "@babel/helper-module-imports": "^7.10.4", - "@rollup/pluginutils": "^3.1.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0", - "@types/babel__core": "^7.1.9", - "rollup": "^1.20.0||^2.0.0" - }, - "peerDependenciesMeta": { - "@types/babel__core": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "11.2.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", - "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", - "dependencies": { - "@rollup/pluginutils": "^3.1.0", - "@types/resolve": "1.17.1", - "builtin-modules": "^3.1.0", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0" - } - }, - "node_modules/@rollup/plugin-replace": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", - "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", - "dependencies": { - "@rollup/pluginutils": "^3.1.0", - "magic-string": "^0.25.7" - }, - "peerDependencies": { - "rollup": "^1.20.0 || ^2.0.0" - } - }, - "node_modules/@rollup/pluginutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", - "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", - "dependencies": { - "@types/estree": "0.0.39", - "estree-walker": "^1.0.1", - "picomatch": "^2.2.2" - }, - "engines": { - "node": ">= 8.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0" - } - }, - "node_modules/@rollup/pluginutils/node_modules/@types/estree": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", - "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==" - }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz", - "integrity": "sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==" - }, - "node_modules/@sinclair/typebox": { - "version": "0.24.51", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", - "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==" - }, - "node_modules/@sinonjs/commons": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", - "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", - "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", - "dependencies": { - "@sinonjs/commons": "^1.7.0" + "node": ">=14.0.0" } }, "node_modules/@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", + "dev": true, "dependencies": { "ejs": "^3.1.6", "json5": "^2.2.0", @@ -3619,233 +2757,11 @@ "string.prototype.matchall": "^4.0.6" } }, - "node_modules/@svgr/babel-plugin-add-jsx-attribute": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz", - "integrity": "sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg==", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-5.4.0.tgz", - "integrity": "sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg==", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-5.0.1.tgz", - "integrity": "sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA==", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-5.0.1.tgz", - "integrity": "sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-svg-dynamic-title": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-5.4.0.tgz", - "integrity": "sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg==", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-svg-em-dimensions": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-5.4.0.tgz", - "integrity": "sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw==", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-transform-react-native-svg": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz", - "integrity": "sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q==", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-transform-svg-component": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz", - "integrity": "sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-preset": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.5.0.tgz", - "integrity": "sha512-4FiXBjvQ+z2j7yASeGPEi8VD/5rrGQk4Xrq3EdJmoZgz/tpqChpo5hgXDvmEauwtvOc52q8ghhZK4Oy7qph4ig==", - "dependencies": { - "@svgr/babel-plugin-add-jsx-attribute": "^5.4.0", - "@svgr/babel-plugin-remove-jsx-attribute": "^5.4.0", - "@svgr/babel-plugin-remove-jsx-empty-expression": "^5.0.1", - "@svgr/babel-plugin-replace-jsx-attribute-value": "^5.0.1", - "@svgr/babel-plugin-svg-dynamic-title": "^5.4.0", - "@svgr/babel-plugin-svg-em-dimensions": "^5.4.0", - "@svgr/babel-plugin-transform-react-native-svg": "^5.4.0", - "@svgr/babel-plugin-transform-svg-component": "^5.5.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/core": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/core/-/core-5.5.0.tgz", - "integrity": "sha512-q52VOcsJPvV3jO1wkPtzTuKlvX7Y3xIcWRpCMtBF3MrteZJtBfQw/+u0B1BHy5ColpQc1/YVTrPEtSYIMNZlrQ==", - "dependencies": { - "@svgr/plugin-jsx": "^5.5.0", - "camelcase": "^6.2.0", - "cosmiconfig": "^7.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/hast-util-to-babel-ast": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz", - "integrity": "sha512-cAaR/CAiZRB8GP32N+1jocovUtvlj0+e65TB50/6Lcime+EA49m/8l+P2ko+XPJ4dw3xaPS3jOL4F2X4KWxoeQ==", - "dependencies": { - "@babel/types": "^7.12.6" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/plugin-jsx": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz", - "integrity": "sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA==", - "dependencies": { - "@babel/core": "^7.12.3", - "@svgr/babel-preset": "^5.5.0", - "@svgr/hast-util-to-babel-ast": "^5.5.0", - "svg-parser": "^2.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/plugin-svgo": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-5.5.0.tgz", - "integrity": "sha512-r5swKk46GuQl4RrVejVwpeeJaydoxkdwkM1mBKOgJLBUJPGaLci6ylg/IjhrRsREKDkr4kbMWdgOtbXEh0fyLQ==", - "dependencies": { - "cosmiconfig": "^7.0.0", - "deepmerge": "^4.2.2", - "svgo": "^1.2.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/webpack": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-5.5.0.tgz", - "integrity": "sha512-DOBOK255wfQxguUta2INKkzPj6AIS6iafZYiYmHn6W3pHlycSRRlvWKCfLDG10fXfLWqE3DJHgRUOyJYmARa7g==", - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/plugin-transform-react-constant-elements": "^7.12.1", - "@babel/preset-env": "^7.12.1", - "@babel/preset-react": "^7.12.5", - "@svgr/core": "^5.5.0", - "@svgr/plugin-jsx": "^5.5.0", - "@svgr/plugin-svgo": "^5.5.0", - "loader-utils": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@types/babel__core": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.0.tgz", - "integrity": "sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ==", + "version": "7.20.3", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.3.tgz", + "integrity": "sha512-54fjTSeSHwfan8AyHWrKbfBWiEUrNTZsUwPTDSNaaP1QDQIZbeNUg3a59E9D+375MzUw/x1vx2/0F5LBz+AeYA==", + "dev": true, "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -3855,222 +2771,86 @@ } }, "node_modules/@types/babel__generator": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", - "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.6.tgz", + "integrity": "sha512-66BXMKb/sUWbMdBNdMvajU7i/44RkrA3z/Yt1c7R5xejt8qh84iU54yUWCtm0QwGJlDcf/gg4zd/x4mpLAlb/w==", + "dev": true, "dependencies": { "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__template": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", - "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.3.tgz", + "integrity": "sha512-ciwyCLeuRfxboZ4isgdNZi/tkt06m8Tw6uGbBSBgWrnnZGNXiEyM27xc/PjXGQLqlZ6ylbgHMnm7ccF9tCkOeQ==", + "dev": true, "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__traverse": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.3.tgz", - "integrity": "sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==", + "version": "7.20.3", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.3.tgz", + "integrity": "sha512-Lsh766rGEFbaxMIDH7Qa+Yha8cMVI3qAK6CHt3OR0YfxOIn5Z54iHiyDRycHrBqeIiqGa20Kpsv1cavfBKkRSw==", + "dev": true, "dependencies": { - "@babel/types": "^7.3.0" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/bonjour": { - "version": "3.5.10", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", - "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect-history-api-fallback": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz", - "integrity": "sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==", - "dependencies": { - "@types/express-serve-static-core": "*", - "@types/node": "*" - } - }, - "node_modules/@types/eslint": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.37.0.tgz", - "integrity": "sha512-Piet7dG2JBuDIfohBngQ3rCt7MgO9xCO4xIMKxBThCq5PNRB91IjlJ10eJVwfoNtvTErmxLzwBZ7rHZtbOMmFQ==", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", - "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" + "@babel/types": "^7.20.7" } }, "node_modules/@types/estree": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", - "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==" - }, - "node_modules/@types/express": { - "version": "4.17.17", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", - "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.17.33", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz", - "integrity": "sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" - } - }, - "node_modules/@types/graceful-fs": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", - "integrity": "sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/html-minifier-terser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==" - }, - "node_modules/@types/http-proxy": { - "version": "1.17.10", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.10.tgz", - "integrity": "sha512-Qs5aULi+zV1bwKAg5z1PWnDXWmsn+LxIvUGv6E2+OOMYhclZMO+OXd9pYVf2gLykf2I7IV2u7oTHwChPNsvJ7g==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==" + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true }, - "node_modules/@types/mime": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", - "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==" + "node_modules/@types/mdast": { + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.14.tgz", + "integrity": "sha512-gVZ04PGgw1qLZKsnWnyFv4ORnaJ+DXLdHTVSFbU8yX6xZ34Bjg4Q32yPkmveUP1yItXReKfB0Aknlh/3zxTKAw==", + "dependencies": { + "@types/unist": "^2" + } }, "node_modules/@types/node": { - "version": "18.15.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", - "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==" + "version": "20.8.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz", + "integrity": "sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" - }, - "node_modules/@types/prettier": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", - "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==" + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.1.tgz", + "integrity": "sha512-3YmXzzPAdOTVljVMkTMBdBEvlOLg2cDQaDhnnhT3nT9uDbnJzjWhKlzb+desT12Y7tGqaN6d+AbozcKzyL36Ng==" }, "node_modules/@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" - }, - "node_modules/@types/q": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz", - "integrity": "sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==" - }, - "node_modules/@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" - }, - "node_modules/@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + "version": "15.7.9", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.9.tgz", + "integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==" }, "node_modules/@types/react": { - "version": "18.0.31", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.31.tgz", - "integrity": "sha512-EEG67of7DsvRDU6BLLI0p+k1GojDLz9+lZsnCpCRTa/lOokvyPBvp8S5x+A24hME3yyQuIipcP70KJ6H7Qupww==", + "version": "18.2.35", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.35.tgz", + "integrity": "sha512-LG3xpFZ++rTndV+/XFyX5vUP7NI9yxyk+MQvBDq+CVs8I9DLSc3Ymwb1Vmw5YDoeNeHN4PDZa3HylMKJYT9PNQ==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2" } }, - "node_modules/@types/react-is": { - "version": "17.0.3", - "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.3.tgz", - "integrity": "sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==", - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==", + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.8.tgz", + "integrity": "sha512-QmQ22q+Pb+HQSn04NL3HtrqHwYMf4h3QKArOy5F8U5nEVMaihBs3SR10WiOM1iwPz5jIo8x/u11al+iEGZZrvg==", "dependencies": { "@types/react": "*" } @@ -4079,461 +2859,57 @@ "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "dev": true, "dependencies": { "@types/node": "*" } }, - "node_modules/@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" - }, "node_modules/@types/scheduler": { - "version": "0.16.3", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" - }, - "node_modules/@types/semver": { - "version": "7.3.13", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", - "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==" - }, - "node_modules/@types/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz", - "integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==", - "dependencies": { - "@types/mime": "*", - "@types/node": "*" - } - }, - "node_modules/@types/sockjs": { - "version": "0.3.33", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", - "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/stack-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", - "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==" + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.5.tgz", + "integrity": "sha512-s/FPdYRmZR8SjLWGMCuax7r3qCWQw9QKHzXVukAuuIJkXkDRwp+Pu5LMIVFi0Fxbav35WURicYr8u1QsoybnQw==" }, "node_modules/@types/trusted-types": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz", - "integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==" + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.5.tgz", + "integrity": "sha512-I3pkr8j/6tmQtKV/ZzHtuaqYSQvyjGRKH4go60Rr0IDLlFxuRT5V32uvB1mecM5G1EVAUyF/4r4QZ1GHgz+mxA==", + "dev": true }, - "node_modules/@types/ws": { - "version": "8.5.4", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz", - "integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==", - "dependencies": { - "@types/node": "*" - } + "node_modules/@types/unist": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.9.tgz", + "integrity": "sha512-zC0iXxAv1C1ERURduJueYzkzZ2zaGyc+P2c95hgkikHPr3z8EdUZOlgEQ5X0DRmwDZn+hekycQnoeiiRVrmilQ==" }, - "node_modules/@types/yargs": { - "version": "16.0.5", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.5.tgz", - "integrity": "sha512-AxO/ADJOBFJScHbWhq2xAhlWP24rY4aCEG/NFaMvbT3X2MgRsLjhjQwsn0Zi5zn0LG9jUhCCZMeX9Dkuw6k+vQ==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", - "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.57.0.tgz", - "integrity": "sha512-itag0qpN6q2UMM6Xgk6xoHa0D0/P+M17THnr4SVgqn9Rgam5k/He33MA7/D7QoJcdMxHFyX7U9imaBonAX/6qA==", - "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.57.0", - "@typescript-eslint/type-utils": "5.57.0", - "@typescript-eslint/utils": "5.57.0", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/experimental-utils": { - "version": "5.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.57.0.tgz", - "integrity": "sha512-0RnrwGQ7MmgtOSnzB/rSGYr2iXENi6L+CtPzX3g5ovo0HlruLukSEKcc4s+q0IEc+DLTDc7Edan0Y4WSQ/bFhw==", - "dependencies": { - "@typescript-eslint/utils": "5.57.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "5.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.57.0.tgz", - "integrity": "sha512-orrduvpWYkgLCyAdNtR1QIWovcNZlEm6yL8nwH/eTxWLd8gsP+25pdLHYzL2QdkqrieaDwLpytHqycncv0woUQ==", - "dependencies": { - "@typescript-eslint/scope-manager": "5.57.0", - "@typescript-eslint/types": "5.57.0", - "@typescript-eslint/typescript-estree": "5.57.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "5.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.57.0.tgz", - "integrity": "sha512-NANBNOQvllPlizl9LatX8+MHi7bx7WGIWYjPHDmQe5Si/0YEYfxSljJpoTyTWFTgRy3X8gLYSE4xQ2U+aCozSw==", - "dependencies": { - "@typescript-eslint/types": "5.57.0", - "@typescript-eslint/visitor-keys": "5.57.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "5.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.57.0.tgz", - "integrity": "sha512-kxXoq9zOTbvqzLbdNKy1yFrxLC6GDJFE2Yuo3KqSwTmDOFjUGeWSakgoXT864WcK5/NAJkkONCiKb1ddsqhLXQ==", - "dependencies": { - "@typescript-eslint/typescript-estree": "5.57.0", - "@typescript-eslint/utils": "5.57.0", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "5.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.57.0.tgz", - "integrity": "sha512-mxsod+aZRSyLT+jiqHw1KK6xrANm19/+VFALVFP5qa/aiJnlP38qpyaTd0fEKhWvQk6YeNZ5LGwI1pDpBRBhtQ==", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.57.0.tgz", - "integrity": "sha512-LTzQ23TV82KpO8HPnWuxM2V7ieXW8O142I7hQTxWIHDcCEIjtkat6H96PFkYBQqGFLW/G/eVVOB9Z8rcvdY/Vw==", - "dependencies": { - "@typescript-eslint/types": "5.57.0", - "@typescript-eslint/visitor-keys": "5.57.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "5.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.57.0.tgz", - "integrity": "sha512-ps/4WohXV7C+LTSgAL5CApxvxbMkl9B9AUZRtnEFonpIxZDIT7wC1xfvuJONMidrkB9scs4zhtRyIwHh4+18kw==", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.57.0", - "@typescript-eslint/types": "5.57.0", - "@typescript-eslint/typescript-estree": "5.57.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.57.0.tgz", - "integrity": "sha512-ery2g3k0hv5BLiKpPuwYt9KBkAp2ugT6VvyShXdLOkax895EC55sP0Tx5L0fZaQueiK3fBLvHVvEl3jFS5ia+g==", - "dependencies": { - "@typescript-eslint/types": "5.57.0", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", - "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", - "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", - "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", - "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", - "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", - "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", - "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", - "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", - "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", - "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", - "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", - "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", - "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/helper-wasm-section": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-opt": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "@webassemblyjs/wast-printer": "1.11.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", - "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", - "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", - "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", - "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", - "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", - "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", - "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", - "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@xtuc/ieee754": { + "node_modules/@ungap/structured-clone": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" - }, - "node_modules/abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "node_modules/@vitejs/plugin-react": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.1.1.tgz", + "integrity": "sha512-Jie2HERK+uh27e+ORXXwEP5h0Y2lS9T2PRGbfebiHGlwzDO0dEnd2aNtOR/qjBlPb1YgxwAONeblL1xqLikLag==", + "dev": true, "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "@babel/core": "^7.23.2", + "@babel/plugin-transform-react-jsx-self": "^7.22.5", + "@babel/plugin-transform-react-jsx-source": "^7.22.5", + "@types/babel__core": "^7.20.3", + "react-refresh": "^0.14.0" }, "engines": { - "node": ">= 0.6" + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0" } }, "node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -4541,85 +2917,20 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-globals": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", - "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", - "dependencies": { - "acorn": "^7.1.1", - "acorn-walk": "^7.1.1" - } - }, - "node_modules/acorn-globals/node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "peerDependencies": { - "acorn": "^8" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/address": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", - "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/adjust-sourcemap-loader": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", - "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", - "dependencies": { - "loader-utils": "^2.0.0", - "regex-parser": "^2.2.11" - }, - "engines": { - "node": ">=8.9" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -4631,79 +2942,11 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-html-community": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", - "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", - "engines": [ - "node >= 0.8.0" - ], - "bin": { - "ansi-html": "bin/ansi-html" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "engines": { "node": ">=8" } @@ -4719,48 +2962,26 @@ "node": ">=4" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" - }, "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dependencies": { - "sprintf-js": "~1.0.2" - } + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, "dependencies": { - "deep-equal": "^2.0.5" + "dequal": "^2.0.3" } }, "node_modules/array-buffer-byte-length": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "is-array-buffer": "^3.0.1" @@ -4769,20 +2990,16 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-flatten": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", - "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==" - }, "node_modules/array-includes": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", - "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", + "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", "is-string": "^1.0.7" }, "engines": { @@ -4792,22 +3009,34 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "node_modules/array.prototype.findlastindex": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", + "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/array.prototype.flat": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", - "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-shim-unscopables": "^1.0.0" }, "engines": { @@ -4818,13 +3047,14 @@ } }, "node_modules/array.prototype.flatmap": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", - "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-shim-unscopables": "^1.0.0" }, "engines": { @@ -4834,16 +3064,32 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.reduce": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.5.tgz", - "integrity": "sha512-kDdugMl7id9COE8R7MHF5jWk7Dqt/fs4Pv+JXoICnYwqpjjjbUurz6w5fT5IG6brLdJhv6/VoHB0H7oyIBXd+Q==", + "node_modules/array.prototype.tosorted": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.2.tgz", + "integrity": "sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "es-array-method-boxes-properly": "^1.0.0", - "is-string": "^1.0.7" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -4852,82 +3098,41 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz", - "integrity": "sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.1.3" - } - }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" - }, "node_modules/ast-types-flow": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", - "integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==" + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true }, "node_modules/async": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", - "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "node_modules/asynciterator.prototype": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz", + "integrity": "sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + } }, "node_modules/at-least-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, "engines": { "node": ">= 4.0.0" } }, - "node_modules/autoprefixer": { - "version": "10.4.14", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", - "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - } - ], - "dependencies": { - "browserslist": "^4.21.5", - "caniuse-lite": "^1.0.30001464", - "fraction.js": "^4.2.0", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -4936,168 +3141,21 @@ } }, "node_modules/axe-core": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.6.3.tgz", - "integrity": "sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz", + "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==", + "dev": true, "engines": { "node": ">=4" } }, "node_modules/axobject-query": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz", - "integrity": "sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", + "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", + "dev": true, "dependencies": { - "deep-equal": "^2.0.5" - } - }, - "node_modules/babel-jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz", - "integrity": "sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==", - "dependencies": { - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^27.5.1", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/babel-jest/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/babel-jest/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/babel-jest/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/babel-jest/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/babel-jest/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-jest/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-loader": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz", - "integrity": "sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==", - "dependencies": { - "find-cache-dir": "^3.3.1", - "loader-utils": "^2.0.0", - "make-dir": "^3.1.0", - "schema-utils": "^2.6.5" - }, - "engines": { - "node": ">= 8.9" - }, - "peerDependencies": { - "@babel/core": "^7.0.0", - "webpack": ">=2" - } - }, - "node_modules/babel-loader/node_modules/schema-utils": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", - "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", - "dependencies": { - "@types/json-schema": "^7.0.5", - "ajv": "^6.12.4", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz", - "integrity": "sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ==", - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.0.0", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "dequal": "^2.0.3" } }, "node_modules/babel-plugin-macros": { @@ -5114,243 +3172,65 @@ "npm": ">=6" } }, - "node_modules/babel-plugin-named-asset-import": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.8.tgz", - "integrity": "sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q==", - "peerDependencies": { - "@babel/core": "^7.1.0" - } - }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", - "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.6.tgz", + "integrity": "sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==", + "dev": true, "dependencies": { - "@babel/compat-data": "^7.17.7", - "@babel/helper-define-polyfill-provider": "^0.3.3", - "semver": "^6.1.1" + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.4.3", + "semver": "^6.3.1" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "bin": { - "semver": "bin/semver.js" + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", - "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.6.tgz", + "integrity": "sha512-leDIc4l4tUgU7str5BWLS2h8q2N4Nf6lGZP6UrNDxdtfF2g69eJ5L0H7S8A5Ln/arfFAfHor5InAdZuIOwZdgQ==", + "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.3", - "core-js-compat": "^3.25.1" + "@babel/helper-define-polyfill-provider": "^0.4.3", + "core-js-compat": "^3.33.1" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", - "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.3.tgz", + "integrity": "sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==", + "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.3" + "@babel/helper-define-polyfill-provider": "^0.4.3" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/babel-plugin-transform-react-remove-prop-types": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz", - "integrity": "sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==" - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", - "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-top-level-await": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/babel-preset-jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz", - "integrity": "sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag==", - "dependencies": { - "babel-plugin-jest-hoist": "^27.5.1", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/babel-preset-react-app": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-react-app/-/babel-preset-react-app-10.0.1.tgz", - "integrity": "sha512-b0D9IZ1WhhCWkrTXyFuIIgqGzSkRIH5D5AmB0bXbzYAB1OBAwHcUeyWW2LorutLWF5btNo/N7r/cIdmvvKJlYg==", - "dependencies": { - "@babel/core": "^7.16.0", - "@babel/plugin-proposal-class-properties": "^7.16.0", - "@babel/plugin-proposal-decorators": "^7.16.4", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.0", - "@babel/plugin-proposal-numeric-separator": "^7.16.0", - "@babel/plugin-proposal-optional-chaining": "^7.16.0", - "@babel/plugin-proposal-private-methods": "^7.16.0", - "@babel/plugin-transform-flow-strip-types": "^7.16.0", - "@babel/plugin-transform-react-display-name": "^7.16.0", - "@babel/plugin-transform-runtime": "^7.16.4", - "@babel/preset-env": "^7.16.4", - "@babel/preset-react": "^7.16.0", - "@babel/preset-typescript": "^7.16.0", - "@babel/runtime": "^7.16.3", - "babel-plugin-macros": "^3.1.0", - "babel-plugin-transform-react-remove-prop-types": "^0.4.24" + "node_modules/bail": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", + "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/batch": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==" - }, - "node_modules/bfj": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/bfj/-/bfj-7.0.2.tgz", - "integrity": "sha512-+e/UqUzwmzJamNF50tBV6tZPTORow7gQ96iFow+8b562OdMpEK0BcJEq2OSPEDmAbSMBQ7PKZ87ubFkgxpYWgw==", - "dependencies": { - "bluebird": "^3.5.5", - "check-types": "^11.1.1", - "hoopy": "^0.1.4", - "tryer": "^1.0.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "engines": { - "node": "*" - } - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" - }, - "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/bonjour-service": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz", - "integrity": "sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==", - "dependencies": { - "array-flatten": "^2.1.2", - "dns-equal": "^1.0.0", - "fast-deep-equal": "^3.1.3", - "multicast-dns": "^7.2.5" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5360,6 +3240,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, "dependencies": { "fill-range": "^7.0.1" }, @@ -5367,15 +3248,11 @@ "node": ">=8" } }, - "node_modules/browser-process-hrtime": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" - }, "node_modules/browserslist": { - "version": "4.21.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", - "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -5384,13 +3261,17 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "caniuse-lite": "^1.0.30001449", - "electron-to-chromium": "^1.4.284", - "node-releases": "^2.0.8", - "update-browserslist-db": "^1.0.10" + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" }, "bin": { "browserslist": "cli.js" @@ -5399,23 +3280,17 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dependencies": { - "node-int64": "^0.4.0" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true }, "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, "engines": { "node": ">=6" }, @@ -5423,21 +3298,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5451,49 +3320,11 @@ "node": ">=6" } }, - "node_modules/camel-case": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", - "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", - "dependencies": { - "pascal-case": "^3.1.2", - "tslib": "^2.0.3" - } - }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-api": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", - "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", - "dependencies": { - "browserslist": "^4.0.0", - "caniuse-lite": "^1.0.0", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" - } - }, "node_modules/caniuse-lite": { - "version": "1.0.30001473", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001473.tgz", - "integrity": "sha512-ewDad7+D2vlyy+E4UJuVfiBsU69IL+8oVmTuZnH5Q6CIUbxNfI50uVpRHbUPDD6SUaN2o0Lh4DhTrvLG/Tn1yg==", + "version": "1.0.30001561", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001561.tgz", + "integrity": "sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==", + "dev": true, "funding": [ { "type": "opencollective", @@ -5509,14 +3340,6 @@ } ] }, - "node_modules/case-sensitive-paths-webpack-plugin": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz", - "integrity": "sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==", - "engines": { - "node": ">=4" - } - }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -5538,147 +3361,41 @@ "node": ">=0.8.0" } }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "engines": { - "node": ">=10" + "node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/check-types": { - "version": "11.2.2", - "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.2.tgz", - "integrity": "sha512-HBiYvXvn9Z70Z88XKjz3AEKd4HJhBXsa3j7xFnITAzoS8+q6eIGi8qDB8FKPBAjtuxjI/zFpwuiCb8oDtKOYrA==" - }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/ci-info": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", - "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "engines": { - "node": ">=8" - } - }, - "node_modules/cjs-module-lexer": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", - "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==" - }, - "node_modules/clean-css": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", - "integrity": "sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==", - "dependencies": { - "source-map": "~0.6.0" - }, - "engines": { - "node": ">= 10.0" - } - }, - "node_modules/clean-css/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/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" + "node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, "node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", "engines": { "node": ">=6" } }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/coa": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", - "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", - "dependencies": { - "@types/q": "^1.5.1", - "chalk": "^2.4.1", - "q": "^1.1.2" - }, - "engines": { - "node": ">= 4.0" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", - "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==" - }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -5692,191 +3409,60 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, - "node_modules/colord": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", - "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==" - }, - "node_modules/colorette": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", - "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" - }, - "node_modules/combined-stream": { + "node_modules/comma-separated-tokens": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, "node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "engines": { - "node": ">= 12" - } - }, - "node_modules/common-path-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", - "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==" + "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==", + "dev": true }, "node_modules/common-tags": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, "engines": { "node": ">=4.0.0" } }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" - }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/compression/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/compression/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true }, "node_modules/confusing-browser-globals": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", - "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==" - }, - "node_modules/connect-history-api-fallback": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", - "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" - } + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", + "dev": true }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" }, - "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "node_modules/core-js": { - "version": "3.29.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.29.1.tgz", - "integrity": "sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw==", - "hasInstallScript": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, "node_modules/core-js-compat": { - "version": "3.29.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.29.1.tgz", - "integrity": "sha512-QmchCua884D8wWskMX8tW5ydINzd8oSJVx38lx/pVkFGqztxt73GYre3pm/hyYq8bPf+MW5In4I/uRShFDsbrA==", + "version": "3.33.2", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.2.tgz", + "integrity": "sha512-axfo+wxFVxnqf8RvxTzoAlzW4gRoacrHeoFlc9n0x50+7BEyZL/Rt3hicaED1/CEd7I6tPCPVUYcJwCMO5XUYw==", + "dev": true, "dependencies": { - "browserslist": "^4.21.5" + "browserslist": "^4.22.1" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" } }, - "node_modules/core-js-pure": { - "version": "3.29.1", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.29.1.tgz", - "integrity": "sha512-4En6zYVi0i0XlXHVz/bi6l1XDjCqkKRq765NXuX+SnaIatlE96Odt5lMLjdxUiNI1v9OXI5DSLWYPlmTfkTktg==", - "hasInstallScript": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -5904,6 +3490,7 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -5917,435 +3504,29 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true, "engines": { "node": ">=8" } }, - "node_modules/css-blank-pseudo": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz", - "integrity": "sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==", - "dependencies": { - "postcss-selector-parser": "^6.0.9" - }, - "bin": { - "css-blank-pseudo": "dist/cli.cjs" - }, + "node_modules/cssjanus": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cssjanus/-/cssjanus-2.1.0.tgz", + "integrity": "sha512-kAijbny3GmdOi9k+QT6DGIXqFvL96aksNlGr4Rhk9qXDZYWUojU4bRc3IHWxdaLNOqgEZHuXoe5Wl2l7dxLW5g==", "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=10.0.0" } }, - "node_modules/css-declaration-sorter": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.0.tgz", - "integrity": "sha512-jDfsatwWMWN0MODAFuHszfjphEXfNw9JUAhmY4pLu3TyTU+ohUpsbVtbU+1MZn4a47D9kqh03i4eyOm+74+zew==", - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.0.9" - } - }, - "node_modules/css-has-pseudo": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz", - "integrity": "sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw==", - "dependencies": { - "postcss-selector-parser": "^6.0.9" - }, - "bin": { - "css-has-pseudo": "dist/cli.cjs" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/css-loader": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.3.tgz", - "integrity": "sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ==", - "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.19", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.0", - "postcss-modules-scope": "^3.0.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.3.8" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/css-minimizer-webpack-plugin": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.4.1.tgz", - "integrity": "sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q==", - "dependencies": { - "cssnano": "^5.0.6", - "jest-worker": "^27.0.2", - "postcss": "^8.3.5", - "schema-utils": "^4.0.0", - "serialize-javascript": "^6.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@parcel/css": { - "optional": true - }, - "clean-css": { - "optional": true - }, - "csso": { - "optional": true - }, - "esbuild": { - "optional": true - } - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/css-minimizer-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/css-prefers-color-scheme": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz", - "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==", - "bin": { - "css-prefers-color-scheme": "dist/cli.cjs" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/css-select": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.0.1", - "domhandler": "^4.3.1", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-select-base-adapter": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", - "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==" - }, - "node_modules/css-tree": { - "version": "1.0.0-alpha.37", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", - "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", - "dependencies": { - "mdn-data": "2.0.4", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/css-tree/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/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssdb": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.5.2.tgz", - "integrity": "sha512-Xpu7Bf5Vlw+G7ikA2Lg/lVCRTSY8D5M5qFUgGNFyS4pa8ufGLyCBxIX/3if3krHlF1SKSfVPI/YsAWLDVEbocw==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cssnano": { - "version": "5.1.15", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.15.tgz", - "integrity": "sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==", - "dependencies": { - "cssnano-preset-default": "^5.2.14", - "lilconfig": "^2.0.3", - "yaml": "^1.10.2" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/cssnano" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/cssnano-preset-default": { - "version": "5.2.14", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz", - "integrity": "sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==", - "dependencies": { - "css-declaration-sorter": "^6.3.1", - "cssnano-utils": "^3.1.0", - "postcss-calc": "^8.2.3", - "postcss-colormin": "^5.3.1", - "postcss-convert-values": "^5.1.3", - "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.7", - "postcss-merge-rules": "^5.1.4", - "postcss-minify-font-values": "^5.1.0", - "postcss-minify-gradients": "^5.1.1", - "postcss-minify-params": "^5.1.4", - "postcss-minify-selectors": "^5.2.1", - "postcss-normalize-charset": "^5.1.0", - "postcss-normalize-display-values": "^5.1.0", - "postcss-normalize-positions": "^5.1.1", - "postcss-normalize-repeat-style": "^5.1.1", - "postcss-normalize-string": "^5.1.0", - "postcss-normalize-timing-functions": "^5.1.0", - "postcss-normalize-unicode": "^5.1.1", - "postcss-normalize-url": "^5.1.0", - "postcss-normalize-whitespace": "^5.1.1", - "postcss-ordered-values": "^5.1.3", - "postcss-reduce-initial": "^5.1.2", - "postcss-reduce-transforms": "^5.1.0", - "postcss-svgo": "^5.1.0", - "postcss-unique-selectors": "^5.1.1" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/cssnano-utils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz", - "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/csso": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", - "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", - "dependencies": { - "css-tree": "^1.1.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/csso/node_modules/css-tree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", - "dependencies": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" - }, - "node_modules/csso/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/cssom": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", - "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==" - }, - "node_modules/cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", - "dependencies": { - "cssom": "~0.3.6" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cssstyle/node_modules/cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" - }, "node_modules/csstype": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", - "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==" - }, - "node_modules/data-urls": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", - "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", - "dependencies": { - "abab": "^2.0.3", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/data-urls/node_modules/tr46": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", - "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", - "dependencies": { - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/data-urls/node_modules/whatwg-url": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", - "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", - "dependencies": { - "lodash": "^4.7.0", - "tr46": "^2.1.0", - "webidl-conversions": "^6.1.0" - }, - "engines": { - "node": ">=10" - } + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true }, "node_modules/debug": { "version": "4.3.4", @@ -6363,80 +3544,42 @@ } } }, - "node_modules/decimal.js": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", - "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" - }, - "node_modules/dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==" - }, - "node_modules/deep-equal": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.0.tgz", - "integrity": "sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw==", - "dependencies": { - "call-bind": "^1.0.2", - "es-get-iterator": "^1.1.2", - "get-intrinsic": "^1.1.3", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.1", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/default-gateway": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", - "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dev": true, "dependencies": { - "execa": "^5.0.0" + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" }, "engines": { - "node": ">= 10" - } - }, - "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "engines": { - "node": ">=8" + "node": ">= 0.4" } }, "node_modules/define-properties": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", - "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, "dependencies": { + "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" }, @@ -6447,140 +3590,38 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, "engines": { - "node": ">=0.4.0" + "node": ">=6" } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==" - }, - "node_modules/detect-port-alt": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", - "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", - "dependencies": { - "address": "^1.0.1", - "debug": "^2.6.0" - }, - "bin": { - "detect": "bin/detect-port", - "detect-port": "bin/detect-port" - }, - "engines": { - "node": ">= 4.2.1" - } - }, - "node_modules/detect-port-alt/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/detect-port-alt/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, "node_modules/dexie": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/dexie/-/dexie-3.2.3.tgz", - "integrity": "sha512-iHayBd4UYryDCVUNa3PMsJMEnd8yjyh5p7a+RFeC8i8n476BC9wMhVvqiImq5zJZJf5Tuer+s4SSj+AA3x+ZbQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/dexie/-/dexie-3.2.4.tgz", + "integrity": "sha512-VKoTQRSv7+RnffpOJ3Dh6ozknBqzWw/F3iqMdsZg958R0AS8AnY9x9d1lbwENr0gzeGJHXKcGhAMRaqys6SxqA==", "engines": { "node": ">=6.0" } }, "node_modules/dexie-react-hooks": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dexie-react-hooks/-/dexie-react-hooks-1.1.3.tgz", - "integrity": "sha512-bXXE1gfYtfuVYTNiOlyam+YVaO8KaqacgRuxFuP37YtpS6o/jxT6KOl5h+hhqY36s0UavlHWbL+HWJFMcQumIg==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/dexie-react-hooks/-/dexie-react-hooks-1.1.7.tgz", + "integrity": "sha512-Lwv5W0Hk+uOW3kGnsU9GZoR1er1B7WQ5DSdonoNG+focTNeJbHW6vi6nBoX534VKI3/uwHebYzSw1fwY6a7mTw==", "peerDependencies": { "@types/react": ">=16", - "dexie": ">=3.1.0-alpha.1 <5.0.0", + "dexie": "^3.2 || ^4.0.1-alpha", "react": ">=16" } }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" - }, - "node_modules/diff-sequences": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", - "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" - }, - "node_modules/dns-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==" - }, - "node_modules/dns-packet": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.5.0.tgz", - "integrity": "sha512-USawdAUzRkV6xrqTjiAEp6M9YagZEzWcSUaZTcIFAiyQWW1SoI6KyId8y2+/71wbgHKQAKd+iupLv4YvEwYWvA==", - "dependencies": { - "@leichtgewicht/ip-codec": "^2.0.1" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, "dependencies": { "esutils": "^2.0.2" }, @@ -6588,14 +3629,6 @@ "node": ">=6.0.0" } }, - "node_modules/dom-converter": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", - "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", - "dependencies": { - "utila": "~0.4" - } - }, "node_modules/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", @@ -6605,112 +3638,11 @@ "csstype": "^3.0.2" } }, - "node_modules/dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/domexception": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", - "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", - "dependencies": { - "webidl-conversions": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/domexception/node_modules/webidl-conversions": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", - "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "dependencies": { - "domelementtype": "^2.2.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dot-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", - "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/dotenv": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", - "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", - "engines": { - "node": ">=10" - } - }, - "node_modules/dotenv-expand": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", - "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==" - }, - "node_modules/duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, "node_modules/ejs": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "dev": true, "dependencies": { "jake": "^10.8.5" }, @@ -6722,61 +3654,16 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.347", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.347.tgz", - "integrity": "sha512-LNi3+/9nV0vT6Bz1OsSoZ/w7IgNuWdefZ7mjKNjZxyRlI/ag6uMXxsxAy5Etvuixq3Q26exw2fc4bNYvYQqXSw==" - }, - "node_modules/emittery": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", - "integrity": "sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } + "version": "1.4.576", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.576.tgz", + "integrity": "sha512-yXsZyXJfAqzWk1WKryr0Wl0MN2D47xodPvEEwlVePBnhU5E7raevLQR+E6b9JAD3GfL/7MbAL9ZtWQQPcLx7wA==", + "dev": true }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" - }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "engines": { - "node": ">= 4" - } - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", - "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true }, "node_modules/error-ex": { "version": "1.3.2", @@ -6795,24 +3682,26 @@ } }, "node_modules/es-abstract": { - "version": "1.21.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", - "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", + "version": "1.22.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", + "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", + "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "call-bind": "^1.0.5", "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.2.0", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.2", "get-symbol-description": "^1.0.0", "globalthis": "^1.0.3", "gopd": "^1.0.1", - "has": "^1.0.3", "has-property-descriptors": "^1.0.0", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", + "hasown": "^2.0.0", "internal-slot": "^1.0.5", "is-array-buffer": "^3.0.2", "is-callable": "^1.2.7", @@ -6820,19 +3709,23 @@ "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", - "is-typed-array": "^1.1.10", + "is-typed-array": "^1.1.12", "is-weakref": "^1.0.2", - "object-inspect": "^1.12.3", + "object-inspect": "^1.13.1", "object-keys": "^1.1.1", "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.7", - "string.prototype.trimend": "^1.0.6", - "string.prototype.trimstart": "^1.0.6", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", "typed-array-length": "^1.0.4", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.9" + "which-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -6841,60 +3734,56 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-array-method-boxes-properly": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", - "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==" - }, - "node_modules/es-get-iterator": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "node_modules/es-iterator-helpers": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz", + "integrity": "sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==", + "dev": true, "dependencies": { + "asynciterator.prototype": "^1.0.0", "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.1", + "es-set-tostringtag": "^2.0.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "internal-slot": "^1.0.5", + "iterator.prototype": "^1.1.2", + "safe-array-concat": "^1.0.1" } }, - "node_modules/es-module-lexer": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", - "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==" - }, "node_modules/es-set-tostringtag": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", - "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "dev": true, "dependencies": { - "get-intrinsic": "^1.1.3", - "has": "^1.0.3", - "has-tostringtag": "^1.0.0" + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" }, "engines": { "node": ">= 0.4" } }, "node_modules/es-shim-unscopables": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", - "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" } }, "node_modules/es-to-primitive": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, "dependencies": { "is-callable": "^1.1.4", "is-date-object": "^1.0.1", @@ -6907,19 +3796,52 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, "engines": { "node": ">=6" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -6931,104 +3853,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/escodegen": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", - "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/escodegen/node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/escodegen/node_modules/optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "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": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/escodegen/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==", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/escodegen/node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", - "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/eslint": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.37.0.tgz", - "integrity": "sha512-NU3Ps9nI05GUoVMxcZx1J8CNR6xOvUT4jAUMH5+z8lpp3aEdPVCImKw6PWG4PY+Vfkpr+jvMpxs/qoE7wq0sPw==", + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz", + "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==", + "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.2", - "@eslint/js": "8.37.0", - "@humanwhocodes/config-array": "^0.11.8", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.3", + "@eslint/js": "8.53.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", - "eslint-visitor-keys": "^3.4.0", - "espree": "^9.5.1", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -7036,22 +3883,19 @@ "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", + "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "bin": { @@ -7064,55 +3908,83 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-config-react-app": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz", - "integrity": "sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==", + "node_modules/eslint-config-airbnb": { + "version": "19.0.4", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz", + "integrity": "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==", + "dev": true, "dependencies": { - "@babel/core": "^7.16.0", - "@babel/eslint-parser": "^7.16.3", - "@rushstack/eslint-patch": "^1.1.0", - "@typescript-eslint/eslint-plugin": "^5.5.0", - "@typescript-eslint/parser": "^5.5.0", - "babel-preset-react-app": "^10.0.1", - "confusing-browser-globals": "^1.0.11", - "eslint-plugin-flowtype": "^8.0.3", - "eslint-plugin-import": "^2.25.3", - "eslint-plugin-jest": "^25.3.0", - "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-react": "^7.27.1", - "eslint-plugin-react-hooks": "^4.3.0", - "eslint-plugin-testing-library": "^5.0.1" + "eslint-config-airbnb-base": "^15.0.0", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5" }, "engines": { - "node": ">=14.0.0" + "node": "^10.12.0 || ^12.22.0 || ^14.17.0 || >=16.0.0" }, "peerDependencies": { - "eslint": "^8.0.0" + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.3", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-react": "^7.28.0", + "eslint-plugin-react-hooks": "^4.3.0" + } + }, + "node_modules/eslint-config-airbnb-base": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", + "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", + "dev": true, + "dependencies": { + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5", + "semver": "^6.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "peerDependencies": { + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.2" + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", + "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" } }, "node_modules/eslint-import-resolver-node": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz", - "integrity": "sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==", + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, "dependencies": { "debug": "^3.2.7", - "is-core-module": "^2.11.0", - "resolve": "^1.22.1" + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" } }, "node_modules/eslint-import-resolver-node/node_modules/debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, "dependencies": { "ms": "^2.1.1" } }, "node_modules/eslint-module-utils": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz", - "integrity": "sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "dev": true, "dependencies": { "debug": "^3.2.7" }, @@ -7129,47 +4001,34 @@ "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, "dependencies": { "ms": "^2.1.1" } }, - "node_modules/eslint-plugin-flowtype": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz", - "integrity": "sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==", - "dependencies": { - "lodash": "^4.17.21", - "string-natural-compare": "^3.0.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@babel/plugin-syntax-flow": "^7.14.5", - "@babel/plugin-transform-react-jsx": "^7.14.9", - "eslint": "^8.1.0" - } - }, "node_modules/eslint-plugin-import": { - "version": "2.27.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", - "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==", + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz", + "integrity": "sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==", + "dev": true, "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "array.prototype.flatmap": "^1.3.1", + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.7", - "eslint-module-utils": "^2.7.4", - "has": "^1.0.3", - "is-core-module": "^2.11.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.values": "^1.1.6", - "resolve": "^1.22.1", - "semver": "^6.3.0", - "tsconfig-paths": "^3.14.1" + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.14.2" }, "engines": { "node": ">=4" @@ -7182,6 +4041,7 @@ "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, "dependencies": { "ms": "^2.1.1" } @@ -7190,6 +4050,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, "dependencies": { "esutils": "^2.0.2" }, @@ -7197,58 +4058,28 @@ "node": ">=0.10.0" } }, - "node_modules/eslint-plugin-import/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-plugin-jest": { - "version": "25.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz", - "integrity": "sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==", - "dependencies": { - "@typescript-eslint/experimental-utils": "^5.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - }, - "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^4.0.0 || ^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@typescript-eslint/eslint-plugin": { - "optional": true - }, - "jest": { - "optional": true - } - } - }, "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz", - "integrity": "sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz", + "integrity": "sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.20.7", - "aria-query": "^5.1.3", - "array-includes": "^3.1.6", - "array.prototype.flatmap": "^1.3.1", - "ast-types-flow": "^0.0.7", - "axe-core": "^4.6.2", - "axobject-query": "^3.1.1", + "@babel/runtime": "^7.23.2", + "aria-query": "^5.3.0", + "array-includes": "^3.1.7", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "=4.7.0", + "axobject-query": "^3.2.1", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", - "has": "^1.0.3", - "jsx-ast-utils": "^3.3.3", - "language-tags": "=1.0.5", + "es-iterator-helpers": "^1.0.15", + "hasown": "^2.0.0", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", "minimatch": "^3.1.2", - "object.entries": "^1.1.6", - "object.fromentries": "^2.0.6", - "semver": "^6.3.0" + "object.entries": "^1.1.7", + "object.fromentries": "^2.0.7" }, "engines": { "node": ">=4.0" @@ -7257,23 +4088,17 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" } }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/eslint-plugin-react": { - "version": "7.32.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz", - "integrity": "sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==", + "version": "7.33.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", + "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==", + "dev": true, "dependencies": { "array-includes": "^3.1.6", "array.prototype.flatmap": "^1.3.1", "array.prototype.tosorted": "^1.1.1", "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.12", "estraverse": "^5.3.0", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", @@ -7283,7 +4108,7 @@ "object.values": "^1.1.6", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.4", - "semver": "^6.3.0", + "semver": "^6.3.1", "string.prototype.matchall": "^4.0.8" }, "engines": { @@ -7297,6 +4122,7 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "dev": true, "engines": { "node": ">=10" }, @@ -7308,6 +4134,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, "dependencies": { "esutils": "^2.0.2" }, @@ -7316,11 +4143,12 @@ } }, "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.4", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", - "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, "dependencies": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -7331,45 +4159,15 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-plugin-testing-library": { - "version": "5.10.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.10.2.tgz", - "integrity": "sha512-f1DmDWcz5SDM+IpCkEX0lbFqrrTs8HRsEElzDEqN/EBI0hpRj8Cns5+IVANXswE8/LeybIJqPAOQIFu2j5Y5sw==", - "dependencies": { - "@typescript-eslint/utils": "^5.43.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0", - "npm": ">=6" - }, - "peerDependencies": { - "eslint": "^7.5.0 || ^8.0.0" - } - }, "node_modules/eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", - "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -7377,117 +4175,23 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-webpack-plugin": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz", - "integrity": "sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w==", - "dependencies": { - "@types/eslint": "^7.29.0 || ^8.4.1", - "jest-worker": "^28.0.2", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "schema-utils": "^4.0.0" - }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, "engines": { - "node": ">= 12.13.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0", - "webpack": "^5.0.0" - } - }, - "node_modules/eslint-webpack-plugin/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/eslint-webpack-plugin/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/eslint-webpack-plugin/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint-webpack-plugin/node_modules/jest-worker": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz", - "integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/eslint-webpack-plugin/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "node_modules/eslint-webpack-plugin/node_modules/schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/eslint-webpack-plugin/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -7498,15 +4202,11 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/eslint/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, "node_modules/eslint/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -7522,6 +4222,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -7532,12 +4233,14 @@ "node_modules/eslint/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/eslint/node_modules/globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "dev": true, "dependencies": { "type-fest": "^0.20.2" }, @@ -7552,25 +4255,16 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "engines": { "node": ">=8" } }, - "node_modules/eslint/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/eslint/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -7582,6 +4276,7 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, "engines": { "node": ">=10" }, @@ -7590,13 +4285,14 @@ } }, "node_modules/espree": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz", - "integrity": "sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, "dependencies": { - "acorn": "^8.8.0", + "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.0" + "eslint-visitor-keys": "^3.4.1" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -7605,22 +4301,11 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/esquery": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, "dependencies": { "estraverse": "^5.1.0" }, @@ -7632,6 +4317,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, "dependencies": { "estraverse": "^5.2.0" }, @@ -7643,6 +4329,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, "engines": { "node": ">=4.0" } @@ -7650,149 +4337,34 @@ "node_modules/estree-walker": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", - "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==" + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expect": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz", - "integrity": "sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==", - "dependencies": { - "@jest/types": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.1", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.5.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/express/node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true }, "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -7808,6 +4380,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -7818,44 +4391,29 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true }, "node_modules/fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, "dependencies": { "reusify": "^1.0.4" } }, - "node_modules/faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", - "dependencies": { - "websocket-driver": ">=0.5.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dependencies": { - "bser": "2.1.1" - } - }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, "dependencies": { "flat-cache": "^3.0.4" }, @@ -7863,29 +4421,11 @@ "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/file-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", - "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", - "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, "dependencies": { "minimatch": "^5.0.1" } @@ -7894,6 +4434,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -7902,6 +4443,7 @@ "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -7909,18 +4451,11 @@ "node": ">=10" } }, - "node_modules/filesize": { - "version": "8.0.7", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", - "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -7928,52 +4463,6 @@ "node": ">=8" } }, - "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", @@ -7983,6 +4472,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -7995,151 +4485,39 @@ } }, "node_modules/flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", + "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", + "dev": true, "dependencies": { - "flatted": "^3.1.0", + "flatted": "^3.2.9", + "keyv": "^4.5.3", "rimraf": "^3.0.2" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=12.0.0" } }, "node_modules/flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" - }, - "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, "dependencies": { "is-callable": "^1.1.3" } }, - "node_modules/fork-ts-checker-webpack-plugin": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz", - "integrity": "sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==", - "dependencies": { - "@babel/code-frame": "^7.8.3", - "@types/json-schema": "^7.0.5", - "chalk": "^4.1.0", - "chokidar": "^3.4.2", - "cosmiconfig": "^6.0.0", - "deepmerge": "^4.2.2", - "fs-extra": "^9.0.0", - "glob": "^7.1.6", - "memfs": "^3.1.2", - "minimatch": "^3.0.4", - "schema-utils": "2.7.0", - "semver": "^7.3.2", - "tapable": "^1.0.0" - }, - "engines": { - "node": ">=10", - "yarn": ">=1.0.0" - }, - "peerDependencies": { - "eslint": ">= 6", - "typescript": ">= 2.7", - "vue-template-compiler": "*", - "webpack": ">= 4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - }, - "vue-template-compiler": { - "optional": true - } - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.7.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": { + "node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", @@ -8150,118 +4528,17 @@ "node": ">=10" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", - "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", - "dependencies": { - "@types/json-schema": "^7.0.4", - "ajv": "^6.12.2", - "ajv-keywords": "^3.4.1" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fraction.js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", - "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://www.patreon.com/infusion" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/fs-monkey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", - "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==" - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -8272,19 +4549,23 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" }, "engines": { "node": ">= 0.4" @@ -8297,6 +4578,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -8305,26 +4587,21 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, "engines": { "node": ">=6.9.0" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, "node_modules/get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8333,31 +4610,14 @@ "node_modules/get-own-enumerable-property-symbols": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", - "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==" - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "dev": true }, "node_modules/get-symbol-description": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.1" @@ -8373,6 +4633,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -8392,6 +4653,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -8399,50 +4661,11 @@ "node": ">=10.13.0" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" - }, - "node_modules/global-modules": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", - "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", - "dependencies": { - "global-prefix": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/global-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", - "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", - "dependencies": { - "ini": "^1.3.5", - "kind-of": "^6.0.2", - "which": "^1.3.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/global-prefix/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, "engines": { "node": ">=4" } @@ -8451,6 +4674,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, "dependencies": { "define-properties": "^1.1.3" }, @@ -8461,29 +4685,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -8494,52 +4700,20 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true }, - "node_modules/grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" - }, - "node_modules/gzip-size": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", - "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", - "dependencies": { - "duplexer": "^0.1.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/handle-thing": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==" - }, - "node_modules/harmony-reflect": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", - "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==" - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -8553,11 +4727,12 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dev": true, "dependencies": { - "get-intrinsic": "^1.1.1" + "get-intrinsic": "^1.2.2" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8567,6 +4742,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -8578,6 +4754,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -8589,6 +4766,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, "dependencies": { "has-symbols": "^1.0.2" }, @@ -8599,12 +4777,33 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "bin": { - "he": "bin/he" + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-to-hyperscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hast-to-hyperscript/-/hast-to-hyperscript-9.0.1.tgz", + "integrity": "sha512-zQgLKqF+O2F72S1aa4y2ivxzSlko3MAvxkwG8ehGmNiqd98BIN3JM1rAJPmplEyLmGLO2QZYJtIneOSZ2YbJuA==", + "dependencies": { + "@types/unist": "^2.0.3", + "comma-separated-tokens": "^1.0.0", + "property-information": "^5.3.0", + "space-separated-tokens": "^1.0.0", + "style-to-object": "^0.3.0", + "unist-util-is": "^4.0.0", + "web-namespaces": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, "node_modules/hoist-non-react-statics": { @@ -8620,98 +4819,6 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, - "node_modules/hoopy": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", - "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==", - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", - "dependencies": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" - } - }, - "node_modules/hpack.js/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/hpack.js/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/hpack.js/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/hpack.js/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/html-encoding-sniffer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", - "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", - "dependencies": { - "whatwg-encoding": "^1.0.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/html-entities": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", - "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==" - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" - }, - "node_modules/html-minifier-terser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", - "dependencies": { - "camel-case": "^4.1.2", - "clean-css": "^5.2.2", - "commander": "^8.3.0", - "he": "^1.2.0", - "param-case": "^3.0.4", - "relateurl": "^0.2.7", - "terser": "^5.10.0" - }, - "bin": { - "html-minifier-terser": "cli.js" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/html-parse-stringify": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", @@ -8720,144 +4827,10 @@ "void-elements": "3.1.0" } }, - "node_modules/html-webpack-plugin": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz", - "integrity": "sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw==", - "dependencies": { - "@types/html-minifier-terser": "^6.0.0", - "html-minifier-terser": "^6.0.2", - "lodash": "^4.17.21", - "pretty-error": "^4.0.0", - "tapable": "^2.0.0" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/html-webpack-plugin" - }, - "peerDependencies": { - "webpack": "^5.20.0" - } - }, - "node_modules/htmlparser2": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", - "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" - } - }, - "node_modules/http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==" - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-parser-js": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", - "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" - }, - "node_modules/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "dependencies": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/http-proxy-middleware": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", - "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", - "dependencies": { - "@types/http-proxy": "^1.17.8", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@types/express": "^4.17.13" - }, - "peerDependenciesMeta": { - "@types/express": { - "optional": true - } - } - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "engines": { - "node": ">=10.17.0" - } - }, "node_modules/humanize-duration": { - "version": "3.28.0", - "resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.28.0.tgz", - "integrity": "sha512-jMAxraOOmHuPbffLVDKkEKi/NeG8dMqP8lGRd6Tbf7JgAeG33jjgPWDbXXU7ypCI0o+oNKJFgbSB9FKVdWNI2A==" + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.30.0.tgz", + "integrity": "sha512-NxpT0fhQTFuMTLnuu1Xp+ozNpYirQnbV3NlOjEKBYlE3uvMRu3LDuq8EPc3gVXxVYnchQfqVM4/+T9iwHPLLeA==" }, "node_modules/i18next": { "version": "21.10.0", @@ -8897,61 +4870,21 @@ "cross-fetch": "3.1.5" } }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, "node_modules/idb": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", - "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" - }, - "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": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==", - "dependencies": { - "harmony-reflect": "^1.4.6" - }, - "engines": { - "node": ">=4" - } + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "dev": true }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, "engines": { "node": ">= 4" } }, - "node_modules/immer": { - "version": "9.0.21", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", - "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -8967,28 +4900,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, "engines": { "node": ">=0.8.19" } @@ -8997,6 +4913,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -9005,53 +4922,55 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + "node_modules/inline-style-parser": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", + "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" }, "node_modules/internal-slot": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", - "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", + "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "dev": true, "dependencies": { - "get-intrinsic": "^1.2.0", - "has": "^1.0.3", + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", "side-channel": "^1.0.4" }, "engines": { "node": ">= 0.4" } }, - "node_modules/ipaddr.js": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", - "integrity": "sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==", - "engines": { - "node": ">= 10" + "node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, "node_modules/is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.2.0", @@ -9066,10 +4985,26 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, + "node_modules/is-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-bigint": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, "dependencies": { "has-bigints": "^1.0.1" }, @@ -9077,21 +5012,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-boolean-object": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -9103,10 +5028,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -9115,11 +5063,11 @@ } }, "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9129,6 +5077,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -9139,48 +5088,56 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, + "node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" + "node_modules/is-finalizationregistry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, "engines": { - "node": ">=6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -9188,10 +5145,20 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-map": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -9199,12 +5166,14 @@ "node_modules/is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true }, "node_modules/is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -9216,6 +5185,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, "engines": { "node": ">=0.12.0" } @@ -9224,6 +5194,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -9238,6 +5209,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -9246,30 +5218,24 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, "engines": { "node": ">=8" } }, "node_modules/is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" - }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -9285,22 +5251,16 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/is-root": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", - "integrity": "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==", - "engines": { - "node": ">=6" - } - }, "node_modules/is-set": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -9309,6 +5269,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2" }, @@ -9320,6 +5281,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, "engines": { "node": ">=8" }, @@ -9331,6 +5293,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -9345,6 +5308,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, "dependencies": { "has-symbols": "^1.0.2" }, @@ -9356,15 +5320,12 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "which-typed-array": "^1.1.11" }, "engines": { "node": ">= 0.4" @@ -9373,15 +5334,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" - }, "node_modules/is-weakmap": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -9390,6 +5347,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, "dependencies": { "call-bind": "^1.0.2" }, @@ -9401,6 +5359,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.1" @@ -9409,132 +5368,41 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "node_modules/iterator.prototype": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", + "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "dev": true, "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps/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/istanbul-reports": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", - "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" } }, "node_modules/jake": { - "version": "10.8.5", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz", - "integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==", + "version": "10.8.7", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", + "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", + "dev": true, "dependencies": { "async": "^3.2.3", "chalk": "^4.0.2", - "filelist": "^1.0.1", - "minimatch": "^3.0.4" + "filelist": "^1.0.4", + "minimatch": "^3.1.2" }, "bin": { "jake": "bin/cli.js" @@ -9547,6 +5415,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -9561,6 +5430,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -9576,6 +5446,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -9586,12 +5457,14 @@ "node_modules/jake/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/jake/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -9600,1843 +5473,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", - "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", - "dependencies": { - "@jest/core": "^27.5.1", - "import-local": "^3.0.2", - "jest-cli": "^27.5.1" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.5.1.tgz", - "integrity": "sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==", - "dependencies": { - "@jest/types": "^27.5.1", - "execa": "^5.0.0", - "throat": "^6.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-circus": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-27.5.1.tgz", - "integrity": "sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw==", - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^0.7.0", - "expect": "^27.5.1", - "is-generator-fn": "^2.0.0", - "jest-each": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.3", - "throat": "^6.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-circus/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-circus/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-circus/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-circus/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-circus/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-circus/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-cli": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-27.5.1.tgz", - "integrity": "sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw==", - "dependencies": { - "@jest/core": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "import-local": "^3.0.2", - "jest-config": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "prompts": "^2.0.1", - "yargs": "^16.2.0" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-cli/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-cli/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-cli/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-cli/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-cli/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-cli/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-config": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-27.5.1.tgz", - "integrity": "sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA==", - "dependencies": { - "@babel/core": "^7.8.0", - "@jest/test-sequencer": "^27.5.1", - "@jest/types": "^27.5.1", - "babel-jest": "^27.5.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.1", - "graceful-fs": "^4.2.9", - "jest-circus": "^27.5.1", - "jest-environment-jsdom": "^27.5.1", - "jest-environment-node": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-jasmine2": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-runner": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^27.5.1", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-config/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-config/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-config/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-config/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-config/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-config/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-diff": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", - "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-diff/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-diff/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-diff/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-diff/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-docblock": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-27.5.1.tgz", - "integrity": "sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ==", - "dependencies": { - "detect-newline": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-each": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-27.5.1.tgz", - "integrity": "sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ==", - "dependencies": { - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "jest-get-type": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-each/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-each/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-each/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-each/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-each/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-each/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-environment-jsdom": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz", - "integrity": "sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw==", - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1", - "jsdom": "^16.6.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-environment-node": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-27.5.1.tgz", - "integrity": "sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw==", - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-get-type": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", - "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-haste-map": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.5.1.tgz", - "integrity": "sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==", - "dependencies": { - "@jest/types": "^27.5.1", - "@types/graceful-fs": "^4.1.2", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^27.5.1", - "jest-serializer": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", - "micromatch": "^4.0.4", - "walker": "^1.0.7" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-jasmine2": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz", - "integrity": "sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ==", - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/source-map": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "expect": "^27.5.1", - "is-generator-fn": "^2.0.0", - "jest-each": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1", - "throat": "^6.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-jasmine2/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-jasmine2/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-jasmine2/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-jasmine2/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-leak-detector": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz", - "integrity": "sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ==", - "dependencies": { - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", - "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-matcher-utils/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-matcher-utils/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-matcher-utils/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-matcher-utils/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-message-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", - "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^27.5.1", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^27.5.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-message-util/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-message-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-message-util/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-message-util/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-message-util/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-message-util/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-mock": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz", - "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==", - "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz", - "integrity": "sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.5.1.tgz", - "integrity": "sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw==", - "dependencies": { - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "resolve": "^1.20.0", - "resolve.exports": "^1.1.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz", - "integrity": "sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg==", - "dependencies": { - "@jest/types": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-snapshot": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-resolve/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-resolve/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-resolve/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-resolve/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-resolve/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-resolve/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-runner": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-27.5.1.tgz", - "integrity": "sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ==", - "dependencies": { - "@jest/console": "^27.5.1", - "@jest/environment": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.8.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^27.5.1", - "jest-environment-jsdom": "^27.5.1", - "jest-environment-node": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-leak-detector": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", - "source-map-support": "^0.5.6", - "throat": "^6.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-runner/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-runner/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-runner/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-runner/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-runner/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-runner/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-runtime": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.5.1.tgz", - "integrity": "sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A==", - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/globals": "^27.5.1", - "@jest/source-map": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "execa": "^5.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-mock": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-runtime/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-runtime/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-runtime/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-runtime/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-runtime/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-runtime/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-serializer": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz", - "integrity": "sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==", - "dependencies": { - "@types/node": "*", - "graceful-fs": "^4.2.9" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-snapshot": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.5.1.tgz", - "integrity": "sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==", - "dependencies": { - "@babel/core": "^7.7.2", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.0.0", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/babel__traverse": "^7.0.4", - "@types/prettier": "^2.1.5", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^27.5.1", - "graceful-fs": "^4.2.9", - "jest-diff": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-util": "^27.5.1", - "natural-compare": "^1.4.0", - "pretty-format": "^27.5.1", - "semver": "^7.3.2" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-snapshot/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-snapshot/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-snapshot/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-snapshot/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", - "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", - "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-util/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-util/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-util/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-util/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-util/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-validate": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-27.5.1.tgz", - "integrity": "sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ==", - "dependencies": { - "@jest/types": "^27.5.1", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^27.5.1", - "leven": "^3.1.0", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-validate/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-validate/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-validate/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-validate/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-validate/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-validate/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-watch-typeahead": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jest-watch-typeahead/-/jest-watch-typeahead-1.1.0.tgz", - "integrity": "sha512-Va5nLSJTN7YFtC2jd+7wsoe1pNe5K4ShLux/E5iHEwlB9AxaxmggY7to9KUqKojhaJw3aXqt5WAb4jGPOolpEw==", - "dependencies": { - "ansi-escapes": "^4.3.1", - "chalk": "^4.0.0", - "jest-regex-util": "^28.0.0", - "jest-watcher": "^28.0.0", - "slash": "^4.0.0", - "string-length": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "jest": "^27.0.0 || ^28.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/@jest/console": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-28.1.3.tgz", - "integrity": "sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw==", - "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^28.1.3", - "jest-util": "^28.1.3", - "slash": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/@jest/console/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-watch-typeahead/node_modules/@jest/test-result": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-28.1.3.tgz", - "integrity": "sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg==", - "dependencies": { - "@jest/console": "^28.1.3", - "@jest/types": "^28.1.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/@jest/types": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-28.1.3.tgz", - "integrity": "sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==", - "dependencies": { - "@jest/schemas": "^28.1.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/@types/yargs": { - "version": "17.0.24", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", - "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest-watch-typeahead/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-watch-typeahead/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-watch-typeahead/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-watch-typeahead/node_modules/emittery": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", - "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/jest-watch-typeahead/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-message-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz", - "integrity": "sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==", - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^28.1.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^28.1.3", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-message-util/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-regex-util": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", - "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", - "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", - "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-watcher": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-28.1.3.tgz", - "integrity": "sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g==", - "dependencies": { - "@jest/test-result": "^28.1.3", - "@jest/types": "^28.1.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.10.2", - "jest-util": "^28.1.3", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-watcher/node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-watcher/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-watch-typeahead/node_modules/pretty-format": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz", - "integrity": "sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==", - "dependencies": { - "@jest/schemas": "^28.1.3", - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-watch-typeahead/node_modules/slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-watch-typeahead/node_modules/string-length": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-5.0.1.tgz", - "integrity": "sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow==", - "dependencies": { - "char-regex": "^2.0.0", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-watch-typeahead/node_modules/string-length/node_modules/char-regex": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-2.0.1.tgz", - "integrity": "sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw==", - "engines": { - "node": ">=12.20" - } - }, - "node_modules/jest-watch-typeahead/node_modules/strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/jest-watch-typeahead/node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/jest-watch-typeahead/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-watcher": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-27.5.1.tgz", - "integrity": "sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw==", - "dependencies": { - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "jest-util": "^27.5.1", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-watcher/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-watcher/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-watcher/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-watcher/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-watcher/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-watcher/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -11445,13 +5482,14 @@ } }, "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "dev": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" + "supports-color": "^7.0.0" }, "engines": { "node": ">= 10.13.0" @@ -11461,30 +5499,21 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "engines": { "node": ">=8" } }, "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/jiti": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz", - "integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==", - "bin": { - "jiti": "bin/jiti.js" + "node": ">=8" } }, "node_modules/js-base64": { @@ -11492,105 +5521,28 @@ "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.5.tgz", "integrity": "sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==" }, - "node_modules/js-sdsl": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", - "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsdom": { - "version": "16.7.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", - "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", - "dependencies": { - "abab": "^2.0.5", - "acorn": "^8.2.4", - "acorn-globals": "^6.0.0", - "cssom": "^0.4.4", - "cssstyle": "^2.3.0", - "data-urls": "^2.0.0", - "decimal.js": "^10.2.1", - "domexception": "^2.0.1", - "escodegen": "^2.0.0", - "form-data": "^3.0.0", - "html-encoding-sniffer": "^2.0.1", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.0", - "parse5": "6.0.1", - "saxes": "^5.0.1", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.0.0", - "w3c-hr-time": "^1.0.2", - "w3c-xmlserializer": "^2.0.0", - "webidl-conversions": "^6.1.0", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.5.0", - "ws": "^7.4.6", - "xml-name-validator": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "canvas": "^2.5.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsdom/node_modules/tr46": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", - "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", - "dependencies": { - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jsdom/node_modules/whatwg-url": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", - "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", - "dependencies": { - "lodash": "^4.7.0", - "tr46": "^2.1.0", - "webidl-conversions": "^6.1.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, "bin": { "jsesc": "bin/jsesc" }, @@ -11598,6 +5550,12 @@ "node": ">=4" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -11606,22 +5564,26 @@ "node_modules/json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, "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": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, "bin": { "json5": "lib/cli.js" }, @@ -11633,6 +5595,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, "dependencies": { "universalify": "^2.0.0" }, @@ -11644,72 +5607,58 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/jsx-ast-utils": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", - "integrity": "sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, "dependencies": { - "array-includes": "^3.1.5", - "object.assign": "^4.1.3" + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" }, "engines": { "node": ">=4.0" } }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "engines": { - "node": ">=6" - } - }, - "node_modules/klona": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", - "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", - "engines": { - "node": ">= 8" + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" } }, "node_modules/language-subtag-registry": { "version": "0.3.22", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", - "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==" + "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==", + "dev": true }, "node_modules/language-tags": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", - "integrity": "sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, "dependencies": { - "language-subtag-registry": "~0.3.2" - } - }, - "node_modules/launch-editor": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.0.tgz", - "integrity": "sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==", - "dependencies": { - "picocolors": "^1.0.0", - "shell-quote": "^1.7.3" + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" } }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, "engines": { "node": ">=6" } @@ -11718,6 +5667,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -11726,44 +5676,16 @@ "node": ">= 0.8.0" } }, - "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "engines": { - "node": ">=10" - } - }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "engines": { - "node": ">=6.11.5" - } - }, - "node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, "dependencies": { "p-locate": "^5.0.0" }, @@ -11777,32 +5699,26 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "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": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==" + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true }, "node_modules/lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "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": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true }, "node_modules/loose-envify": { "version": "1.4.0", @@ -11815,18 +5731,11 @@ "loose-envify": "cli.js" } }, - "node_modules/lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "dependencies": { - "tslib": "^2.0.3" - } - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, "dependencies": { "yallist": "^3.0.2" } @@ -11835,94 +5744,165 @@ "version": "0.25.9", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, "dependencies": { "sourcemap-codec": "^1.4.8" } }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "node_modules/mdast-util-definitions": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz", + "integrity": "sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ==", "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" + "unist-util-visit": "^2.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "node_modules/mdast-util-definitions/node_modules/unist-util-visit": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz", + "integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==", "dependencies": { - "tmpl": "1.0.5" - } - }, - "node_modules/mdn-data": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", - "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==" - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/memfs": { - "version": "3.4.13", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.13.tgz", - "integrity": "sha512-omTM41g3Skpvx5dSYeZIbXKcXoAVc/AoMNwn9TKx++L/gaen/+4TTttmu8ZSch5vfVJ8uJvGbroTsIlslRg6lg==", - "dependencies": { - "fs-monkey": "^1.0.3" + "@types/unist": "^2.0.0", + "unist-util-is": "^4.0.0", + "unist-util-visit-parents": "^3.0.0" }, - "engines": { - "node": ">= 4.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/merge-descriptors": { + "node_modules/mdast-util-definitions/node_modules/unist-util-visit-parents": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", + "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz", + "integrity": "sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-string": "^2.0.0", + "micromark": "~2.11.0", + "parse-entities": "^2.0.0", + "unist-util-stringify-position": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-10.2.0.tgz", + "integrity": "sha512-JoPBfJ3gBnHZ18icCwHR50orC9kNH81tiR1gs01D8Q5YpV6adHNO9nKNuFBCJQ941/32PT1a63UF/DitmS3amQ==", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "mdast-util-definitions": "^4.0.0", + "mdurl": "^1.0.0", + "unist-builder": "^2.0.0", + "unist-util-generated": "^1.0.0", + "unist-util-position": "^3.0.0", + "unist-util-visit": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast/node_modules/unist-util-visit": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz", + "integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^4.0.0", + "unist-util-visit-parents": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast/node_modules/unist-util-visit-parents": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", + "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz", + "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdurl": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, "engines": { "node": ">= 8" } }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "engines": { - "node": ">= 0.6" + "node_modules/micromark": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz", + "integrity": "sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "debug": "^4.0.0", + "parse-entities": "^2.0.0" } }, "node_modules/micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" @@ -11931,120 +5911,11 @@ "node": ">=8.6" } }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/mini-css-extract-plugin": { - "version": "2.7.5", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.5.tgz", - "integrity": "sha512-9HaR++0mlgom81s95vvNjxkg52n2b5s//3ZTI1EtzFb98awsLSivs2LMsVqnQ3ay0PVhqWcGNyDaTE961FOcjQ==", - "dependencies": { - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/mini-css-extract-plugin/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/mini-css-extract-plugin/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/mini-css-extract-plugin/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "node_modules/mini-css-extract-plugin/node_modules/schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -12056,52 +5927,21 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "node_modules/multicast-dns": { - "version": "7.2.5", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", - "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", - "dependencies": { - "dns-packet": "^5.2.2", - "thunky": "^1.0.2" - }, - "bin": { - "multicast-dns": "cli.js" - } - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, "funding": [ { "type": "github", @@ -12118,34 +5958,8 @@ "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" - }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" - }, - "node_modules/no-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "dependencies": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - } + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true }, "node_modules/node-fetch": { "version": "2.6.7", @@ -12166,77 +5980,11 @@ } } }, - "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "engines": { - "node": ">= 6.13.0" - } - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==" - }, "node_modules/node-releases": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", - "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/nwsapi": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz", - "integrity": "sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==" + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true }, "node_modules/object-assign": { "version": "4.1.1", @@ -12246,33 +5994,11 @@ "node": ">=0.10.0" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "engines": { - "node": ">= 6" - } - }, "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-is": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -12281,6 +6007,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, "engines": { "node": ">= 0.4" } @@ -12289,6 +6016,7 @@ "version": "4.1.4", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -12303,26 +6031,28 @@ } }, "node_modules/object.entries": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz", - "integrity": "sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz", + "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" } }, "node_modules/object.fromentries": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz", - "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" @@ -12331,43 +6061,40 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object.getownpropertydescriptors": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.5.tgz", - "integrity": "sha512-yDNzckpM6ntyQiGTik1fKV1DcVDRS+w8bvpWNCBanvH5LfRX9O8WTHqQzG4RZwRAM4I0oU7TV11Lj5v0g20ibw==", + "node_modules/object.groupby": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", + "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "dev": true, "dependencies": { - "array.prototype.reduce": "^1.0.5", "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1" } }, "node_modules/object.hasown": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz", - "integrity": "sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.3.tgz", + "integrity": "sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA==", + "dev": true, "dependencies": { - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/object.values": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", - "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", + "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" @@ -12376,79 +6103,27 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "dependencies": { "wrappy": "1" } }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", - "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" }, "engines": { "node": ">= 0.8.0" @@ -12458,6 +6133,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -12472,6 +6148,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, "dependencies": { "p-limit": "^3.0.2" }, @@ -12482,35 +6159,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", - "dependencies": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/param-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", - "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", - "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -12522,6 +6170,23 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -12539,32 +6204,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/pascal-case": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", - "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, "engines": { "node": ">=8" } @@ -12573,6 +6217,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -12581,6 +6226,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "engines": { "node": ">=8" } @@ -12590,11 +6236,6 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, - "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": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -12603,20 +6244,17 @@ "node": ">=8" } }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" - }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "engines": { "node": ">=8.6" }, @@ -12624,152 +6262,11 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", - "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-up": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", - "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", - "dependencies": { - "find-up": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-up/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-up/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "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": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "engines": { - "node": ">=4" - } - }, "node_modules/postcss": { - "version": "8.4.21", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", - "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -12778,10 +6275,14 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -12789,1232 +6290,42 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-attribute-case-insensitive": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz", - "integrity": "sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ==", - "dependencies": { - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-browser-comments": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-browser-comments/-/postcss-browser-comments-4.0.0.tgz", - "integrity": "sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg==", - "engines": { - "node": ">=8" - }, - "peerDependencies": { - "browserslist": ">=4", - "postcss": ">=8" - } - }, - "node_modules/postcss-calc": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz", - "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", - "dependencies": { - "postcss-selector-parser": "^6.0.9", - "postcss-value-parser": "^4.2.0" - }, - "peerDependencies": { - "postcss": "^8.2.2" - } - }, - "node_modules/postcss-clamp": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", - "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=7.6.0" - }, - "peerDependencies": { - "postcss": "^8.4.6" - } - }, - "node_modules/postcss-color-functional-notation": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.4.tgz", - "integrity": "sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-color-hex-alpha": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.4.tgz", - "integrity": "sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-color-rebeccapurple": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.1.1.tgz", - "integrity": "sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-colormin": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.1.tgz", - "integrity": "sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==", - "dependencies": { - "browserslist": "^4.21.4", - "caniuse-api": "^3.0.0", - "colord": "^2.9.1", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-convert-values": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz", - "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==", - "dependencies": { - "browserslist": "^4.21.4", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-custom-media": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-8.0.2.tgz", - "integrity": "sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.3" - } - }, - "node_modules/postcss-custom-properties": { - "version": "12.1.11", - "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.11.tgz", - "integrity": "sha512-0IDJYhgU8xDv1KY6+VgUwuQkVtmYzRwu+dMjnmdMafXYv86SWqfxkc7qdDvWS38vsjaEtv8e0vGOUQrAiMBLpQ==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-custom-selectors": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-6.0.3.tgz", - "integrity": "sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg==", - "dependencies": { - "postcss-selector-parser": "^6.0.4" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.3" - } - }, - "node_modules/postcss-dir-pseudo-class": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.5.tgz", - "integrity": "sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA==", - "dependencies": { - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-discard-comments": { - "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" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-discard-duplicates": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", - "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-discard-empty": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", - "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-discard-overridden": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", - "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-double-position-gradients": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.2.tgz", - "integrity": "sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ==", - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-env-function": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-4.0.6.tgz", - "integrity": "sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-flexbugs-fixes": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz", - "integrity": "sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==", - "peerDependencies": { - "postcss": "^8.1.4" - } - }, - "node_modules/postcss-focus-visible": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz", - "integrity": "sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==", - "dependencies": { - "postcss-selector-parser": "^6.0.9" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-focus-within": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz", - "integrity": "sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ==", - "dependencies": { - "postcss-selector-parser": "^6.0.9" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-font-variant": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", - "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-gap-properties": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz", - "integrity": "sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==", - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-image-set-function": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-4.0.7.tgz", - "integrity": "sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-import": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", - "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-initial": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz", - "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==", - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-lab-function": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-4.2.1.tgz", - "integrity": "sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w==", - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-load-config": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", - "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", - "dependencies": { - "lilconfig": "^2.0.5", - "yaml": "^1.10.2" - }, - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-loader": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz", - "integrity": "sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==", - "dependencies": { - "cosmiconfig": "^7.0.0", - "klona": "^2.0.5", - "semver": "^7.3.5" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "postcss": "^7.0.0 || ^8.0.1", - "webpack": "^5.0.0" - } - }, - "node_modules/postcss-logical": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz", - "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==", - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-media-minmax": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz", - "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-merge-longhand": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz", - "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==", - "dependencies": { - "postcss-value-parser": "^4.2.0", - "stylehacks": "^5.1.1" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-merge-rules": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz", - "integrity": "sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==", - "dependencies": { - "browserslist": "^4.21.4", - "caniuse-api": "^3.0.0", - "cssnano-utils": "^3.1.0", - "postcss-selector-parser": "^6.0.5" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-font-values": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz", - "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-gradients": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz", - "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==", - "dependencies": { - "colord": "^2.9.1", - "cssnano-utils": "^3.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-params": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz", - "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==", - "dependencies": { - "browserslist": "^4.21.4", - "cssnano-utils": "^3.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-selectors": { - "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" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", - "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", - "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-scope": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", - "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", - "dependencies": { - "postcss-selector-parser": "^6.0.4" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "dependencies": { - "icss-utils": "^5.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-nested": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz", - "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==", - "dependencies": { - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": ">=12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-nesting": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-10.2.0.tgz", - "integrity": "sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA==", - "dependencies": { - "@csstools/selector-specificity": "^2.0.0", - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-normalize": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize/-/postcss-normalize-10.0.1.tgz", - "integrity": "sha512-+5w18/rDev5mqERcG3W5GZNMJa1eoYYNGo8gB7tEwaos0ajk3ZXAI4mHGcNT47NE+ZnZD1pEpUOFLvltIwmeJA==", - "dependencies": { - "@csstools/normalize.css": "*", - "postcss-browser-comments": "^4", - "sanitize.css": "*" - }, - "engines": { - "node": ">= 12" - }, - "peerDependencies": { - "browserslist": ">= 4", - "postcss": ">= 8" - } - }, - "node_modules/postcss-normalize-charset": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", - "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-display-values": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz", - "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-positions": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz", - "integrity": "sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-repeat-style": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz", - "integrity": "sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-string": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz", - "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-timing-functions": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz", - "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-unicode": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz", - "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==", - "dependencies": { - "browserslist": "^4.21.4", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-url": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz", - "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==", - "dependencies": { - "normalize-url": "^6.0.1", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-whitespace": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz", - "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-opacity-percentage": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.3.tgz", - "integrity": "sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A==", - "funding": [ - { - "type": "kofi", - "url": "https://ko-fi.com/mrcgrtz" - }, - { - "type": "liberapay", - "url": "https://liberapay.com/mrcgrtz" - } - ], - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-ordered-values": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz", - "integrity": "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==", - "dependencies": { - "cssnano-utils": "^3.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-overflow-shorthand": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.4.tgz", - "integrity": "sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-page-break": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", - "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", - "peerDependencies": { - "postcss": "^8" - } - }, - "node_modules/postcss-place": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-7.0.5.tgz", - "integrity": "sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-preset-env": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.8.3.tgz", - "integrity": "sha512-T1LgRm5uEVFSEF83vHZJV2z19lHg4yJuZ6gXZZkqVsqv63nlr6zabMH3l4Pc01FQCyfWVrh2GaUeCVy9Po+Aag==", - "dependencies": { - "@csstools/postcss-cascade-layers": "^1.1.1", - "@csstools/postcss-color-function": "^1.1.1", - "@csstools/postcss-font-format-keywords": "^1.0.1", - "@csstools/postcss-hwb-function": "^1.0.2", - "@csstools/postcss-ic-unit": "^1.0.1", - "@csstools/postcss-is-pseudo-class": "^2.0.7", - "@csstools/postcss-nested-calc": "^1.0.0", - "@csstools/postcss-normalize-display-values": "^1.0.1", - "@csstools/postcss-oklab-function": "^1.1.1", - "@csstools/postcss-progressive-custom-properties": "^1.3.0", - "@csstools/postcss-stepped-value-functions": "^1.0.1", - "@csstools/postcss-text-decoration-shorthand": "^1.0.0", - "@csstools/postcss-trigonometric-functions": "^1.0.2", - "@csstools/postcss-unset-value": "^1.0.2", - "autoprefixer": "^10.4.13", - "browserslist": "^4.21.4", - "css-blank-pseudo": "^3.0.3", - "css-has-pseudo": "^3.0.4", - "css-prefers-color-scheme": "^6.0.3", - "cssdb": "^7.1.0", - "postcss-attribute-case-insensitive": "^5.0.2", - "postcss-clamp": "^4.1.0", - "postcss-color-functional-notation": "^4.2.4", - "postcss-color-hex-alpha": "^8.0.4", - "postcss-color-rebeccapurple": "^7.1.1", - "postcss-custom-media": "^8.0.2", - "postcss-custom-properties": "^12.1.10", - "postcss-custom-selectors": "^6.0.3", - "postcss-dir-pseudo-class": "^6.0.5", - "postcss-double-position-gradients": "^3.1.2", - "postcss-env-function": "^4.0.6", - "postcss-focus-visible": "^6.0.4", - "postcss-focus-within": "^5.0.4", - "postcss-font-variant": "^5.0.0", - "postcss-gap-properties": "^3.0.5", - "postcss-image-set-function": "^4.0.7", - "postcss-initial": "^4.0.1", - "postcss-lab-function": "^4.2.1", - "postcss-logical": "^5.0.4", - "postcss-media-minmax": "^5.0.0", - "postcss-nesting": "^10.2.0", - "postcss-opacity-percentage": "^1.1.2", - "postcss-overflow-shorthand": "^3.0.4", - "postcss-page-break": "^3.0.4", - "postcss-place": "^7.0.5", - "postcss-pseudo-class-any-link": "^7.1.6", - "postcss-replace-overflow-wrap": "^4.0.0", - "postcss-selector-not": "^6.0.1", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-pseudo-class-any-link": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.6.tgz", - "integrity": "sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w==", - "dependencies": { - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-reduce-initial": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz", - "integrity": "sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==", - "dependencies": { - "browserslist": "^4.21.4", - "caniuse-api": "^3.0.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-reduce-transforms": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz", - "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-replace-overflow-wrap": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", - "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", - "peerDependencies": { - "postcss": "^8.0.3" - } - }, - "node_modules/postcss-selector-not": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-6.0.1.tgz", - "integrity": "sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ==", - "dependencies": { - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", - "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-svgo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz", - "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==", - "dependencies": { - "postcss-value-parser": "^4.2.0", - "svgo": "^2.7.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-svgo/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "engines": { - "node": ">= 10" - } - }, - "node_modules/postcss-svgo/node_modules/css-tree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", - "dependencies": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/postcss-svgo/node_modules/mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" - }, - "node_modules/postcss-svgo/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/postcss-svgo/node_modules/svgo": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", - "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", - "dependencies": { - "@trysound/sax": "0.2.0", - "commander": "^7.2.0", - "css-select": "^4.1.3", - "css-tree": "^1.1.3", - "csso": "^4.2.0", - "picocolors": "^1.0.0", - "stable": "^0.1.8" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/postcss-unique-selectors": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz", - "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==", - "dependencies": { - "postcss-selector-parser": "^6.0.5" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, "engines": { "node": ">= 0.8.0" } }, - "node_modules/pretty-bytes": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", - "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, "engines": { - "node": ">=6" + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-bytes": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", + "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", + "dev": true, + "engines": { + "node": "^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pretty-error": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", - "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", - "dependencies": { - "lodash": "^4.17.20", - "renderkid": "^3.0.0" - } - }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/pretty-format/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "node_modules/promise": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", - "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", - "dependencies": { - "asap": "~2.0.6" - } - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -14030,71 +6341,32 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "node_modules/property-information": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", + "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" + "xtend": "^4.0.0" }, - "engines": { - "node": ">= 0.10" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/proxy-addr/node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" - }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, "engines": { "node": ">=6" } }, - "node_modules/q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", - "engines": { - "node": ">=0.6.0", - "teleport": ">=0.2.0" - } - }, - "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, "funding": [ { "type": "github", @@ -14110,74 +6382,15 @@ } ] }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/raf": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", - "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", - "dependencies": { - "performance-now": "^2.1.0" - } - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, "dependencies": { "safe-buffer": "^5.1.0" } }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -14189,128 +6402,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-app-polyfill": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-3.0.0.tgz", - "integrity": "sha512-sZ41cxiU5llIB003yxxQBYrARBqe0repqPTTYBTmMqTz9szeBbE37BehCE891NZsmdZqqP+xWKdT3eo3vOzN8w==", - "dependencies": { - "core-js": "^3.19.2", - "object-assign": "^4.1.1", - "promise": "^8.1.0", - "raf": "^3.4.1", - "regenerator-runtime": "^0.13.9", - "whatwg-fetch": "^3.6.2" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/react-dev-utils": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", - "integrity": "sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==", - "dependencies": { - "@babel/code-frame": "^7.16.0", - "address": "^1.1.2", - "browserslist": "^4.18.1", - "chalk": "^4.1.2", - "cross-spawn": "^7.0.3", - "detect-port-alt": "^1.1.6", - "escape-string-regexp": "^4.0.0", - "filesize": "^8.0.6", - "find-up": "^5.0.0", - "fork-ts-checker-webpack-plugin": "^6.5.0", - "global-modules": "^2.0.0", - "globby": "^11.0.4", - "gzip-size": "^6.0.0", - "immer": "^9.0.7", - "is-root": "^2.1.0", - "loader-utils": "^3.2.0", - "open": "^8.4.0", - "pkg-up": "^3.1.0", - "prompts": "^2.4.2", - "react-error-overlay": "^6.0.11", - "recursive-readdir": "^2.2.2", - "shell-quote": "^1.7.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/react-dev-utils/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/react-dev-utils/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/react-dev-utils/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/react-dev-utils/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/react-dev-utils/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/react-dev-utils/node_modules/loader-utils": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", - "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/react-dev-utils/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -14323,11 +6414,6 @@ "react": "^18.2.0" } }, - "node_modules/react-error-overlay": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", - "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" - }, "node_modules/react-i18next": { "version": "11.18.6", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.6.tgz", @@ -14366,113 +6452,63 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, "node_modules/react-refresh": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", - "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", + "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", + "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/react-router": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.10.0.tgz", - "integrity": "sha512-Nrg0BWpQqrC3ZFFkyewrflCud9dio9ME3ojHCF/WLsprJVzkq3q3UeEhMCAW1dobjeGbWgjNn/PVF6m46ANxXQ==", + "node_modules/react-remark": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/react-remark/-/react-remark-2.1.0.tgz", + "integrity": "sha512-7dEPxRGQ23sOdvteuRGaQAs9cEOH/BOeCN4CqsJdk3laUDIDYRCWnM6a3z92PzXHUuxIRLXQNZx7SiO0ijUcbw==", "dependencies": { - "@remix-run/router": "1.5.0" + "rehype-react": "^6.0.0", + "remark-parse": "^9.0.0", + "remark-rehype": "^8.0.0", + "unified": "^9.0.0" }, "engines": { - "node": ">=14" + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.18.0.tgz", + "integrity": "sha512-vk2y7Dsy8wI02eRRaRmOs9g2o+aE72YCx5q9VasT1N9v+lrdB79tIqrjMfByHiY5+6aYkH2rUa5X839nwWGPDg==", + "dependencies": { + "@remix-run/router": "1.11.0" + }, + "engines": { + "node": ">=14.0.0" }, "peerDependencies": { "react": ">=16.8" } }, "node_modules/react-router-dom": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.10.0.tgz", - "integrity": "sha512-E5dfxRPuXKJqzwSe/qGcqdwa18QiWC6f3H3cWXM24qj4N0/beCIf/CWTipop2xm7mR0RCS99NnaqPNjHtrAzCg==", + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.18.0.tgz", + "integrity": "sha512-Ubrue4+Ercc/BoDkFQfc6og5zRQ4A8YxSO3Knsne+eRbZ+IepAsK249XBH/XaFuOYOYr3L3r13CXTLvYt5JDjw==", "dependencies": { - "@remix-run/router": "1.5.0", - "react-router": "6.10.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/react-scripts": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", - "integrity": "sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==", - "dependencies": { - "@babel/core": "^7.16.0", - "@pmmmwh/react-refresh-webpack-plugin": "^0.5.3", - "@svgr/webpack": "^5.5.0", - "babel-jest": "^27.4.2", - "babel-loader": "^8.2.3", - "babel-plugin-named-asset-import": "^0.3.8", - "babel-preset-react-app": "^10.0.1", - "bfj": "^7.0.2", - "browserslist": "^4.18.1", - "camelcase": "^6.2.1", - "case-sensitive-paths-webpack-plugin": "^2.4.0", - "css-loader": "^6.5.1", - "css-minimizer-webpack-plugin": "^3.2.0", - "dotenv": "^10.0.0", - "dotenv-expand": "^5.1.0", - "eslint": "^8.3.0", - "eslint-config-react-app": "^7.0.1", - "eslint-webpack-plugin": "^3.1.1", - "file-loader": "^6.2.0", - "fs-extra": "^10.0.0", - "html-webpack-plugin": "^5.5.0", - "identity-obj-proxy": "^3.0.0", - "jest": "^27.4.3", - "jest-resolve": "^27.4.2", - "jest-watch-typeahead": "^1.0.0", - "mini-css-extract-plugin": "^2.4.5", - "postcss": "^8.4.4", - "postcss-flexbugs-fixes": "^5.0.2", - "postcss-loader": "^6.2.1", - "postcss-normalize": "^10.0.1", - "postcss-preset-env": "^7.0.1", - "prompts": "^2.4.2", - "react-app-polyfill": "^3.0.0", - "react-dev-utils": "^12.0.1", - "react-refresh": "^0.11.0", - "resolve": "^1.20.0", - "resolve-url-loader": "^4.0.0", - "sass-loader": "^12.3.0", - "semver": "^7.3.5", - "source-map-loader": "^3.0.0", - "style-loader": "^3.3.1", - "tailwindcss": "^3.0.2", - "terser-webpack-plugin": "^5.2.5", - "webpack": "^5.64.4", - "webpack-dev-server": "^4.6.0", - "webpack-manifest-plugin": "^4.0.2", - "workbox-webpack-plugin": "^6.4.1" - }, - "bin": { - "react-scripts": "bin/react-scripts.js" + "@remix-run/router": "1.11.0", + "react-router": "6.18.0" }, "engines": { "node": ">=14.0.0" }, - "optionalDependencies": { - "fsevents": "^2.3.2" - }, "peerDependencies": { - "react": ">= 16", - "typescript": "^3.2.1 || ^4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "react": ">=16.8", + "react-dom": ">=16.8" } }, "node_modules/react-transition-group": { @@ -14490,58 +6526,37 @@ "react-dom": ">=16.6.0" } }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "node_modules/reflect.getprototypeof": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", + "integrity": "sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==", + "dev": true, "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" }, "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dependencies": { - "picomatch": "^2.2.1" + "node": ">= 0.4" }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/recursive-readdir": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", - "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", - "dependencies": { - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=6.0.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true }, "node_modules/regenerate-unicode-properties": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", - "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "dev": true, "dependencies": { "regenerate": "^1.4.2" }, @@ -14550,31 +6565,28 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" }, "node_modules/regenerator-transform": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", - "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, "dependencies": { "@babel/runtime": "^7.8.4" } }, - "node_modules/regex-parser": { - "version": "2.2.11", - "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", - "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==" - }, "node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" }, "engines": { "node": ">= 0.4" @@ -14587,6 +6599,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, "dependencies": { "@babel/regjsgen": "^0.8.0", "regenerate": "^1.4.2", @@ -14603,6 +6616,7 @@ "version": "0.9.1", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, "dependencies": { "jsesc": "~0.5.0" }, @@ -14614,57 +6628,63 @@ "version": "0.5.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, "bin": { "jsesc": "bin/jsesc" } }, - "node_modules/relateurl": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/renderkid": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", - "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "node_modules/rehype-react": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/rehype-react/-/rehype-react-6.2.1.tgz", + "integrity": "sha512-f9KIrjktvLvmbGc7si25HepocOg4z0MuNOtweigKzBcDjiGSTGhyz6VSgaV5K421Cq1O+z4/oxRJ5G9owo0KVg==", "dependencies": { - "css-select": "^4.1.3", - "dom-converter": "^0.2.0", - "htmlparser2": "^6.1.0", - "lodash": "^4.17.21", - "strip-ansi": "^6.0.1" + "@mapbox/hast-util-table-cell-style": "^0.2.0", + "hast-to-hyperscript": "^9.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "engines": { - "node": ">=0.10.0" + "node_modules/remark-parse": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-9.0.0.tgz", + "integrity": "sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw==", + "dependencies": { + "mdast-util-from-markdown": "^0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-8.1.0.tgz", + "integrity": "sha512-EbCu9kHgAxKmW1yEYjx3QafMyGY3q8noUbNUI5xyKbaFP89wbhDrKxyIQNukNYthzjNHZu6J7hwFg7hRm1svYA==", + "dependencies": { + "mdast-util-to-hast": "^10.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" - }, "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dependencies": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -14675,25 +6695,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-cwd/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "engines": { - "node": ">=8" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -14702,82 +6703,11 @@ "node": ">=4" } }, - "node_modules/resolve-url-loader": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz", - "integrity": "sha512-05VEMczVREcbtT7Bz+C+96eUO5HDNvdthIiMB34t7FcF8ehcu4wC0sSgPUubs3XW2Q3CNLJk/BJrCU9wVRymiA==", - "dependencies": { - "adjust-sourcemap-loader": "^4.0.0", - "convert-source-map": "^1.7.0", - "loader-utils": "^2.0.0", - "postcss": "^7.0.35", - "source-map": "0.6.1" - }, - "engines": { - "node": ">=8.9" - }, - "peerDependencies": { - "rework": "1.0.1", - "rework-visit": "1.0.0" - }, - "peerDependenciesMeta": { - "rework": { - "optional": true - }, - "rework-visit": { - "optional": true - } - } - }, - "node_modules/resolve-url-loader/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/resolve-url-loader/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/resolve-url-loader/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/resolve.exports": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.1.tgz", - "integrity": "sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ==", - "engines": { - "node": ">=10" - } - }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "engines": { - "node": ">= 4" - } - }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -14787,6 +6717,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -14798,78 +6729,26 @@ } }, "node_modules/rollup": { - "version": "2.79.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", - "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, "bin": { "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=10.0.0" + "node": ">=14.18.0", + "npm": ">=8.0.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, - "node_modules/rollup-plugin-terser": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", - "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", - "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "jest-worker": "^26.2.1", - "serialize-javascript": "^4.0.0", - "terser": "^5.0.0" - }, - "peerDependencies": { - "rollup": "^2.0.0" - } - }, - "node_modules/rollup-plugin-terser/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/rollup-plugin-terser/node_modules/jest-worker": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", - "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^7.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/rollup-plugin-terser/node_modules/serialize-javascript": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", - "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/rollup-plugin-terser/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, "funding": [ { "type": "github", @@ -14888,10 +6767,29 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-array-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", + "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, "funding": [ { "type": "github", @@ -14911,6 +6809,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", @@ -14920,69 +6819,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/sanitize.css": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz", - "integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==" - }, - "node_modules/sass-loader": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", - "integrity": "sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==", - "dependencies": { - "klona": "^2.0.4", - "neo-async": "^2.6.2" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "fibers": ">= 3.1.0", - "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", - "sass": "^1.3.0", - "sass-embedded": "*", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "fibers": { - "optional": true - }, - "node-sass": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - } - } - }, - "node_modules/sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, - "node_modules/saxes": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", - "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/scheduler": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", @@ -14991,211 +6827,58 @@ "loose-envify": "^1.1.0" } }, - "node_modules/schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/select-hose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==" - }, - "node_modules/selfsigned": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz", - "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", - "dependencies": { - "node-forge": "^1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "bin": { "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" } }, - "node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "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": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, "node_modules/serialize-javascript": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", - "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, "dependencies": { "randombytes": "^2.1.0" } }, - "node_modules/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dev": true, "dependencies": { - "accepts": "~1.3.4", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 0.4" } }, - "node_modules/serve-index/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dev": true, "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/serve-index/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "engines": { - "node": ">= 0.6" - } - }, - "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": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">= 0.4" } }, - "node_modules/serve-index/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "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": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/serve-index/node_modules/setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" - }, - "node_modules/serve-index/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -15207,22 +6890,16 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "engines": { "node": ">=8" } }, - "node_modules/shell-quote": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.0.tgz", - "integrity": "sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, "dependencies": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", @@ -15232,39 +6909,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/sockjs": { - "version": "0.3.24", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", - "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", - "dependencies": { - "faye-websocket": "^0.11.3", - "uuid": "^8.3.2", - "websocket-driver": "^0.7.4" - } - }, - "node_modules/source-list-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" - }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -15277,34 +6921,16 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/source-map-loader": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-3.0.2.tgz", - "integrity": "sha512-BokxPoLjyl3iOrgkWaakaxqnelAJSS+0V+De0kKIq6lyWrXuiPgYTGp6z3iHmqljKAaLXwZa+ctD8GccRJeVvg==", - "dependencies": { - "abab": "^2.0.5", - "iconv-lite": "^0.6.3", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -15314,6 +6940,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -15322,47 +6949,18 @@ "version": "1.4.8", "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "deprecated": "Please use @jridgewell/sourcemap-codec instead" + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true }, - "node_modules/spdy": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", - "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", - "dependencies": { - "debug": "^4.1.0", - "handle-thing": "^2.0.0", - "http-deceiver": "^1.2.7", - "select-hose": "^2.0.0", - "spdy-transport": "^3.0.0" - }, - "engines": { - "node": ">=6.0.0" + "node_modules/space-separated-tokens": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", + "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/spdy-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", - "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", - "dependencies": { - "debug": "^4.1.0", - "detect-node": "^2.0.4", - "hpack.js": "^2.1.6", - "obuf": "^1.1.2", - "readable-stream": "^3.0.6", - "wbuf": "^1.7.3" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" - }, - "node_modules/stable": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", - "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility" - }, "node_modules/stack-generator": { "version": "2.0.10", "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz", @@ -15371,25 +6969,6 @@ "stackframe": "^1.3.4" } }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stack-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "engines": { - "node": ">=8" - } - }, "node_modules/stackframe": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", @@ -15422,80 +7001,20 @@ "stacktrace-gps": "^3.0.4" } }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/stop-iteration-iterator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", - "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", - "dependencies": { - "internal-slot": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-natural-compare": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz", - "integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==" - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, "node_modules/string.prototype.matchall": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", - "integrity": "sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==", + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz", + "integrity": "sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "regexp.prototype.flags": "^1.4.3", + "internal-slot": "^1.0.5", + "regexp.prototype.flags": "^1.5.0", + "set-function-name": "^2.0.0", "side-channel": "^1.0.4" }, "funding": { @@ -15503,13 +7022,14 @@ } }, "node_modules/string.prototype.trim": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", - "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" @@ -15519,26 +7039,28 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", - "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", - "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -15548,6 +7070,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "dev": true, "dependencies": { "get-own-enumerable-property-symbols": "^3.0.0", "is-obj": "^1.0.1", @@ -15561,6 +7084,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -15569,33 +7093,28 @@ } }, "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, "engines": { - "node": ">=8" + "node": ">=4" } }, "node_modules/strip-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", + "dev": true, "engines": { "node": ">=10" } }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "engines": { - "node": ">=6" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, "engines": { "node": ">=8" }, @@ -15603,86 +7122,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/style-loader": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.2.tgz", - "integrity": "sha512-RHs/vcrKdQK8wZliteNK4NKzxvLBzpuHMqYmUVWeKa6MkaIQ97ZTOS0b+zapZhy6GcrgWnvWYCMHRirC3FsUmw==", - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/stylehacks": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", - "integrity": "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==", + "node_modules/style-to-object": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz", + "integrity": "sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==", "dependencies": { - "browserslist": "^4.21.4", - "postcss-selector-parser": "^6.0.4" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" + "inline-style-parser": "0.1.1" } }, "node_modules/stylis": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz", - "integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==" + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.0.tgz", + "integrity": "sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ==" }, - "node_modules/sucrase": { - "version": "3.31.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.31.0.tgz", - "integrity": "sha512-6QsHnkqyVEzYcaiHsOKkzOtOgdJcb8i54x6AV2hDwyZcY9ZyykGZVw6L/YN98xC0evwTP6utsWWrKRaa8QlfEQ==", + "node_modules/stylis-plugin-rtl": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/stylis-plugin-rtl/-/stylis-plugin-rtl-2.1.1.tgz", + "integrity": "sha512-q6xIkri6fBufIO/sV55md2CbgS5c6gg9EhSVATtHHCdOnbN/jcI0u3lYhNVeuI65c4lQPo67g8xmq5jrREvzlg==", "dependencies": { - "commander": "^4.0.0", - "glob": "7.1.6", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" + "cssjanus": "^2.0.1" }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/sucrase/node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/sucrase/node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "peerDependencies": { + "stylis": "4.x" } }, "node_modules/supports-color": { @@ -15696,37 +7157,6 @@ "node": ">=4" } }, - "node_modules/supports-hyperlinks": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", - "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", - "dependencies": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-hyperlinks/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-hyperlinks/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -15738,154 +7168,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/svg-parser": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", - "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==" - }, - "node_modules/svgo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", - "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", - "deprecated": "This SVGO version is no longer supported. Upgrade to v2.x.x.", - "dependencies": { - "chalk": "^2.4.1", - "coa": "^2.0.2", - "css-select": "^2.0.0", - "css-select-base-adapter": "^0.1.1", - "css-tree": "1.0.0-alpha.37", - "csso": "^4.0.2", - "js-yaml": "^3.13.1", - "mkdirp": "~0.5.1", - "object.values": "^1.1.0", - "sax": "~1.2.4", - "stable": "^0.1.8", - "unquote": "~1.1.1", - "util.promisify": "~1.0.0" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/svgo/node_modules/css-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", - "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^3.2.1", - "domutils": "^1.7.0", - "nth-check": "^1.0.2" - } - }, - "node_modules/svgo/node_modules/css-what": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", - "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/svgo/node_modules/dom-serializer": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", - "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", - "dependencies": { - "domelementtype": "^2.0.1", - "entities": "^2.0.0" - } - }, - "node_modules/svgo/node_modules/domutils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", - "dependencies": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "node_modules/svgo/node_modules/domutils/node_modules/domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" - }, - "node_modules/svgo/node_modules/nth-check": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", - "dependencies": { - "boolbase": "~1.0.0" - } - }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" - }, - "node_modules/tailwindcss": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.1.tgz", - "integrity": "sha512-Vkiouc41d4CEq0ujXl6oiGFQ7bA3WEhUZdTgXAhtKxSy49OmKs8rEfQmupsfF0IGW8fv2iQkp1EVUuapCFrZ9g==", - "dependencies": { - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "color-name": "^1.1.4", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.2.12", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.17.2", - "lilconfig": "^2.0.6", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.0.9", - "postcss-import": "^14.1.0", - "postcss-js": "^4.0.0", - "postcss-load-config": "^3.1.4", - "postcss-nested": "6.0.0", - "postcss-selector-parser": "^6.0.11", - "postcss-value-parser": "^4.2.0", - "quick-lru": "^5.1.1", - "resolve": "^1.22.1", - "sucrase": "^3.29.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=12.13.0" - }, - "peerDependencies": { - "postcss": "^8.0.9" - } - }, - "node_modules/tailwindcss/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "engines": { - "node": ">=6" - } - }, "node_modules/temp-dir": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "dev": true, "engines": { "node": ">=8" } @@ -15894,6 +7181,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", + "dev": true, "dependencies": { "is-stream": "^2.0.0", "temp-dir": "^2.0.0", @@ -15907,39 +7195,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/tempy/node_modules/type-fest": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", - "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/terminal-link": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", - "dependencies": { - "ansi-escapes": "^4.2.1", - "supports-hyperlinks": "^2.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/terser": { - "version": "5.16.8", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.16.8.tgz", - "integrity": "sha512-QI5g1E/ef7d+PsDifb+a6nnVgC4F22Bg6T0xrBrz6iloVB4PUkkunp6V8nzoOOZJIzjWVdAGqCdlKlhLq/TbIA==", + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.24.0.tgz", + "integrity": "sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==", + "dev": true, "dependencies": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -15950,85 +7213,11 @@ "node": ">=10" } }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.7.tgz", - "integrity": "sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw==", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.16.5" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "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/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/throat": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.2.tgz", - "integrity": "sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==" + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true }, "node_modules/throttle-debounce": { "version": "2.3.0", @@ -16038,16 +7227,6 @@ "node": ">=8" } }, - "node_modules/thunky": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", - "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" - }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==" - }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -16060,6 +7239,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -16067,55 +7247,25 @@ "node": ">=8.0" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tough-cookie": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", - "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==", - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tough-cookie/node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, - "node_modules/tryer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", - "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==" - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" + "node_modules/trough": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz", + "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } }, "node_modules/tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "dev": true, "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", @@ -16127,6 +7277,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, "dependencies": { "minimist": "^1.2.0" }, @@ -16134,42 +7285,11 @@ "json5": "lib/cli.js" } }, - "node_modules/tsconfig-paths/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" - }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, - "node_modules/tsutils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, "dependencies": { "prelude-ls": "^1.2.1" }, @@ -16177,18 +7297,11 @@ "node": ">= 0.8.0" } }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "engines": { - "node": ">=4" - } - }, "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "dev": true, "engines": { "node": ">=10" }, @@ -16196,22 +7309,62 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" }, "engines": { - "node": ">= 0.6" + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/typed-array-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "for-each": "^0.3.3", @@ -16221,31 +7374,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dependencies": { - "is-typedarray": "^1.0.0" - } - }, - "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-bigints": "^1.0.2", @@ -16256,10 +7389,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, "engines": { "node": ">=4" } @@ -16268,6 +7408,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", "unicode-property-aliases-ecmascript": "^2.0.0" @@ -16280,6 +7421,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true, "engines": { "node": ">=4" } @@ -16288,14 +7430,33 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, "engines": { "node": ">=4" } }, + "node_modules/unified": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz", + "integrity": "sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==", + "dependencies": { + "bail": "^1.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^2.0.0", + "trough": "^1.0.0", + "vfile": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unique-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, "dependencies": { "crypto-random-string": "^2.0.0" }, @@ -16303,40 +7464,99 @@ "node": ">=8" } }, + "node_modules/unist-builder": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-2.0.3.tgz", + "integrity": "sha512-f98yt5pnlMWlzP539tPc4grGMsFaQQlP/vM396b00jngsiINumNmsY8rkXjfoi1c6QaM8nQ3vaGDuoKWbe/1Uw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-generated": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-1.1.6.tgz", + "integrity": "sha512-cln2Mm1/CZzN5ttGK7vkoGw+RZ8VcUH6BtGbq98DDtRGquAAOXig1mrBQYelOwMXYS8rK+vZDyyojSjp7JX+Lg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", + "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-3.1.0.tgz", + "integrity": "sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", + "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", + "dependencies": { + "@types/unist": "^2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.1.tgz", + "integrity": "sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==", + "dependencies": { + "unist-util-visit-parents": "^2.0.0" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz", + "integrity": "sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==", + "dependencies": { + "unist-util-is": "^3.0.0" + } + }, + "node_modules/unist-util-visit-parents/node_modules/unist-util-is": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz", + "integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==" + }, "node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, "engines": { "node": ">= 10.0.0" } }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/unquote": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", - "integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==" - }, "node_modules/upath": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true, "engines": { "node": ">=4", "yarn": "*" } }, "node_modules/update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, "funding": [ { "type": "opencollective", @@ -16345,6 +7565,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { @@ -16352,7 +7576,7 @@ "picocolors": "^1.0.0" }, "bin": { - "browserslist-lint": "cli.js" + "update-browserslist-db": "cli.js" }, "peerDependencies": { "browserslist": ">= 4.21.0" @@ -16362,86 +7586,113 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "dependencies": { "punycode": "^2.1.0" } }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "node_modules/vfile": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz", + "integrity": "sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==", "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/util.promisify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", - "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.2", - "has-symbols": "^1.0.1", - "object.getownpropertydescriptors": "^2.1.0" + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^2.0.0", + "vfile-message": "^2.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/utila": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", - "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/v8-to-istanbul": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", - "integrity": "sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==", + "node_modules/vfile-message": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", + "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0", - "source-map": "^0.7.3" + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", + "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==", + "dev": true, + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" }, "engines": { - "node": ">=10.12.0" + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } } }, - "node_modules/v8-to-istanbul/node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" + "node_modules/vite-plugin-pwa": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.15.2.tgz", + "integrity": "sha512-l1srtaad5NMNrAtAuub6ArTYG5Ci9AwofXXQ6IsbpCMYQ/0HUndwI7RB2x95+1UBFm7VGttQtT7woBlVnNhBRw==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "fast-glob": "^3.2.12", + "pretty-bytes": "^6.0.0", + "workbox-build": "^6.5.4", + "workbox-window": "^6.5.4" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^3.1.0 || ^4.0.0", + "workbox-build": "^6.5.4", + "workbox-window": "^6.5.4" } }, "node_modules/void-elements": { @@ -16452,423 +7703,19 @@ "node": ">=0.10.0" } }, - "node_modules/w3c-hr-time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", - "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", - "deprecated": "Use your platform's native performance.now() and performance.timeOrigin.", - "dependencies": { - "browser-process-hrtime": "^1.0.0" - } - }, - "node_modules/w3c-xmlserializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", - "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", - "dependencies": { - "xml-name-validator": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dependencies": { - "makeerror": "1.0.12" - } - }, - "node_modules/watchpack": { - "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" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/wbuf": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", - "dependencies": { - "minimalistic-assert": "^1.0.0" + "node_modules/web-namespaces": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-1.1.4.tgz", + "integrity": "sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, "node_modules/webidl-conversions": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", - "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", - "engines": { - "node": ">=10.4" - } - }, - "node_modules/webpack": { - "version": "5.77.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.77.0.tgz", - "integrity": "sha512-sbGNjBr5Ya5ss91yzjeJTLKyfiwo5C628AFjEa6WSXcZa4E+F57om3Cc8xLb1Jh0b243AWuSYRf3dn7HVeFQ9Q==", - "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^0.0.51", - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/wasm-edit": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "acorn": "^8.7.1", - "acorn-import-assertions": "^1.7.6", - "browserslist": "^4.14.5", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.10.0", - "es-module-lexer": "^0.9.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.1.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.4.0", - "webpack-sources": "^3.2.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-dev-middleware": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", - "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==", - "dependencies": { - "colorette": "^2.0.10", - "memfs": "^3.4.3", - "mime-types": "^2.1.31", - "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/webpack-dev-middleware/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/webpack-dev-middleware/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/webpack-dev-middleware/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "node_modules/webpack-dev-middleware/node_modules/schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/webpack-dev-server": { - "version": "4.13.1", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.13.1.tgz", - "integrity": "sha512-5tWg00bnWbYgkN+pd5yISQKDejRBYGEw15RaEEslH+zdbNDxxaZvEAO2WulaSaFKb5n3YG8JXsGaDsut1D0xdA==", - "dependencies": { - "@types/bonjour": "^3.5.9", - "@types/connect-history-api-fallback": "^1.3.5", - "@types/express": "^4.17.13", - "@types/serve-index": "^1.9.1", - "@types/serve-static": "^1.13.10", - "@types/sockjs": "^0.3.33", - "@types/ws": "^8.5.1", - "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.0.11", - "chokidar": "^3.5.3", - "colorette": "^2.0.10", - "compression": "^1.7.4", - "connect-history-api-fallback": "^2.0.0", - "default-gateway": "^6.0.3", - "express": "^4.17.3", - "graceful-fs": "^4.2.6", - "html-entities": "^2.3.2", - "http-proxy-middleware": "^2.0.3", - "ipaddr.js": "^2.0.1", - "launch-editor": "^2.6.0", - "open": "^8.0.9", - "p-retry": "^4.5.0", - "rimraf": "^3.0.2", - "schema-utils": "^4.0.0", - "selfsigned": "^2.1.1", - "serve-index": "^1.9.1", - "sockjs": "^0.3.24", - "spdy": "^4.0.2", - "webpack-dev-middleware": "^5.3.1", - "ws": "^8.13.0" - }, - "bin": { - "webpack-dev-server": "bin/webpack-dev-server.js" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.37.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - }, - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-dev-server/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/webpack-dev-server/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/webpack-dev-server/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "node_modules/webpack-dev-server/node_modules/schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/webpack-manifest-plugin": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-4.1.1.tgz", - "integrity": "sha512-YXUAwxtfKIJIKkhg03MKuiFAD72PlrqCiwdwO4VEXdRO5V0ORCNwaOwAZawPZalCbmH9kBDmXnNeQOw+BIEiow==", - "dependencies": { - "tapable": "^2.0.0", - "webpack-sources": "^2.2.0" - }, - "engines": { - "node": ">=12.22.0" - }, - "peerDependencies": { - "webpack": "^4.44.2 || ^5.47.0" - } - }, - "node_modules/webpack-manifest-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/webpack-manifest-plugin/node_modules/webpack-sources": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz", - "integrity": "sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==", - "dependencies": { - "source-list-map": "^2.0.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack/node_modules/@types/estree": { - "version": "0.0.51", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", - "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==" - }, - "node_modules/webpack/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/webpack/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", - "dependencies": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/websocket-extensions": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", - "dependencies": { - "iconv-lite": "0.4.24" - } - }, - "node_modules/whatwg-encoding/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/whatwg-fetch": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", - "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" - }, - "node_modules/whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/whatwg-url": { "version": "5.0.0", @@ -16879,15 +7726,11 @@ "webidl-conversions": "^3.0.0" } }, - "node_modules/whatwg-url/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -16902,6 +7745,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, "dependencies": { "is-bigint": "^1.0.1", "is-boolean-object": "^1.1.0", @@ -16913,10 +7757,37 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-builtin-type": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", + "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", + "dev": true, + "dependencies": { + "function.prototype.name": "^1.1.5", + "has-tostringtag": "^1.0.0", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/which-collection": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, "dependencies": { "is-map": "^2.0.1", "is-set": "^2.0.1", @@ -16928,16 +7799,16 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", + "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "dev": true, "dependencies": { "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "call-bind": "^1.0.4", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" + "has-tostringtag": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -16946,35 +7817,30 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/workbox-background-sync": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.5.4.tgz", - "integrity": "sha512-0r4INQZMyPky/lj4Ou98qxcThrETucOde+7mRGJl13MPJugQNKeZQOdIJe/1AchOP23cTqHcN/YVpD6r8E6I8g==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.6.0.tgz", + "integrity": "sha512-jkf4ZdgOJxC9u2vztxLuPT/UjlH7m/nWRQ/MgGL0v8BJHoZdVGJd18Kck+a0e55wGXdqyHO+4IQTk0685g4MUw==", + "dev": true, "dependencies": { "idb": "^7.0.1", - "workbox-core": "6.5.4" + "workbox-core": "6.6.0" } }, "node_modules/workbox-broadcast-update": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-6.5.4.tgz", - "integrity": "sha512-I/lBERoH1u3zyBosnpPEtcAVe5lwykx9Yg1k6f8/BGEPGaMMgZrwVrqL1uA9QZ1NGGFoyE6t9i7lBjOlDhFEEw==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-6.6.0.tgz", + "integrity": "sha512-nm+v6QmrIFaB/yokJmQ/93qIJ7n72NICxIwQwe5xsZiV2aI93MGGyEyzOzDPVz5THEr5rC3FJSsO3346cId64Q==", + "dev": true, "dependencies": { - "workbox-core": "6.5.4" + "workbox-core": "6.6.0" } }, "node_modules/workbox-build": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-6.5.4.tgz", - "integrity": "sha512-kgRevLXEYvUW9WS4XoziYqZ8Q9j/2ziJYEtTrjdz5/L/cTUa2XfyMP2i7c3p34lgqJ03+mTiz13SdFef2POwbA==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-6.6.0.tgz", + "integrity": "sha512-Tjf+gBwOTuGyZwMz2Nk/B13Fuyeo0Q84W++bebbVsfr9iLkDSo6j6PST8tET9HYA58mlRXwlMGpyWO8ETJiXdQ==", + "dev": true, "dependencies": { "@apideck/better-ajv-errors": "^0.3.1", "@babel/core": "^7.11.1", @@ -16998,21 +7864,21 @@ "strip-comments": "^2.0.1", "tempy": "^0.6.0", "upath": "^1.2.0", - "workbox-background-sync": "6.5.4", - "workbox-broadcast-update": "6.5.4", - "workbox-cacheable-response": "6.5.4", - "workbox-core": "6.5.4", - "workbox-expiration": "6.5.4", - "workbox-google-analytics": "6.5.4", - "workbox-navigation-preload": "6.5.4", - "workbox-precaching": "6.5.4", - "workbox-range-requests": "6.5.4", - "workbox-recipes": "6.5.4", - "workbox-routing": "6.5.4", - "workbox-strategies": "6.5.4", - "workbox-streams": "6.5.4", - "workbox-sw": "6.5.4", - "workbox-window": "6.5.4" + "workbox-background-sync": "6.6.0", + "workbox-broadcast-update": "6.6.0", + "workbox-cacheable-response": "6.6.0", + "workbox-core": "6.6.0", + "workbox-expiration": "6.6.0", + "workbox-google-analytics": "6.6.0", + "workbox-navigation-preload": "6.6.0", + "workbox-precaching": "6.6.0", + "workbox-range-requests": "6.6.0", + "workbox-recipes": "6.6.0", + "workbox-routing": "6.6.0", + "workbox-strategies": "6.6.0", + "workbox-streams": "6.6.0", + "workbox-sw": "6.6.0", + "workbox-window": "6.6.0" }, "engines": { "node": ">=10.0.0" @@ -17022,6 +7888,7 @@ "version": "0.3.6", "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz", "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==", + "dev": true, "dependencies": { "json-schema": "^0.4.0", "jsonpointer": "^5.0.0", @@ -17034,10 +7901,84 @@ "ajv": ">=8" } }, + "node_modules/workbox-build/node_modules/@rollup/plugin-babel": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", + "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.10.4", + "@rollup/pluginutils": "^3.1.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "@types/babel__core": "^7.1.9", + "rollup": "^1.20.0||^2.0.0" + }, + "peerDependenciesMeta": { + "@types/babel__core": { + "optional": true + } + } + }, + "node_modules/workbox-build/node_modules/@rollup/plugin-node-resolve": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", + "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "builtin-modules": "^3.1.0", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/workbox-build/node_modules/@rollup/plugin-replace": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", + "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "magic-string": "^0.25.7" + }, + "peerDependencies": { + "rollup": "^1.20.0 || ^2.0.0" + } + }, + "node_modules/workbox-build/node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "dependencies": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, "node_modules/workbox-build/node_modules/ajv": { "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -17049,29 +7990,60 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/workbox-build/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/workbox-build/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/workbox-build/node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/workbox-build/node_modules/rollup": { + "version": "2.79.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", + "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/workbox-build/node_modules/rollup-plugin-terser": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", + "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "jest-worker": "^26.2.1", + "serialize-javascript": "^4.0.0", + "terser": "^5.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0" + } }, "node_modules/workbox-build/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==", + "dev": true, "dependencies": { "whatwg-url": "^7.0.0" }, @@ -17083,6 +8055,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -17090,12 +8063,14 @@ "node_modules/workbox-build/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==" + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true }, "node_modules/workbox-build/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==", + "dev": true, "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", @@ -17103,255 +8078,149 @@ } }, "node_modules/workbox-cacheable-response": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-6.5.4.tgz", - "integrity": "sha512-DCR9uD0Fqj8oB2TSWQEm1hbFs/85hXXoayVwFKLVuIuxwJaihBsLsp4y7J9bvZbqtPJ1KlCkmYVGQKrBU4KAug==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-6.6.0.tgz", + "integrity": "sha512-JfhJUSQDwsF1Xv3EV1vWzSsCOZn4mQ38bWEBR3LdvOxSPgB65gAM6cS2CX8rkkKHRgiLrN7Wxoyu+TuH67kHrw==", + "deprecated": "workbox-background-sync@6.6.0", + "dev": true, "dependencies": { - "workbox-core": "6.5.4" + "workbox-core": "6.6.0" } }, "node_modules/workbox-core": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-6.5.4.tgz", - "integrity": "sha512-OXYb+m9wZm8GrORlV2vBbE5EC1FKu71GGp0H4rjmxmF4/HLbMCoTFws87M3dFwgpmg0v00K++PImpNQ6J5NQ6Q==" + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-6.6.0.tgz", + "integrity": "sha512-GDtFRF7Yg3DD859PMbPAYPeJyg5gJYXuBQAC+wyrWuuXgpfoOrIQIvFRZnQ7+czTIQjIr1DhLEGFzZanAT/3bQ==", + "dev": true }, "node_modules/workbox-expiration": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-6.5.4.tgz", - "integrity": "sha512-jUP5qPOpH1nXtjGGh1fRBa1wJL2QlIb5mGpct3NzepjGG2uFFBn4iiEBiI9GUmfAFR2ApuRhDydjcRmYXddiEQ==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-6.6.0.tgz", + "integrity": "sha512-baplYXcDHbe8vAo7GYvyAmlS4f6998Jff513L4XvlzAOxcl8F620O91guoJ5EOf5qeXG4cGdNZHkkVAPouFCpw==", + "dev": true, "dependencies": { "idb": "^7.0.1", - "workbox-core": "6.5.4" + "workbox-core": "6.6.0" } }, "node_modules/workbox-google-analytics": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-6.5.4.tgz", - "integrity": "sha512-8AU1WuaXsD49249Wq0B2zn4a/vvFfHkpcFfqAFHNHwln3jK9QUYmzdkKXGIZl9wyKNP+RRX30vcgcyWMcZ9VAg==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-6.6.0.tgz", + "integrity": "sha512-p4DJa6OldXWd6M9zRl0H6vB9lkrmqYFkRQ2xEiNdBFp9U0LhsGO7hsBscVEyH9H2/3eZZt8c97NB2FD9U2NJ+Q==", + "dev": true, "dependencies": { - "workbox-background-sync": "6.5.4", - "workbox-core": "6.5.4", - "workbox-routing": "6.5.4", - "workbox-strategies": "6.5.4" + "workbox-background-sync": "6.6.0", + "workbox-core": "6.6.0", + "workbox-routing": "6.6.0", + "workbox-strategies": "6.6.0" } }, "node_modules/workbox-navigation-preload": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-6.5.4.tgz", - "integrity": "sha512-IIwf80eO3cr8h6XSQJF+Hxj26rg2RPFVUmJLUlM0+A2GzB4HFbQyKkrgD5y2d84g2IbJzP4B4j5dPBRzamHrng==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-6.6.0.tgz", + "integrity": "sha512-utNEWG+uOfXdaZmvhshrh7KzhDu/1iMHyQOV6Aqup8Mm78D286ugu5k9MFD9SzBT5TcwgwSORVvInaXWbvKz9Q==", + "dev": true, "dependencies": { - "workbox-core": "6.5.4" + "workbox-core": "6.6.0" } }, "node_modules/workbox-precaching": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-6.5.4.tgz", - "integrity": "sha512-hSMezMsW6btKnxHB4bFy2Qfwey/8SYdGWvVIKFaUm8vJ4E53JAY+U2JwLTRD8wbLWoP6OVUdFlXsTdKu9yoLTg==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-6.6.0.tgz", + "integrity": "sha512-eYu/7MqtRZN1IDttl/UQcSZFkHP7dnvr/X3Vn6Iw6OsPMruQHiVjjomDFCNtd8k2RdjLs0xiz9nq+t3YVBcWPw==", + "dev": true, "dependencies": { - "workbox-core": "6.5.4", - "workbox-routing": "6.5.4", - "workbox-strategies": "6.5.4" + "workbox-core": "6.6.0", + "workbox-routing": "6.6.0", + "workbox-strategies": "6.6.0" } }, "node_modules/workbox-range-requests": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-6.5.4.tgz", - "integrity": "sha512-Je2qR1NXCFC8xVJ/Lux6saH6IrQGhMpDrPXWZWWS8n/RD+WZfKa6dSZwU+/QksfEadJEr/NfY+aP/CXFFK5JFg==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-6.6.0.tgz", + "integrity": "sha512-V3aICz5fLGq5DpSYEU8LxeXvsT//mRWzKrfBOIxzIdQnV/Wj7R+LyJVTczi4CQ4NwKhAaBVaSujI1cEjXW+hTw==", + "dev": true, "dependencies": { - "workbox-core": "6.5.4" + "workbox-core": "6.6.0" } }, "node_modules/workbox-recipes": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-6.5.4.tgz", - "integrity": "sha512-QZNO8Ez708NNwzLNEXTG4QYSKQ1ochzEtRLGaq+mr2PyoEIC1xFW7MrWxrONUxBFOByksds9Z4//lKAX8tHyUA==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-6.6.0.tgz", + "integrity": "sha512-TFi3kTgYw73t5tg73yPVqQC8QQjxJSeqjXRO4ouE/CeypmP2O/xqmB/ZFBBQazLTPxILUQ0b8aeh0IuxVn9a6A==", + "dev": true, "dependencies": { - "workbox-cacheable-response": "6.5.4", - "workbox-core": "6.5.4", - "workbox-expiration": "6.5.4", - "workbox-precaching": "6.5.4", - "workbox-routing": "6.5.4", - "workbox-strategies": "6.5.4" + "workbox-cacheable-response": "6.6.0", + "workbox-core": "6.6.0", + "workbox-expiration": "6.6.0", + "workbox-precaching": "6.6.0", + "workbox-routing": "6.6.0", + "workbox-strategies": "6.6.0" } }, "node_modules/workbox-routing": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-6.5.4.tgz", - "integrity": "sha512-apQswLsbrrOsBUWtr9Lf80F+P1sHnQdYodRo32SjiByYi36IDyL2r7BH1lJtFX8fwNHDa1QOVY74WKLLS6o5Pg==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-6.6.0.tgz", + "integrity": "sha512-x8gdN7VDBiLC03izAZRfU+WKUXJnbqt6PG9Uh0XuPRzJPpZGLKce/FkOX95dWHRpOHWLEq8RXzjW0O+POSkKvw==", + "dev": true, "dependencies": { - "workbox-core": "6.5.4" + "workbox-core": "6.6.0" } }, "node_modules/workbox-strategies": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-6.5.4.tgz", - "integrity": "sha512-DEtsxhx0LIYWkJBTQolRxG4EI0setTJkqR4m7r4YpBdxtWJH1Mbg01Cj8ZjNOO8etqfA3IZaOPHUxCs8cBsKLw==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-6.6.0.tgz", + "integrity": "sha512-eC07XGuINAKUWDnZeIPdRdVja4JQtTuc35TZ8SwMb1ztjp7Ddq2CJ4yqLvWzFWGlYI7CG/YGqaETntTxBGdKgQ==", + "dev": true, "dependencies": { - "workbox-core": "6.5.4" + "workbox-core": "6.6.0" } }, "node_modules/workbox-streams": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-6.5.4.tgz", - "integrity": "sha512-FXKVh87d2RFXkliAIheBojBELIPnWbQdyDvsH3t74Cwhg0fDheL1T8BqSM86hZvC0ZESLsznSYWw+Va+KVbUzg==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-6.6.0.tgz", + "integrity": "sha512-rfMJLVvwuED09CnH1RnIep7L9+mj4ufkTyDPVaXPKlhi9+0czCu+SJggWCIFbPpJaAZmp2iyVGLqS3RUmY3fxg==", + "dev": true, "dependencies": { - "workbox-core": "6.5.4", - "workbox-routing": "6.5.4" + "workbox-core": "6.6.0", + "workbox-routing": "6.6.0" } }, "node_modules/workbox-sw": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-6.5.4.tgz", - "integrity": "sha512-vo2RQo7DILVRoH5LjGqw3nphavEjK4Qk+FenXeUsknKn14eCNedHOXWbmnvP4ipKhlE35pvJ4yl4YYf6YsJArA==" - }, - "node_modules/workbox-webpack-plugin": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/workbox-webpack-plugin/-/workbox-webpack-plugin-6.5.4.tgz", - "integrity": "sha512-LmWm/zoaahe0EGmMTrSLUi+BjyR3cdGEfU3fS6PN1zKFYbqAKuQ+Oy/27e4VSXsyIwAw8+QDfk1XHNGtZu9nQg==", - "dependencies": { - "fast-json-stable-stringify": "^2.1.0", - "pretty-bytes": "^5.4.1", - "upath": "^1.2.0", - "webpack-sources": "^1.4.3", - "workbox-build": "6.5.4" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "webpack": "^4.4.0 || ^5.9.0" - } - }, - "node_modules/workbox-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/workbox-webpack-plugin/node_modules/webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "dependencies": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - } + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-6.6.0.tgz", + "integrity": "sha512-R2IkwDokbtHUE4Kus8pKO5+VkPHD2oqTgl+XJwh4zbF1HyjAbgNmK/FneZHVU7p03XUt9ICfuGDYISWG9qV/CQ==", + "dev": true }, "node_modules/workbox-window": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-6.5.4.tgz", - "integrity": "sha512-HnLZJDwYBE+hpG25AQBO8RUWBJRaCsI9ksQJEp3aCOFCaG5kqaToAYXFRAHxzRluM2cQbGzdQF5rjKPWPA1fug==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-6.6.0.tgz", + "integrity": "sha512-L4N9+vka17d16geaJXXRjENLFldvkWy7JyGxElRD0JvBxvFEd8LOhr+uXCcar/NzAmIBRv9EZ+M+Qr4mOoBITw==", + "dev": true, "dependencies": { "@types/trusted-types": "^2.0.2", - "workbox-core": "6.5.4" + "workbox-core": "6.6.0" } }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true }, - "node_modules/write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dependencies": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "engines": { - "node": ">=10" + "node": ">=0.4" } }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true }, "node_modules/yaml": { "version": "1.10.2", @@ -17361,35 +8230,11 @@ "node": ">= 6" } }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "engines": { - "node": ">=10" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, "engines": { "node": ">=10" }, diff --git a/web/package.json b/web/package.json index 9e919ef7..bb84ff16 100644 --- a/web/package.json +++ b/web/package.json @@ -3,14 +3,17 @@ "version": "1.0.0", "private": true, "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test", - "eject": "react-scripts eject" + "start": "NODE_OPTIONS=\"--enable-source-maps\" vite", + "build": "vite build", + "serve": "vite preview", + "format": "prettier . --write", + "format:check": "prettier . --check", + "lint": "eslint --report-unused-disable-directives --ext .js,.jsx ./src/" }, "dependencies": { - "@emotion/react": "^11.8.2", - "@emotion/styled": "^11.8.1", + "@emotion/cache": "^11.11.0", + "@emotion/react": "^11.11.0", + "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.4.2", "@mui/material": "latest", "dexie": "^3.2.1", @@ -24,10 +27,25 @@ "react-dom": "latest", "react-i18next": "^11.16.2", "react-infinite-scroll-component": "^6.1.0", + "react-remark": "^2.1.0", "react-router-dom": "^6.2.2", - "react-scripts": "^5.0.0", "stacktrace-gps": "^3.0.4", - "stacktrace-js": "^2.0.2" + "stacktrace-js": "^2.0.2", + "stylis": "^4.3.0", + "stylis-plugin-rtl": "^2.1.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.0.0", + "eslint": "^8.41.0", + "eslint-config-airbnb": "^19.0.4", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", + "prettier": "^2.8.8", + "vite": "^4.3.9", + "vite-plugin-pwa": "^0.15.0" }, "browserslist": { "production": [ @@ -40,5 +58,8 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "prettier": { + "printWidth": 140 } } diff --git a/web/public/config.js b/web/public/config.js index 30da6913..63bc97bd 100644 --- a/web/public/config.js +++ b/web/public/config.js @@ -6,12 +6,16 @@ // During web development, you may change values here for rapid testing. var config = { - base_url: window.location.origin, // Change to test against a different server - app_root: "/app", - enable_login: true, - enable_signup: true, - enable_payments: true, - enable_reservations: true, - billing_contact: "", - disallowed_topics: ["docs", "static", "file", "app", "account", "settings", "signup", "login", "v1"] + base_url: window.location.origin, // Change to test against a different server + app_root: "/", + enable_login: true, + enable_signup: true, + enable_payments: false, + enable_reservations: true, + enable_emails: true, + enable_calls: true, + enable_web_push: true, + billing_contact: "", + web_push_public_key: "", + disallowed_topics: ["docs", "static", "file", "app", "account", "settings", "signup", "login", "v1"], }; diff --git a/web/public/home.html b/web/public/home.html deleted file mode 100644 index 43007ca3..00000000 --- a/web/public/home.html +++ /dev/null @@ -1,182 +0,0 @@ - - - - - - ntfy.sh | Send push notifications to your phone via PUT/POST - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

Send push notifications to your phone or desktop via PUT/POST

-

- ntfy (pronounce: notify) is a simple HTTP-based pub-sub notification service. - It allows you to send notifications to your phone or desktop via scripts from any computer, - entirely without signup, cost or setup. It's also open source if you want to run your own. -

-
- - - - - - - -
- -

Publishing messages

-

- Publishing messages can be done via PUT or POST. Topics are created on the fly by subscribing or publishing to them. - Because there is no sign-up, the topic is essentially a password, so pick something that's not easily guessable. -

-

- Here's an example showing how to publish a message using a POST request (via curl -d): -

- - curl -d "Backup successful 😀" ntfy.sh/mytopic - -

- There are more features related to publishing messages: You can set a - notification priority, a title, - and tag messages. - Here's an example using some of them together: -

- - curl \
-   -H "Title: Unauthorized access detected" \
-   -H "Priority: urgent" \
-   -H "Tags: warning,skull" \
-   -d "Remote access to $(hostname) detected. Act right away." \
-   ntfy.sh/mytopic -
-

- Here's what that looks like in the Android app: -

-
- -
Urgent notification with pop-over
-
- -

Subscribe to a topic

-

- You can create and subscribe to a topic either using your phone, - in this web UI, or in your own app by subscribing via the API. -

- -

Subscribe from your phone

-

- Simply get the app and start publishing messages. To learn more about the app, - check out the documentation. -

-

- - - -

-

- Here's a video showing the app in action: -

-
- -
Sending push notifications to your Android phone
-
- -

Subscribe via web app

-

- Subscribe to topics in the web app and receive messages as desktop notification. - It is available at ntfy.sh/app. -

-
- -
ntfy web app, available at ntfy.sh/app
-
- -

Subscribe using the API

-

- There's a super simple API that you can use to integrate your own app. You can consume - a JSON stream, - an SSE/EventSource stream, - a plain text stream, - or via WebSockets. -

-

- Here's an example for JSON. The connection stays open, so you can retrieve messages as they come in: -

- - $ curl -s ntfy.sh/mytopic/json
- {"id":"SLiKI64DOt","time":1635528757,"event":"open","topic":"mytopic"}
- {"id":"hwQ2YpKdmg","time":1635528741,"event":"message","topic":"mytopic","message":"Hi!"}
- {"id":"DGUDShMCsc","time":1635528787,"event":"keepalive","topic":"mytopic"}
- ... -
-

- Here's a short video demonstrating it in action: -

-
- -
Subscribing to the JSON stream with curl
-
- -

Check out the docs!

-

- ntfy has so many more features and you can learn about all of them in the documentation - (I tried my very best to make it the best docs ever 😉, not sure if I succeeded, hehe). -

-
- -
Check out the documentation
-
- -

100% open source & forever free

-

- I love free software, and I'm doing this because it's fun. I have no bad intentions, and I will - never monetize or sell your information. This service will always stay - free and open. - You can read more in the FAQs and in the privacy policy. -

- -
Made with ❤️ by Philipp C. Heckel
-
- - - - diff --git a/web/public/index.html b/web/public/index.html deleted file mode 100644 index 4dd8ef21..00000000 --- a/web/public/index.html +++ /dev/null @@ -1,44 +0,0 @@ - - - - - ntfy web - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - diff --git a/web/public/static/css/app.css b/web/public/static/css/app.css index 12b105a2..213859c0 100644 --- a/web/public/static/css/app.css +++ b/web/public/static/css/app.css @@ -1,10 +1,11 @@ /* web app styling overrides */ -a, a:visited { - color: #338574; +a, +a:visited { + color: #338574; } a:hover { - text-decoration: none; - color: #317f6f; + text-decoration: none; + color: #317f6f; } diff --git a/web/public/static/css/fonts.css b/web/public/static/css/fonts.css index 4245d0f5..2cf00a3c 100644 --- a/web/public/static/css/fonts.css +++ b/web/public/static/css/fonts.css @@ -2,36 +2,32 @@ /* roboto-300 - latin */ @font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 300; - src: local(''), - url('../fonts/roboto-v29-latin-300.woff2') format('woff2'); + font-family: "Roboto"; + font-style: normal; + font-weight: 300; + src: local(""), url("../fonts/roboto-v29-latin-300.woff2") format("woff2"); } /* roboto-regular - latin */ @font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 400; - src: local(''), - url('../fonts/roboto-v29-latin-regular.woff2') format('woff2'); + font-family: "Roboto"; + font-style: normal; + font-weight: 400; + src: local(""), url("../fonts/roboto-v29-latin-regular.woff2") format("woff2"); } /* roboto-500 - latin */ @font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 500; - src: local(''), - url('../fonts/roboto-v29-latin-500.woff2') format('woff2'); + font-family: "Roboto"; + font-style: normal; + font-weight: 500; + src: local(""), url("../fonts/roboto-v29-latin-500.woff2") format("woff2"); } /* roboto-700 - latin */ @font-face { - font-family: 'Roboto'; - font-style: normal; - font-weight: 700; - src: local(''), - url('../fonts/roboto-v29-latin-700.woff2') format('woff2'); + font-family: "Roboto"; + font-style: normal; + font-weight: 700; + src: local(""), url("../fonts/roboto-v29-latin-700.woff2") format("woff2"); } diff --git a/web/public/static/css/home.css b/web/public/static/css/home.css deleted file mode 100644 index feeaa7ee..00000000 --- a/web/public/static/css/home.css +++ /dev/null @@ -1,280 +0,0 @@ -/* general styling */ - -html, body { - font-family: 'Roboto', sans-serif; - font-weight: 400; - font-size: 1.1em; - color: #444; - margin: 0; - padding: 0; -} - -html { - /* prevent scrollbar from repositioning website: - * https://www.w3docs.com/snippets/css/how-to-prevent-scrollbar-from-repositioning-web-page.html */ - overflow-y: scroll; -} - -a, a:visited { - color: #338574; -} - -a:hover { - text-decoration: none; - color: #317f6f; -} - -h1 { - margin-top: 35px; - margin-bottom: 30px; - font-size: 2.5em; - word-wrap: break-word; /* For very long topics */ - padding-right: 40px; /* For the X on the detail page */ - font-weight: 300; - color: #666; -} - -h2 { - margin-top: 30px; - margin-bottom: 5px; - font-size: 1.8em; - font-weight: 300; - color: #333; -} - -h3 { - margin-top: 25px; - margin-bottom: 5px; - font-size: 1.3em; - font-weight: 300; - color: #333; -} - -p { - margin-top: 10px; - margin-bottom: 20px; - line-height: 160%; - font-weight: 400; -} - -p.smallMarginBottom { - margin-bottom: 10px; -} - -b { - font-weight: 500; -} - -tt { - background: #eee; - padding: 2px 7px; - border-radius: 3px; -} - -code { - display: block; - background: #eee; - font-family: monospace; - padding: 20px; - border-radius: 3px; - margin-top: 10px; - margin-bottom: 20px; - overflow-x: auto; - white-space: nowrap; -} - -/* Main page */ - -#main { - max-width: 900px; - margin: 0 auto 50px auto; - padding: 0 10px; -} - -#error { - color: darkred; - font-style: italic; -} - -#ironicCenterTagDontFreakOut { - color: #666; -} - -/* Anchors */ - -.anchor .anchorLink { - color: #ccc; - text-decoration: none; - padding: 0 5px; - visibility: hidden; -} - -.anchor:hover .anchorLink { - visibility: visible; -} - -.anchor .anchorLink:hover { - color: #338574; - visibility: visible; -} - -/* Figures */ - -figure { - text-align: center; -} - -figure img, figure video { - filter: drop-shadow(3px 3px 3px #ccc); - border-radius: 7px; - max-width: 100%; -} - -figure video { - width: 100%; - max-height: 450px; -} - -figcaption { - text-align: center; - font-style: italic; - padding-top: 10px; -} - -/* Screenshots */ - -#screenshots { - text-align: center; -} - -#screenshots img { - height: 190px; - margin: 3px; - border-radius: 5px; - filter: drop-shadow(2px 2px 2px #ddd); -} - -#screenshots .nowrap { - white-space: nowrap; -} - -/* Lightbox; thanks to https://yossiabramov.com/blog/vanilla-js-lightbox */ - -.lightbox { - opacity: 0; - visibility: hidden; - position: fixed; - left:0; - right: 0; - top: 0; - bottom: 0; - z-index: -1; - display: flex; - align-items: center; - justify-content: center; - transition: all 0.15s ease-in; -} - -.lightbox.show { - background-color: rgba(0,0,0, 0.75); - opacity: 1; - visibility: visible; - z-index: 1000; -} - -.lightbox img { - max-width: 90%; - max-height: 90%; - filter: drop-shadow(5px 5px 10px #222); - border-radius: 5px; -} - -.lightbox .close-lightbox { - cursor: pointer; - position: absolute; - top: 30px; - right: 30px; - width: 20px; - height: 20px; -} - -.lightbox .close-lightbox::after, -.lightbox .close-lightbox::before { - content: ''; - width: 3px; - height: 20px; - background-color: #ddd; - position: absolute; - border-radius: 5px; - transform: rotate(45deg); -} - -.lightbox .close-lightbox::before { - transform: rotate(-45deg); -} - -.lightbox .close-lightbox:hover::after, -.lightbox .close-lightbox:hover::before { - background-color: #fff; -} - -/* Header */ - -#header { - background: #338574; - height: 130px; -} - -#header #headerBox { - max-width: 900px; - margin: 0 auto; - padding: 0 10px; -} - -#header #logo { - margin-top: 23px; - float: left; -} - -#header #name { - float: left; - color: white; - font-size: 2.6em; - font-weight: 300; - margin: 35px 0 0 20px; -} - -#header ol { - list-style-type: none; - float: right; - margin-top: 80px; -} - -#header ol li { - display: inline-block; - margin: 0 10px; - font-weight: 400; -} - -#header ol li a, nav ol li a:visited { - color: white; - text-decoration: none; -} - -#header ol li a:hover { - text-decoration: underline; -} - -li { - padding: 4px 0; - margin: 4px 0; - font-size: 0.9em; -} - - -/* Hide top menu SMALL SCREEN */ -@media only screen and (max-width: 780px) { - #header ol { - display: none; - } -} diff --git a/web/public/static/images/apple-touch-icon.png b/web/public/static/images/apple-touch-icon.png new file mode 100644 index 00000000..8f890509 Binary files /dev/null and b/web/public/static/images/apple-touch-icon.png differ diff --git a/web/public/static/images/favicon.ico b/web/public/static/images/favicon.ico new file mode 100644 index 00000000..857fa54c Binary files /dev/null and b/web/public/static/images/favicon.ico differ diff --git a/web/public/static/images/mask-icon.svg b/web/public/static/images/mask-icon.svg new file mode 100644 index 00000000..32fced6d --- /dev/null +++ b/web/public/static/images/mask-icon.svg @@ -0,0 +1,20 @@ + + + + + + + diff --git a/web/public/static/img/ntfy.png b/web/public/static/images/ntfy.png similarity index 100% rename from web/public/static/img/ntfy.png rename to web/public/static/images/ntfy.png diff --git a/web/public/static/images/pwa-192x192.png b/web/public/static/images/pwa-192x192.png new file mode 100644 index 00000000..8aaebcc4 Binary files /dev/null and b/web/public/static/images/pwa-192x192.png differ diff --git a/web/public/static/images/pwa-512x512.png b/web/public/static/images/pwa-512x512.png new file mode 100644 index 00000000..d9003a19 Binary files /dev/null and b/web/public/static/images/pwa-512x512.png differ diff --git a/web/public/static/img/android-video-overview.mp4 b/web/public/static/img/android-video-overview.mp4 deleted file mode 100644 index cf295099..00000000 Binary files a/web/public/static/img/android-video-overview.mp4 and /dev/null differ diff --git a/web/public/static/img/android-video-subscribe-api.mp4 b/web/public/static/img/android-video-subscribe-api.mp4 deleted file mode 100644 index d73e5c6e..00000000 Binary files a/web/public/static/img/android-video-subscribe-api.mp4 and /dev/null differ diff --git a/web/public/static/img/badge-appstore.png b/web/public/static/img/badge-appstore.png deleted file mode 100644 index 0b4ce1c0..00000000 Binary files a/web/public/static/img/badge-appstore.png and /dev/null differ diff --git a/web/public/static/img/badge-fdroid.png b/web/public/static/img/badge-fdroid.png deleted file mode 100644 index 9464d38a..00000000 Binary files a/web/public/static/img/badge-fdroid.png and /dev/null differ diff --git a/web/public/static/img/badge-googleplay.png b/web/public/static/img/badge-googleplay.png deleted file mode 100644 index 36036d8b..00000000 Binary files a/web/public/static/img/badge-googleplay.png and /dev/null differ diff --git a/web/public/static/img/favicon.png b/web/public/static/img/favicon.png deleted file mode 100644 index 92312fea..00000000 Binary files a/web/public/static/img/favicon.png and /dev/null differ diff --git a/web/public/static/img/screenshot-docs.png b/web/public/static/img/screenshot-docs.png deleted file mode 100644 index 4345ded4..00000000 Binary files a/web/public/static/img/screenshot-docs.png and /dev/null differ diff --git a/web/public/static/img/screenshot-phone-add.jpg b/web/public/static/img/screenshot-phone-add.jpg deleted file mode 100644 index f728ec99..00000000 Binary files a/web/public/static/img/screenshot-phone-add.jpg and /dev/null differ diff --git a/web/public/static/img/screenshot-phone-popover.png b/web/public/static/img/screenshot-phone-popover.png deleted file mode 100644 index 31d15152..00000000 Binary files a/web/public/static/img/screenshot-phone-popover.png and /dev/null differ diff --git a/web/public/static/js/home.js b/web/public/static/js/home.js deleted file mode 100644 index 80b14055..00000000 --- a/web/public/static/js/home.js +++ /dev/null @@ -1,84 +0,0 @@ - -/* All the things */ - -let currentUrl = window.location.hostname; -if (window.location.port) { - currentUrl += ':' + window.location.port -} - -/* Screenshots */ -const lightbox = document.getElementById("lightbox"); - -const showScreenshotOverlay = (e, el, index) => { - lightbox.classList.add('show'); - document.addEventListener('keydown', nextScreenshotKeyboardListener); - return showScreenshot(e, index); -}; - -const showScreenshot = (e, index) => { - const actualIndex = resolveScreenshotIndex(index); - lightbox.innerHTML = '
' + screenshots[actualIndex].innerHTML; - lightbox.querySelector('img').onclick = (e) => { return showScreenshot(e,actualIndex+1); }; - currentScreenshotIndex = actualIndex; - e.stopPropagation(); - return false; -}; - -const nextScreenshot = (e) => { - return showScreenshot(e, currentScreenshotIndex+1); -}; - -const previousScreenshot = (e) => { - return showScreenshot(e, currentScreenshotIndex-1); -}; - -const resolveScreenshotIndex = (index) => { - if (index < 0) { - return screenshots.length - 1; - } else if (index > screenshots.length - 1) { - return 0; - } - return index; -}; - -const hideScreenshotOverlay = (e) => { - lightbox.classList.remove('show'); - document.removeEventListener('keydown', nextScreenshotKeyboardListener); -}; - -const nextScreenshotKeyboardListener = (e) => { - switch (e.keyCode) { - case 37: - previousScreenshot(e); - break; - case 39: - nextScreenshot(e); - break; - } -}; - -let currentScreenshotIndex = 0; -const screenshots = [...document.querySelectorAll("#screenshots a")]; -screenshots.forEach((el, index) => { - el.onclick = (e) => { return showScreenshotOverlay(e, el, index); }; -}); - -lightbox.onclick = hideScreenshotOverlay; - -// Add anchor links -document.querySelectorAll('.anchor').forEach((el) => { - if (el.hasAttribute('id')) { - const id = el.getAttribute('id'); - const anchor = document.createElement('a'); - anchor.innerHTML = `#`; - el.appendChild(anchor); - } -}); - -// Change ntfy.sh url and protocol to match self-hosted one -document.querySelectorAll('.ntfyUrl').forEach((el) => { - el.innerHTML = currentUrl; -}); -document.querySelectorAll('.ntfyProtocol').forEach((el) => { - el.innerHTML = window.location.protocol + "//"; -}); diff --git a/web/public/static/langs/ar.json b/web/public/static/langs/ar.json index a3919ffd..ce1d88bb 100644 --- a/web/public/static/langs/ar.json +++ b/web/public/static/langs/ar.json @@ -12,8 +12,8 @@ "nav_button_publish_message": "نشر الإشعار", "nav_button_subscribe": "اشترك في الموضوع", "nav_button_connecting": "جارٍ الاتصال", - "alert_grant_title": "تم تعطيل الإشعارات", - "alert_grant_description": "امنح متصفحك الإذن لعرض إشعارات سطح المكتب.", + "alert_notification_permission_required_title": "تم تعطيل الإشعارات", + "alert_notification_permission_required_description": "امنح متصفحك الإذن لعرض إشعارات سطح المكتب.", "notifications_list": "قائمة الإشعارات", "notifications_list_item": "إشعار", "notifications_mark_read": "وضع علامة كمقروء", @@ -44,7 +44,7 @@ "notifications_copied_to_clipboard": "تم نسخه إلى الحافظة", "action_bar_toggle_mute": "كتم / إلغاء كتم الإشعارات", "action_bar_toggle_action_menu": "فتح/إغلاق قائمة الإجراءات", - "alert_grant_button": "امنح الآن", + "alert_notification_permission_required_button": "امنح الآن", "notifications_attachment_open_button": "فتح المرفق", "notifications_attachment_copy_url_title": "نسخ عنوان URL للمرفق إلى الحافظة", "notifications_click_copy_url_title": "انسخ رابط URL إلى الحافظة", @@ -152,7 +152,7 @@ "publish_dialog_chip_delay_label": "تأخير التسليم", "subscribe_dialog_login_description": "هذا الموضوع محمي بكلمة مرور. الرجاء إدخال اسم المستخدم وكلمة المرور للاشتراك.", "subscribe_dialog_subscribe_button_cancel": "إلغاء", - "subscribe_dialog_login_button_back": "العودة", + "common_back": "الرجوع", "prefs_notifications_sound_play": "تشغيل الصوت المحدد", "prefs_notifications_min_priority_title": "أولوية دنيا", "prefs_notifications_min_priority_max_only": "الأولوية القصوى فقط", @@ -225,7 +225,7 @@ "account_tokens_table_expires_header": "تنتهي مدة صلاحيته في", "account_tokens_table_never_expires": "لا تنتهي صلاحيتها أبدا", "account_tokens_table_current_session": "جلسة المتصفح الحالية", - "account_tokens_table_copy_to_clipboard": "انسخ إلى الحافظة", + "common_copy_to_clipboard": "انسخ إلى الحافظة", "account_tokens_table_cannot_delete_or_edit": "لا يمكن تحرير أو حذف الرمز المميز للجلسة الحالية", "account_tokens_table_create_token_button": "إنشاء رمز مميز للوصول", "account_tokens_table_last_origin_tooltip": "من عنوان IP {{ip}}، انقر للبحث", @@ -329,5 +329,6 @@ "publish_dialog_attachment_limits_quota_reached": "يتجاوز الحصة، {{remainingBytes}} متبقية", "account_basics_tier_paid_until": "تم دفع مبلغ الاشتراك إلى غاية {{date}}، وسيتم تجديده تِلْقائيًا", "account_basics_tier_canceled_subscription": "تم إلغاء اشتراكك وسيتم إعادته إلى مستوى حساب مجاني بداية مِن {{date}}.", - "account_delete_dialog_billing_warning": "إلغاء حسابك أيضاً يلغي اشتراكك في الفوترة فوراً ولن تتمكن من الوصول إلى لوح الفوترة بعد الآن." + "account_delete_dialog_billing_warning": "إلغاء حسابك أيضاً يلغي اشتراكك في الفوترة فوراً ولن تتمكن من الوصول إلى لوح الفوترة بعد الآن.", + "nav_upgrade_banner_description": "حجز المواضيع والمزيد من الرسائل ورسائل البريد الإلكتروني والمرفقات الأكبر حجمًا" } diff --git a/web/public/static/langs/bg.json b/web/public/static/langs/bg.json index 69523036..c4cdb102 100644 --- a/web/public/static/langs/bg.json +++ b/web/public/static/langs/bg.json @@ -1,9 +1,9 @@ { "action_bar_clear_notifications": "Премахване на известия", - "alert_grant_description": "Разрешете на мрежовия четец да показва известия.", + "alert_notification_permission_required_description": "Разрешете на мрежовия четец да показва известия.", "notifications_attachment_copy_url_title": "Копиране на адреса на прикачения файл", "notifications_example": "Пример", - "notifications_no_subscriptions_title": "Липсват абонаменти", + "notifications_no_subscriptions_title": "Липсват абонаменти.", "nav_topics_title": "Абонаменти", "action_bar_send_test_notification": "Пробно известие", "action_bar_unsubscribe": "Отписване", @@ -47,8 +47,8 @@ "nav_button_settings": "Настройки", "nav_button_documentation": "Ръководство", "nav_button_subscribe": "Абониране за тема", - "alert_grant_title": "Известията са изключени", - "alert_grant_button": "Разрешаване", + "alert_notification_permission_required_title": "Известията са изключени", + "alert_notification_permission_required_button": "Разрешаване", "notifications_tags": "Етикети", "nav_button_publish_message": "Изпращане", "alert_not_supported_title": "Не се поддържат известия", @@ -60,8 +60,8 @@ "notifications_click_copy_url_button": "Копиране на препратка", "notifications_click_open_button": "Отваряне", "notifications_click_copy_url_title": "Копиране на препратката в междинната памет", - "notifications_none_for_topic_title": "Липсват известия в темата", - "notifications_none_for_any_title": "Липсват известия", + "notifications_none_for_topic_title": "Липсват известия в темата.", + "notifications_none_for_any_title": "Липсват известия.", "notifications_none_for_topic_description": "За да изпратите известия в тази тема направете заявка чрез методите PUT или POST към адреса й.", "notifications_none_for_any_description": "За да изпратите известия в тема направете заявка чрез методите PUT или POST към адреса ѝ. Ето пример с една от вашите теми.", "notifications_no_subscriptions_description": "Щракнете върху „{{linktext}}“, за да създадете тема или да се абонирате. След това като направите заявка чрез методите PUT или POST ще ги получите тук.", @@ -104,7 +104,7 @@ "subscribe_dialog_subscribe_topic_placeholder": "Име на темата, напр. phils_alerts", "subscribe_dialog_subscribe_use_another_label": "Използване на друг сървър", "subscribe_dialog_login_username_label": "Потребител, напр. phil", - "subscribe_dialog_login_button_back": "Назад", + "common_back": "Назад", "subscribe_dialog_subscribe_button_cancel": "Отказ", "subscribe_dialog_login_description": "Темата е защитена. За да се абонирате въведете потребител и парола.", "subscribe_dialog_subscribe_button_subscribe": "Абониране", @@ -278,5 +278,60 @@ "account_basics_tier_upgrade_button": "Надграждане до Pro", "account_usage_messages_title": "Публикувани съобщения", "account_tokens_table_last_access_header": "Последен достъп", - "account_basics_tier_payment_overdue": "Имате просрочено задължение. Обновете начина на плащане, защото в противен случай скоро профилът ви ще загуби предимствата на абонамента." + "account_basics_tier_payment_overdue": "Имате просрочено задължение. Обновете начина на плащане, защото в противен случай скоро профилът ви ще загуби предимствата на абонамента.", + "account_usage_basis_ip_description": "Статистиката и ограниченията на използване се отчитат по IP адрес, така че може да бъдат споделени с други потребители. Показаните по-горе ограничения са приблизителни и се основават на съществуващите ограничения на използване.", + "account_delete_dialog_description": "Това действие ще доведе до безвъзвратното изтриване на профила ви, включително на всички данни, които се съхраняват на сървъра. След изтриването потребителското ви име няма да бъде достъпно в продължение на 7 дни. Ако наистина искате да продължите, потвърдете с паролата си в полето по-долу.", + "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} резервирана тема", + "account_upgrade_dialog_interval_yearly_discount_save_up_to": "спестете до {{discount}}%", + "account_delete_dialog_billing_warning": "Изтриването на профила незабавно отменя и платения абонамент. Няма да имате достъп до таблото за плащания.", + "account_upgrade_dialog_cancel_warning": "Това действие ще прекрати абонамента и ще промени профила ви на неплатен на {{date}}. На тази дата резервираните теми, както и пазените на сървъра съобщения, ще бъдат премахнати.", + "account_upgrade_dialog_proration_info": "Преизчисляване на плащания: При надграждане между платени планове разликата в цената ще бъде начислена незабавно. При преминаване към по-евтин план надплатената сума ще бъде използвана за плащане за бъдещи периоди.", + "account_basics_tier_manage_billing_button": "Управление на плащанията", + "account_basics_tier_canceled_subscription": "Абонаментът е прекратен и профилът ще бъде променен на неплатен на {{date}}.", + "account_basics_phone_numbers_dialog_verify_button_sms": "Изпращане на SMS", + "account_basics_phone_numbers_dialog_verify_button_call": "Обаждане до мен", + "account_upgrade_dialog_tier_features_calls_other": "{{calls}} телефонни обаждания на ден", + "common_copy_to_clipboard": "Копиране в междинната памет", + "publish_dialog_call_label": "Телефонно обаждане", + "publish_dialog_call_reset": "Премахване на телефонно обаждане", + "publish_dialog_chip_call_label": "Телефонно обаждане", + "account_basics_phone_numbers_dialog_description": "За да възползвате от услугата известяване чрез телефонно обаждане, трябва да добавите и потвърдите поне един телефонен номер. Проверката може да бъде извършена чрез SMS или телефонно обаждане.", + "account_basics_phone_numbers_title": "Телефонни номера", + "account_basics_phone_numbers_dialog_number_placeholder": "напр. +1222333444", + "account_basics_phone_numbers_dialog_number_label": "Телефонен номер", + "account_basics_phone_numbers_dialog_title": "Добавяне на телефонен номер", + "account_basics_phone_numbers_copied_to_clipboard": "Телефонният номер е копиран в междинната памет", + "account_basics_phone_numbers_no_phone_numbers_yet": "Все още няма телефонни номера", + "account_basics_phone_numbers_description": "За известяване чрез телефонно обаждане", + "publish_dialog_call_item": "Обаждане на телефонен номер {{number}}", + "publish_dialog_chip_call_no_verified_numbers_tooltip": "Няма потвърдени телефонни номера", + "account_basics_phone_numbers_dialog_channel_call": "Обаждане", + "account_basics_phone_numbers_dialog_channel_sms": "SMS", + "account_basics_phone_numbers_dialog_check_verification_button": "Код за потвърждаване", + "account_basics_phone_numbers_dialog_code_placeholder": "напр. 123456", + "account_basics_phone_numbers_dialog_code_label": "Код за потвърждение", + "account_usage_calls_none": "С този профил не могат да се извършват телефонни обаждания", + "account_usage_calls_title": "Извършени телефонни обаждания", + "account_upgrade_dialog_tier_features_no_calls": "Без телефонни обаждания", + "account_upgrade_dialog_tier_features_messages_one": "{{messages}} съобщение на ден", + "account_upgrade_dialog_tier_features_messages_other": "{{messages}} съобщения на ден", + "account_upgrade_dialog_tier_features_emails_one": "{{emails}} ел. писмо на ден", + "account_upgrade_dialog_tier_features_emails_other": "{{emails}} ел. писма на ден", + "account_upgrade_dialog_tier_features_calls_one": "{{calls}} телефонни обаждания на ден", + "account_usage_attachment_storage_description": "{{filesize}} на файл, изтриване след {{expiry}}", + "account_upgrade_dialog_billing_contact_email": "За въпроси относно плащанията се свържете с нас.", + "account_upgrade_dialog_tier_current_label": "Текущо", + "account_upgrade_dialog_billing_contact_website": "За въпроси относно плащанията се обърнете към страницата.", + "account_upgrade_dialog_button_cancel_subscription": "Прекратяване на абонамент", + "account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} на файл", + "account_upgrade_dialog_reservations_warning_one": "Избраното ниво разрешава по-малко резервирани теми, от колкото текущото. Преди промяна на нивото изтрийте най-малко една резервирана тема. Можете да премахвате теми в Настройки.", + "account_tokens_title": "Кодове за достъп", + "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} на година. Плаща се всеки месец.", + "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} плащане на година. Спестявате {{save}}.", + "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} общ обем", + "account_upgrade_dialog_tier_price_per_month": "на месец", + "account_upgrade_dialog_button_pay_now": "Плащане и абониране", + "account_upgrade_dialog_tier_selected_label": "Избрано", + "account_upgrade_dialog_button_update_subscription": "Премяна на абонамент", + "account_upgrade_dialog_reservations_warning_other": "Избраното ниво разрешава по-малко резервирани теми, от колкото текущото. Преди промяна на нивото изтрийте най-малко {{count}} резервирани теми. Можете да премахвате теми в Настройки." } diff --git a/web/public/static/langs/cs.json b/web/public/static/langs/cs.json index 423259a3..cd1f851b 100644 --- a/web/public/static/langs/cs.json +++ b/web/public/static/langs/cs.json @@ -11,9 +11,9 @@ "nav_button_documentation": "Dokumentace", "nav_button_publish_message": "Odeslat oznámení", "nav_button_subscribe": "Přihlásit se k odběru tématu", - "alert_grant_title": "Oznámení jsou zakázána", - "alert_grant_description": "Udělte prohlížeči oprávnění k zobrazování oznámení na ploše.", - "alert_grant_button": "Udělit nyní", + "alert_notification_permission_required_title": "Oznámení jsou zakázána", + "alert_notification_permission_required_description": "Udělte prohlížeči oprávnění k zobrazování oznámení na ploše.", + "alert_notification_permission_required_button": "Udělit nyní", "alert_not_supported_title": "Oznámení nejsou podporována", "alert_not_supported_description": "Oznámení nejsou ve vašem prohlížeči podporována.", "notifications_copied_to_clipboard": "Zkopírováno do schránky", @@ -91,7 +91,7 @@ "subscribe_dialog_subscribe_button_subscribe": "Přihlásit odběr", "subscribe_dialog_login_username_label": "Uživatelské jméno, např. phil", "subscribe_dialog_login_password_label": "Heslo", - "subscribe_dialog_login_button_back": "Zpět", + "common_back": "Zpět", "subscribe_dialog_login_button_login": "Přihlásit se", "subscribe_dialog_error_user_not_authorized": "Uživatel {{username}} není autorizován", "subscribe_dialog_error_user_anonymous": "anonymně", @@ -305,7 +305,7 @@ "account_tokens_table_expires_header": "Vyprší", "account_tokens_table_never_expires": "Nikdy nevyprší", "account_tokens_table_current_session": "Současná relace prohlížeče", - "account_tokens_table_copy_to_clipboard": "Kopírování do schránky", + "common_copy_to_clipboard": "Kopírování do schránky", "account_tokens_table_label_header": "Popisek", "account_tokens_table_cannot_delete_or_edit": "Nelze upravit nebo odstranit aktuální token relace", "account_tokens_table_create_token_button": "Vytvořit přístupový token", @@ -352,5 +352,33 @@ "account_upgrade_dialog_interval_yearly": "Roční", "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} za rok. Účtuje se měsíčně.", "account_upgrade_dialog_billing_contact_email": "V případě dotazů týkajících se fakturace nás prosím kontaktujte přímo.", - "account_upgrade_dialog_billing_contact_website": "Otázky týkající se fakturace naleznete na našich webových stránkách." + "account_upgrade_dialog_billing_contact_website": "Otázky týkající se fakturace naleznete na našich webových stránkách.", + "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} rezervované téma", + "account_upgrade_dialog_tier_features_messages_one": "{{messages}} denní zpráva", + "account_upgrade_dialog_tier_features_emails_one": "{{emails}} denní e-mail", + "publish_dialog_call_label": "Telefonát", + "publish_dialog_call_reset": "Odstranit telefonát", + "publish_dialog_chip_call_label": "Telefonát", + "account_basics_phone_numbers_title": "Telefonní čísla", + "account_basics_phone_numbers_dialog_description": "Pro oznámení prostřednictvím tel. hovoru, musíte přidat a ověřit alespoň jedno telefonní číslo. Ověření lze provést pomocí SMS nebo telefonátu.", + "account_basics_phone_numbers_description": "K oznámení telefonátem", + "account_basics_phone_numbers_no_phone_numbers_yet": "Zatím žádná telefonní čísla", + "account_basics_phone_numbers_copied_to_clipboard": "Telefonní číslo zkopírováno do schránky", + "publish_dialog_chip_call_no_verified_numbers_tooltip": "Žádná ověřená telefonní čísla", + "publish_dialog_call_item": "Vytočit číslo {{number}}", + "account_basics_phone_numbers_dialog_channel_sms": "SMS", + "account_basics_phone_numbers_dialog_title": "Přidat telefonní číslo", + "account_basics_phone_numbers_dialog_number_label": "Telefonní číslo", + "account_basics_phone_numbers_dialog_code_placeholder": "např. 123456", + "account_basics_phone_numbers_dialog_code_label": "Ověřovací kód", + "account_usage_calls_none": "S tímto účtem nelze uskutečňovat žádné telefonní hovory", + "account_basics_phone_numbers_dialog_check_verification_button": "Potvrdit kód", + "account_basics_phone_numbers_dialog_number_placeholder": "např. +1222333444", + "account_basics_phone_numbers_dialog_verify_button_sms": "Odeslat SMS", + "account_basics_phone_numbers_dialog_verify_button_call": "Zavolat mi", + "account_basics_phone_numbers_dialog_channel_call": "Zavolat", + "account_usage_calls_title": "Uskutečněné telefonáty", + "account_upgrade_dialog_tier_features_no_calls": "Žádné telefonní hovory", + "account_upgrade_dialog_tier_features_calls_one": "{{calls}} denní telefonní hovor", + "account_upgrade_dialog_tier_features_calls_other": "{{calls}} denních telefonních hovorů" } diff --git a/web/public/static/langs/cy.json b/web/public/static/langs/cy.json new file mode 100644 index 00000000..da6c9b41 --- /dev/null +++ b/web/public/static/langs/cy.json @@ -0,0 +1,48 @@ +{ + "notifications_delete": "Dileu", + "action_bar_sign_in": "Mewngofnodi", + "notifications_copied_to_clipboard": "Wedi'i gopio i'r clipfwrdd", + "common_cancel": "Canslo", + "nav_button_account": "Cyfrif", + "common_save": "Arbed", + "common_add": "Ychwanegu", + "signup_title": "Creu cyfrif ntfy", + "signup_form_username": "Enw defnyddiwr", + "signup_form_password": "Cyfrinair", + "action_bar_logo_alt": "logo ntfy", + "action_bar_settings": "Gosodiadau", + "action_bar_profile_title": "Proffil", + "action_bar_profile_logout": "Allgofnodi", + "message_bar_publish": "Cyhoeddi neges", + "notifications_attachment_copy_url_button": "Copio URL", + "notifications_attachment_open_title": "Ewch i {{url}}", + "publish_dialog_base_url_label": "URL y Gwasanaeth", + "publish_dialog_priority_high": "Blaenoriaeth uchel", + "publish_dialog_title_label": "Teitl", + "publish_dialog_message_label": "Neges", + "publish_dialog_attach_label": "URL Atodiad", + "publish_dialog_filename_label": "Enw ffeil", + "publish_dialog_filename_placeholder": "Enw ffeil yr atodiad", + "action_bar_account": "Cyfrif", + "action_bar_unsubscribe": "Dad-danysgrifio", + "login_title": "Mewngofnodi i'ch cyfrif ntfy", + "login_form_button_submit": "Mewngofnodi", + "action_bar_change_display_name": "Newid enw arddangos", + "action_bar_profile_settings": "Gosodiadau", + "nav_button_settings": "Gosodiadau", + "nav_button_documentation": "Dogfennaeth", + "alert_not_supported_context_description": "Dim ond dros HTTPS y gellir derbyn cyhoeddiadau. Mae hyn yn gyfyngiad ar yr API Notifications.", + "notifications_attachment_open_button": "Agor atodiad", + "notifications_attachment_file_document": "dogfen arall", + "notifications_click_open_button": "Agor linc", + "publish_dialog_base_url_placeholder": "URL y Gwasanaeth, e.e. https://example.com", + "publish_dialog_attach_placeholder": "Atodi ffeil drwy URL, e.e. https://f-droid.org/F-Droid.apk", + "notifications_click_copy_url_button": "Copio linc", + "notifications_actions_open_url_title": "Ewch i {{url}}", + "publish_dialog_email_label": "Ebost", + "signup_form_confirm_password": "Cadarnhau cyfrinair", + "signup_form_button_submit": "Cofrestru", + "common_back": "Yn ôl", + "common_copy_to_clipboard": "Copio i'r clipfwrdd", + "signup_already_have_account": "Gyda chyfrif yn barod? Mewngofnodi!" +} diff --git a/web/public/static/langs/da.json b/web/public/static/langs/da.json index d60c56c2..21e7de76 100644 --- a/web/public/static/langs/da.json +++ b/web/public/static/langs/da.json @@ -40,8 +40,8 @@ "nav_button_all_notifications": "Alle notifikationer", "nav_button_connecting": "forbinder", "nav_upgrade_banner_label": "Opgrader til ntfy Pro", - "alert_grant_title": "Notifikationer er deaktiveret", - "alert_grant_description": "Giv din browser tilladelse til at vise skrivebordsnotifikationer.", + "alert_notification_permission_required_title": "Notifikationer er deaktiveret", + "alert_notification_permission_required_description": "Giv din browser tilladelse til at vise skrivebordsnotifikationer.", "alert_not_supported_title": "Notifikationer understøttes ikke", "alert_not_supported_description": "Notifikationer understøttes ikke i din browser.", "alert_not_supported_context_description": "Notifikationer understøttes kun via HTTPS. Dette skyldes en begrænsning i Notifications API.", @@ -91,7 +91,7 @@ "publish_dialog_delay_label": "Forsinkelse", "publish_dialog_button_send": "Send", "subscribe_dialog_subscribe_button_subscribe": "Tilmeld", - "subscribe_dialog_login_button_back": "Tilbage", + "common_back": "Tilbage", "subscribe_dialog_login_username_label": "Brugernavn, f.eks. phil", "account_basics_title": "Konto", "subscribe_dialog_error_topic_already_reserved": "Emnet er allerede reserveret", @@ -209,7 +209,7 @@ "subscribe_dialog_subscribe_use_another_label": "Brug en anden server", "account_basics_tier_upgrade_button": "Opgrader til Pro", "account_upgrade_dialog_tier_features_messages_other": "{{messages}} daglige beskeder", - "account_tokens_table_copy_to_clipboard": "Kopier til udklipsholder", + "common_copy_to_clipboard": "Kopier til udklipsholder", "prefs_reservations_edit_button": "Rediger emneadgang", "account_upgrade_dialog_title": "Skift kontoniveau", "account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} reserverede emner", diff --git a/web/public/static/langs/de.json b/web/public/static/langs/de.json index 0aee2718..a43fb7da 100644 --- a/web/public/static/langs/de.json +++ b/web/public/static/langs/de.json @@ -5,7 +5,7 @@ "nav_button_documentation": "Dokumentation", "nav_button_publish_message": "Benachrichtigung senden", "nav_button_subscribe": "Thema abonnieren", - "alert_grant_title": "Benachrichtigungen sind deaktiviert", + "alert_notification_permission_required_title": "Benachrichtigungen sind deaktiviert", "publish_dialog_base_url_label": "Service-URL", "publish_dialog_details_examples_description": "Beispiele und ausführliche Informationen zu allen Optionen findest Du in der Dokumentation.", "publish_dialog_attached_file_filename_placeholder": "Dateiname des Anhangs", @@ -25,13 +25,13 @@ "notifications_click_copy_url_title": "Link-URL in Zwischenablage kopieren", "publish_dialog_priority_low": "Niedrige Priorität", "publish_dialog_message_label": "Nachricht", - "action_bar_unsubscribe": "Von Thema abmelden", + "action_bar_unsubscribe": "Abmelden", "notifications_copied_to_clipboard": "In Zwischenablage kopiert", "notifications_loading": "Benachrichtigungen werden geladen …", "notifications_attachment_open_title": "Gehe zu {{url}}", "notifications_none_for_any_title": "Du hast keine Benachrichtigungen empfangen.", "action_bar_send_test_notification": "Test-Benachrichtigung senden", - "alert_grant_description": "Dem Browser erlauben, Desktop-Benachrichtigungen anzuzeigen.", + "alert_notification_permission_required_description": "Dem Browser erlauben, Desktop-Benachrichtigungen anzuzeigen.", "notifications_tags": "Tags", "message_bar_type_message": "Gib hier eine Nachricht ein", "message_bar_error_publishing": "Fehler beim Senden der Benachrichtigung", @@ -39,7 +39,7 @@ "alert_not_supported_description": "Benachrichtigungen werden von Deinem Browser nicht unterstützt.", "action_bar_settings": "Einstellungen", "action_bar_clear_notifications": "Alle Benachrichtigungen löschen", - "alert_grant_button": "Jetzt erlauben", + "alert_notification_permission_required_button": "Jetzt erlauben", "notifications_none_for_topic_title": "Du hast für dieses Thema noch keine Benachrichtigungen empfangen.", "notifications_click_open_button": "Link öffnen", "notifications_more_details": "Ausführlichere Informationen findest Du auf der Website und in der Dokumentation.", @@ -94,7 +94,7 @@ "publish_dialog_delay_placeholder": "Auslieferung verzögern, z.B. {{unixTimestamp}}, {{relativeTime}}, oder \"{{naturalLanguage}}\" (nur Englisch)", "prefs_appearance_title": "Darstellung", "subscribe_dialog_login_password_label": "Kennwort", - "subscribe_dialog_login_button_back": "Zurück", + "common_back": "Zurück", "publish_dialog_chip_attach_url_label": "Datei von URL anhängen", "publish_dialog_chip_delay_label": "Auslieferung verzögern", "publish_dialog_chip_topic_label": "Thema ändern", @@ -154,7 +154,7 @@ "notifications_actions_not_supported": "Diese Aktion wird in der Web-App nicht unterstützt", "notifications_actions_http_request_title": "Sende HTTP {{method}} an {{url}}", "action_bar_show_menu": "Menü anzeigen", - "action_bar_toggle_mute": "Stummschaltung der Benachrichtigungen an/aus", + "action_bar_toggle_mute": "Stummschaltung an/aus", "message_bar_show_dialog": "Dialog zur Veröffentlichung anzeigen", "message_bar_publish": "Benachrichtigung veröffentlichen", "nav_button_connecting": "verbinde", @@ -284,7 +284,7 @@ "account_tokens_table_expires_header": "Verfällt", "account_tokens_table_never_expires": "Verfällt nie", "account_tokens_table_current_session": "Aktuelle Browser-Sitzung", - "account_tokens_table_copy_to_clipboard": "In die Zwischenablage kopieren", + "common_copy_to_clipboard": "In die Zwischenablage kopieren", "account_tokens_table_copied_to_clipboard": "Access-Token kopiert", "account_tokens_table_cannot_delete_or_edit": "Aktuelles Token kann nicht bearbeitet oder gelöscht werden", "account_tokens_table_create_token_button": "Access-Token erzeugen", @@ -310,7 +310,7 @@ "prefs_reservations_delete_button": "Zugriff auf Thema zurücksetzen", "prefs_reservations_table": "Übersicht reservierter Themen", "prefs_reservations_table_topic_header": "Thema", - "prefs_reservations_table_everyone_deny_all": "Nur kann veröffentlichen und lesen", + "prefs_reservations_table_everyone_deny_all": "Nur ich kann veröffentlichen und lesen", "prefs_reservations_table_everyone_write_only": "Ich kann veröffentlichen und lesen, jeder kann veröffentlichen", "prefs_reservations_table_not_subscribed": "Nicht abonniert", "prefs_reservations_table_click_to_subscribe": "Klicken um zu abonnieren", @@ -352,5 +352,33 @@ "account_basics_tier_interval_monthly": "monatlich", "account_upgrade_dialog_interval_monthly": "Monatlich", "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} pro Jahr. Monatlich abgerechnet.", - "account_upgrade_dialog_interval_yearly": "Jährlich" + "account_upgrade_dialog_interval_yearly": "Jährlich", + "account_upgrade_dialog_tier_features_messages_one": "{{messages}} tägliche Nachricht", + "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} reserviertes Thema", + "account_upgrade_dialog_tier_features_emails_one": "{{emails}} tägliche E-Mail", + "publish_dialog_call_label": "Telefonanruf", + "publish_dialog_call_item": "Telefonnummer {{number}} anrufen", + "publish_dialog_chip_call_label": "Telefonanruf", + "publish_dialog_chip_call_no_verified_numbers_tooltip": "Keine verifizierten Telefonnummern", + "account_basics_phone_numbers_title": "Telefonnummern", + "account_basics_phone_numbers_copied_to_clipboard": "Telefonnummer wurde in die Zwischenablage kopiert", + "account_basics_phone_numbers_dialog_title": "Telefonnummer hinzufügen", + "account_upgrade_dialog_tier_features_calls_other": "{{calls}} Telefonanrufe pro Tag", + "account_upgrade_dialog_tier_features_no_calls": "Keine Telefonanrufe", + "publish_dialog_call_reset": "Telefonanruf entfernen", + "account_basics_phone_numbers_dialog_description": "Um die Benachrichtigung per Telefonanruf zu nutzen musst Du mindestens eine Telefonnummer hinzufügen und verifizieren. Die Verifizierung kann per SMS oder über einen Anruf erfolgen.", + "account_basics_phone_numbers_description": "Für Telefon-Benachrichtigungen", + "account_basics_phone_numbers_no_phone_numbers_yet": "Noch keine Telefonnummern", + "account_basics_phone_numbers_dialog_number_label": "Telefonnummer", + "account_basics_phone_numbers_dialog_channel_sms": "SMS", + "account_basics_phone_numbers_dialog_channel_call": "Anruf", + "account_basics_phone_numbers_dialog_number_placeholder": "z.B. +49123456789", + "account_basics_phone_numbers_dialog_verify_button_call": "Ruf mich an", + "account_basics_phone_numbers_dialog_verify_button_sms": "SMS senden", + "account_basics_phone_numbers_dialog_code_label": "Verifizierungs-Code", + "account_basics_phone_numbers_dialog_code_placeholder": "z.B. 123456", + "account_basics_phone_numbers_dialog_check_verification_button": "Code bestätigen", + "account_usage_calls_title": "Getätigte Anrufe", + "account_usage_calls_none": "Noch keine Anrufe mit diesem Account getätigt", + "account_upgrade_dialog_tier_features_calls_one": "{{calls}} Telefonanrufe pro Tag" } diff --git a/web/public/static/langs/en.json b/web/public/static/langs/en.json index 8760eb31..3ad04ea7 100644 --- a/web/public/static/langs/en.json +++ b/web/public/static/langs/en.json @@ -2,6 +2,8 @@ "common_cancel": "Cancel", "common_save": "Save", "common_add": "Add", + "common_back": "Back", + "common_copy_to_clipboard": "Copy to clipboard", "signup_title": "Create a ntfy account", "signup_form_username": "Username", "signup_form_password": "Password", @@ -27,6 +29,8 @@ "action_bar_reservation_limit_reached": "Limit reached", "action_bar_send_test_notification": "Send test notification", "action_bar_clear_notifications": "Clear all notifications", + "action_bar_mute_notifications": "Mute notifications", + "action_bar_unmute_notifications": "Unmute notifications", "action_bar_unsubscribe": "Unsubscribe", "action_bar_toggle_mute": "Mute/unmute notifications", "action_bar_toggle_action_menu": "Open/close action menu", @@ -50,11 +54,15 @@ "nav_button_connecting": "connecting", "nav_upgrade_banner_label": "Upgrade to ntfy Pro", "nav_upgrade_banner_description": "Reserve topics, more messages & emails, and larger attachments", - "alert_grant_title": "Notifications are disabled", - "alert_grant_description": "Grant your browser permission to display desktop notifications.", - "alert_grant_button": "Grant now", + "alert_notification_permission_required_title": "Notifications are disabled", + "alert_notification_permission_required_description": "Grant your browser permission to display desktop notifications", + "alert_notification_permission_required_button": "Grant now", + "alert_notification_permission_denied_title": "Notifications are blocked", + "alert_notification_permission_denied_description": "Please re-enable them in your browser", + "alert_notification_ios_install_required_title": "iOS install required", + "alert_notification_ios_install_required_description": "Click on the Share icon and Add to Home Screen to enable notifications on iOS", "alert_not_supported_title": "Notifications not supported", - "alert_not_supported_description": "Notifications are not supported in your browser.", + "alert_not_supported_description": "Notifications are not supported in your browser", "alert_not_supported_context_description": "Notifications are only supported over HTTPS. This is a limitation of the Notifications API.", "notifications_list": "Notifications list", "notifications_list_item": "Notification", @@ -82,6 +90,7 @@ "notifications_actions_open_url_title": "Go to {{url}}", "notifications_actions_not_supported": "Action not supported in web app", "notifications_actions_http_request_title": "Send HTTP {{method}} to {{url}}", + "notifications_actions_failed_notification": "Unsuccessful action", "notifications_none_for_topic_title": "You haven't received any notifications for this topic yet.", "notifications_none_for_topic_description": "To send notifications to this topic, simply PUT or POST to the topic URL.", "notifications_none_for_any_title": "You haven't received any notifications.", @@ -127,6 +136,9 @@ "publish_dialog_email_label": "Email", "publish_dialog_email_placeholder": "Address to forward the notification to, e.g. phil@example.com", "publish_dialog_email_reset": "Remove email forward", + "publish_dialog_call_label": "Phone call", + "publish_dialog_call_item": "Call phone number {{number}}", + "publish_dialog_call_reset": "Remove phone call", "publish_dialog_attach_label": "Attachment URL", "publish_dialog_attach_placeholder": "Attach file by URL, e.g. https://f-droid.org/F-Droid.apk", "publish_dialog_attach_reset": "Remove attachment URL", @@ -138,6 +150,8 @@ "publish_dialog_other_features": "Other features:", "publish_dialog_chip_click_label": "Click URL", "publish_dialog_chip_email_label": "Forward to email", + "publish_dialog_chip_call_label": "Phone call", + "publish_dialog_chip_call_no_verified_numbers_tooltip": "No verified phone numbers", "publish_dialog_chip_attach_url_label": "Attach file by URL", "publish_dialog_chip_attach_file_label": "Attach local file", "publish_dialog_chip_delay_label": "Delay delivery", @@ -146,6 +160,7 @@ "publish_dialog_button_cancel_sending": "Cancel sending", "publish_dialog_button_cancel": "Cancel", "publish_dialog_button_send": "Send", + "publish_dialog_checkbox_markdown": "Format as Markdown", "publish_dialog_checkbox_publish_another": "Publish another", "publish_dialog_attached_file_title": "Attached file:", "publish_dialog_attached_file_filename_placeholder": "Attachment filename", @@ -157,6 +172,7 @@ "subscribe_dialog_subscribe_description": "Topics may not be password-protected, so choose a name that's not easy to guess. Once subscribed, you can PUT/POST notifications.", "subscribe_dialog_subscribe_topic_placeholder": "Topic name, e.g. phil_alerts", "subscribe_dialog_subscribe_use_another_label": "Use another server", + "subscribe_dialog_subscribe_use_another_background_info": "Notifications from other servers will not be received when the web app is not open", "subscribe_dialog_subscribe_base_url_label": "Service URL", "subscribe_dialog_subscribe_button_generate_topic_name": "Generate name", "subscribe_dialog_subscribe_button_cancel": "Cancel", @@ -165,7 +181,6 @@ "subscribe_dialog_login_description": "This topic is password-protected. Please enter username and password to subscribe.", "subscribe_dialog_login_username_label": "Username, e.g. phil", "subscribe_dialog_login_password_label": "Password", - "subscribe_dialog_login_button_back": "Back", "subscribe_dialog_login_button_login": "Login", "subscribe_dialog_error_user_not_authorized": "User {{username}} not authorized", "subscribe_dialog_error_topic_already_reserved": "Topic already reserved", @@ -182,6 +197,21 @@ "account_basics_password_dialog_confirm_password_label": "Confirm password", "account_basics_password_dialog_button_submit": "Change password", "account_basics_password_dialog_current_password_incorrect": "Password incorrect", + "account_basics_phone_numbers_title": "Phone numbers", + "account_basics_phone_numbers_dialog_description": "To use the call notification feature, you need to add and verify at least one phone number. Verification can be done via SMS or a phone call.", + "account_basics_phone_numbers_description": "For phone call notifications", + "account_basics_phone_numbers_no_phone_numbers_yet": "No phone numbers yet", + "account_basics_phone_numbers_copied_to_clipboard": "Phone number copied to clipboard", + "account_basics_phone_numbers_dialog_title": "Add phone number", + "account_basics_phone_numbers_dialog_number_label": "Phone number", + "account_basics_phone_numbers_dialog_number_placeholder": "e.g. +1222333444", + "account_basics_phone_numbers_dialog_verify_button_sms": "Send SMS", + "account_basics_phone_numbers_dialog_verify_button_call": "Call me", + "account_basics_phone_numbers_dialog_code_label": "Verification code", + "account_basics_phone_numbers_dialog_code_placeholder": "e.g. 123456", + "account_basics_phone_numbers_dialog_check_verification_button": "Confirm code", + "account_basics_phone_numbers_dialog_channel_sms": "SMS", + "account_basics_phone_numbers_dialog_channel_call": "Call", "account_usage_title": "Usage", "account_usage_of_limit": "of {{limit}}", "account_usage_unlimited": "Unlimited", @@ -203,6 +233,8 @@ "account_basics_tier_manage_billing_button": "Manage billing", "account_usage_messages_title": "Published messages", "account_usage_emails_title": "Emails sent", + "account_usage_calls_title": "Phone calls made", + "account_usage_calls_none": "No phone calls can be made with this account", "account_usage_reservations_title": "Reserved topics", "account_usage_reservations_none": "No reserved topics for this account", "account_usage_attachment_storage_title": "Attachment storage", @@ -232,6 +264,9 @@ "account_upgrade_dialog_tier_features_messages_other": "{{messages}} daily messages", "account_upgrade_dialog_tier_features_emails_one": "{{emails}} daily email", "account_upgrade_dialog_tier_features_emails_other": "{{emails}} daily emails", + "account_upgrade_dialog_tier_features_calls_one": "{{calls}} daily phone calls", + "account_upgrade_dialog_tier_features_calls_other": "{{calls}} daily phone calls", + "account_upgrade_dialog_tier_features_no_calls": "No phone calls", "account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} per file", "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} total storage", "account_upgrade_dialog_tier_price_per_month": "month", @@ -254,7 +289,6 @@ "account_tokens_table_expires_header": "Expires", "account_tokens_table_never_expires": "Never expires", "account_tokens_table_current_session": "Current browser session", - "account_tokens_table_copy_to_clipboard": "Copy to clipboard", "account_tokens_table_copied_to_clipboard": "Access token copied", "account_tokens_table_cannot_delete_or_edit": "Cannot edit or delete current session token", "account_tokens_table_create_token_button": "Create access token", @@ -300,6 +334,11 @@ "prefs_notifications_delete_after_one_day_description": "Notifications are auto-deleted after one day", "prefs_notifications_delete_after_one_week_description": "Notifications are auto-deleted after one week", "prefs_notifications_delete_after_one_month_description": "Notifications are auto-deleted after one month", + "prefs_notifications_web_push_title": "Background notifications", + "prefs_notifications_web_push_enabled_description": "Notifications are received even when the web app is not running (via Web Push)", + "prefs_notifications_web_push_disabled_description": "Notification are received when the web app is running (via WebSocket)", + "prefs_notifications_web_push_enabled": "Enabled for {{server}}", + "prefs_notifications_web_push_disabled": "Disabled", "prefs_users_title": "Manage users", "prefs_users_description": "Add/remove users for your protected topics here. Please note that username and password are stored in the browser's local storage.", "prefs_users_description_no_sync": "Users and passwords are not synchronized to your account.", @@ -317,6 +356,10 @@ "prefs_users_dialog_password_label": "Password", "prefs_appearance_title": "Appearance", "prefs_appearance_language_title": "Language", + "prefs_appearance_theme_title": "Theme", + "prefs_appearance_theme_system": "System (default)", + "prefs_appearance_theme_dark": "Dark mode", + "prefs_appearance_theme_light": "Light mode", "prefs_reservations_title": "Reserved topics", "prefs_reservations_description": "You can reserve topic names for personal use here. Reserving a topic gives you ownership over the topic, and allows you to define access permissions for other users over the topic.", "prefs_reservations_limit_reached": "You reached your reserved topics limit.", @@ -352,8 +395,13 @@ "error_boundary_title": "Oh no, ntfy crashed", "error_boundary_description": "This should obviously not happen. Very sorry about this.
If you have a minute, please report this on GitHub, or let us know via Discord or Matrix.", "error_boundary_button_copy_stack_trace": "Copy stack trace", + "error_boundary_button_reload_ntfy": "Reload ntfy", "error_boundary_stack_trace": "Stack trace", "error_boundary_gathering_info": "Gather more info …", "error_boundary_unsupported_indexeddb_title": "Private browsing not supported", - "error_boundary_unsupported_indexeddb_description": "The ntfy web app needs IndexedDB to function, and your browser does not support IndexedDB in private browsing mode.

While this is unfortunate, it also doesn't really make a lot of sense to use the ntfy web app in private browsing mode anyway, because everything is stored in the browser storage. You can read more about it in this GitHub issue, or talk to us on Discord or Matrix." + "error_boundary_unsupported_indexeddb_description": "The ntfy web app needs IndexedDB to function, and your browser does not support IndexedDB in private browsing mode.

While this is unfortunate, it also doesn't really make a lot of sense to use the ntfy web app in private browsing mode anyway, because everything is stored in the browser storage. You can read more about it in this GitHub issue, or talk to us on Discord or Matrix.", + "web_push_subscription_expiring_title": "Notifications will be paused", + "web_push_subscription_expiring_body": "Open ntfy to continue receiving notifications", + "web_push_unknown_notification_title": "Unknown notification received from server", + "web_push_unknown_notification_body": "You may need to update ntfy by opening the web app" } diff --git a/web/public/static/langs/es.json b/web/public/static/langs/es.json index 16fe2cd9..045e43df 100644 --- a/web/public/static/langs/es.json +++ b/web/public/static/langs/es.json @@ -3,12 +3,12 @@ "action_bar_send_test_notification": "Enviar notificación de prueba", "action_bar_clear_notifications": "Borrar todas las notificaciones", "nav_topics_title": "Tópicos suscritos", - "alert_grant_button": "Conceder ahora", + "alert_notification_permission_required_button": "Conceder ahora", "action_bar_unsubscribe": "Cancelar la suscripción", "message_bar_type_message": "Escriba un mensaje aquí", "message_bar_error_publishing": "Error al publicar la notificación", - "alert_grant_title": "Las notificaciones están deshabilitadas", - "alert_grant_description": "Concede a tu navegador permiso para mostrar notificaciones en el escritorio.", + "alert_notification_permission_required_title": "Las notificaciones están deshabilitadas", + "alert_notification_permission_required_description": "Concede a tu navegador permiso para mostrar notificaciones en el escritorio.", "nav_button_all_notifications": "Todas las notificaciones", "nav_button_settings": "Ajustes", "nav_button_subscribe": "Suscribirse al tópico", @@ -81,7 +81,7 @@ "subscribe_dialog_login_description": "Este tópico está protegido por contraseña. Por favor, introduzca su nombre de usuario y contraseña para suscribirse.", "subscribe_dialog_login_username_label": "Nombre de usuario, ej. phil", "subscribe_dialog_login_password_label": "Contraseña", - "subscribe_dialog_login_button_back": "Volver", + "common_back": "Volver", "subscribe_dialog_login_button_login": "Iniciar sesión", "subscribe_dialog_error_user_not_authorized": "Usuario {{username}} no autorizado", "subscribe_dialog_error_user_anonymous": "anónimo", @@ -107,7 +107,7 @@ "prefs_appearance_language_title": "Idioma", "error_boundary_title": "Oh no, ntfy tuvo un error", "error_boundary_button_copy_stack_trace": "Copiar el stack trace", - "error_boundary_stack_trace": "Stack trace", + "error_boundary_stack_trace": "Rastreo de pila", "error_boundary_gathering_info": "Reunir más información …", "notifications_example": "Ejemplo", "prefs_notifications_min_priority_title": "Prioridad mínima", @@ -257,7 +257,7 @@ "account_tokens_table_expires_header": "Expira", "account_tokens_table_never_expires": "Nunca expira", "account_tokens_table_current_session": "Sesión del navegador actual", - "account_tokens_table_copy_to_clipboard": "Copiar al portapapeles", + "common_copy_to_clipboard": "Copiar al portapapeles", "account_tokens_table_copied_to_clipboard": "Token de acceso copiado", "account_tokens_table_cannot_delete_or_edit": "No se puede editar ni eliminar el token de sesión actual", "account_tokens_table_create_token_button": "Crear token de acceso", @@ -352,5 +352,34 @@ "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} facturado anualmente. Guardar {{save}}.", "account_upgrade_dialog_billing_contact_website": "Si tiene preguntas sobre facturación, consulte nuestra página web.", "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} al año. Facturación mensual.", - "account_upgrade_dialog_billing_contact_email": "Para preguntas sobre facturación, por favor contáctenos directamente." + "account_upgrade_dialog_billing_contact_email": "Para preguntas sobre facturación, por favor contáctenos directamente.", + "account_upgrade_dialog_tier_features_messages_one": "{{messages}} mensaje diario", + "account_upgrade_dialog_tier_features_emails_one": "{{emails}} correo electrónico diario", + "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} tema reservado", + "publish_dialog_call_label": "Llamada telefónica", + "publish_dialog_call_placeholder": "Número de teléfono al cual llamar con el mensaje, por ejemplo +12223334444, o \"sí\"", + "publish_dialog_chip_call_label": "Llamada telefónica", + "account_basics_phone_numbers_title": "Números de teléfono", + "account_basics_phone_numbers_description": "Para notificaciones por llamada teléfonica", + "account_basics_phone_numbers_no_phone_numbers_yet": "Aún no hay números de teléfono", + "account_basics_phone_numbers_dialog_number_label": "Número de teléfono", + "account_basics_phone_numbers_dialog_number_placeholder": "p. ej. +1222333444", + "account_basics_phone_numbers_dialog_verify_button_sms": "Envía SMS", + "account_basics_phone_numbers_dialog_verify_button_call": "Llámame", + "account_basics_phone_numbers_dialog_code_label": "Código de verificación", + "account_basics_phone_numbers_dialog_channel_sms": "SMS", + "account_basics_phone_numbers_dialog_channel_call": "Llamar", + "account_usage_calls_title": "Llamadas telefónicas realizadas", + "account_usage_calls_none": "No se pueden hacer llamadas telefónicas con esta cuenta", + "account_upgrade_dialog_tier_features_calls_one": "{{llamadas}} llamadas telefónicas diarias", + "account_upgrade_dialog_tier_features_calls_other": "{{llamadas}} llamadas telefónicas diarias", + "account_upgrade_dialog_tier_features_no_calls": "No hay llamadas telefónicas", + "publish_dialog_call_reset": "Eliminar llamada telefónica", + "account_basics_phone_numbers_dialog_description": "Para utilizar la función de notificación de llamadas, tiene que añadir y verificar al menos un número de teléfono. La verificación puede realizarse mediante un SMS o una llamada telefónica.", + "account_basics_phone_numbers_copied_to_clipboard": "Número de teléfono copiado al portapapeles", + "account_basics_phone_numbers_dialog_check_verification_button": "Confirmar código", + "account_basics_phone_numbers_dialog_title": "Agregar número de teléfono", + "account_basics_phone_numbers_dialog_code_placeholder": "p.ej. 123456", + "publish_dialog_call_item": "Llamar al número de teléfono {{number}}", + "publish_dialog_chip_call_no_verified_numbers_tooltip": "No hay números de teléfono verificados" } diff --git a/web/public/static/langs/fi.json b/web/public/static/langs/fi.json new file mode 100644 index 00000000..ec590ecd --- /dev/null +++ b/web/public/static/langs/fi.json @@ -0,0 +1,368 @@ +{ + "publish_dialog_message_placeholder": "Kirjoita viesti tähän", + "account_upgrade_dialog_tier_features_no_calls": "Ei puheluita", + "account_upgrade_dialog_billing_contact_email": "Laskutukseen liittyvissä kysymyksissä contact us suoraan.", + "account_tokens_dialog_title_create": "Luo käyttöoikeustunnus", + "prefs_reservations_dialog_title_edit": "Muokkaa varattua topikkia", + "account_basics_tier_interval_monthly": "Kuukausittain", + "publish_dialog_checkbox_publish_another": "Julkaise toinen", + "publish_dialog_details_examples_description": "Katso esimerkkejä ja yksityiskohtaisen kuvauksen kaikista lähetysominaisuuksista dokumentaatiosta.", + "account_basics_tier_canceled_subscription": "Tilauksesi peruutettiin ja se muutetaan maksuttomaksi tiliksi {{date}}.", + "priority_default": "oletus", + "prefs_notifications_min_priority_title": "Vähimmäisprioriteetti", + "account_upgrade_dialog_tier_features_calls_one": "{{calls}} päivittäisiä puheluja", + "account_upgrade_dialog_tier_current_label": "Nykyinen", + "action_bar_account": "Kirjautuminen", + "publish_dialog_filename_placeholder": "Liitetiedoston nimi", + "account_basics_password_dialog_current_password_incorrect": "Salasana virheellinen", + "account_tokens_table_token_header": "Token", + "prefs_notifications_delete_after_never": "Ei koskaan", + "prefs_users_description": "Lisää/poista käyttäjiä suojatuista topikeista täällä. Huomaa, että käyttäjätunnus ja salasana on tallennettu selaimen paikalliseen tallennustilaan.", + "account_basics_phone_numbers_dialog_number_label": "Puhelinnumero", + "subscribe_dialog_subscribe_description": "Aiheet eivät välttämättä ole salasanasuojattuja, joten valitse nimi, jota ei ole helposti arvatavissa. Kun olet tilannut, voit käyttää PUT/POST ilmoituksia.", + "action_bar_logo_alt": "ntfy logo", + "account_basics_password_dialog_button_submit": "Vaihda salasana", + "publish_dialog_emoji_picker_show": "Valitse emoji", + "account_basics_username_title": "Käyttäjätunnus", + "login_disabled": "Kirjautuminen poissa käytöstä", + "account_basics_phone_numbers_dialog_check_verification_button": "Vahvista koodi", + "account_upgrade_dialog_interval_yearly_discount_save_up_to": "säästä jopa {{discount}}%", + "account_tokens_dialog_label": "Etiketti, esim. Tutka-ilmoitukset", + "common_add": "Lisää", + "account_tokens_table_expires_header": "Vanhenee", + "account_upgrade_dialog_proration_info": "Osuus suhde: Kun päivität maksullisten pakettien välillä, hintaero veloitetaan välittömästi. Kun siirryt alemmalle tasolle, saldoa käytetään tulevien laskutuskausien maksamiseen.", + "prefs_reservations_dialog_access_label": "Oikeudet", + "account_usage_attachment_storage_title": "Liiteiden säilytys", + "prefs_users_dialog_username_label": "Username, esim pena", + "message_bar_error_publishing": "Virhe ilmoituksen julkaisemisessa", + "publish_dialog_chip_delay_label": "Viivästytä toimitusta", + "account_usage_messages_title": "Julkaistut viestit", + "notifications_attachment_open_button": "Avaa liite", + "emoji_picker_search_clear": "Tyhjennä haku", + "prefs_reservations_table_not_subscribed": "Ei tilattu", + "publish_dialog_topic_placeholder": "Topikin nimi, esim. erkin_hälyt", + "account_upgrade_dialog_tier_features_emails_other": "{{emails}} päivittäisiä emaileja", + "prefs_notifications_min_priority_max_only": "Vain maksimi prioriteetti", + "account_upgrade_dialog_tier_features_calls_other": "{{calls}} päivittäisiä puheluja", + "prefs_notifications_sound_description_some": "Ilmoitukset soittavat {{sound}} äänen saapuessaan", + "prefs_reservations_edit_button": "Muokkaa topikin oikeuksia", + "account_basics_phone_numbers_dialog_verify_button_sms": "Lähetä SMS", + "account_basics_tier_change_button": "Vaihda", + "account_tokens_dialog_expires_never": "Käyttöoikeus ei vanhene koskaan", + "subscribe_dialog_login_title": "Kirjautuminen vaaditaan", + "account_tokens_dialog_expires_x_days": "Tunnus vanhenee {{days}} päivän kuluttua", + "notifications_new_indicator": "Uusi ilmoitus", + "prefs_reservations_table_everyone_read_only": "Minä voin julkaista ja tilata, kaikki voivat tilata", + "prefs_reservations_table_everyone_deny_all": "Vain minä voin julkaista ja tilata", + "publish_dialog_chip_topic_label": "Vaihda topikkia", + "account_basics_phone_numbers_dialog_description": "Jotta voit käyttää puheluilmoitusominaisuutta, sinun on lisättävä ja vahvistettava vähintään yksi puhelinnumero. Vahvistus voidaan tehdä tekstiviestillä tai puhelimitse.", + "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} varatut topikit", + "publish_dialog_tags_placeholder": "Pilkuilla eroteltu luettelo tunnisteista, esim. varoitus, srv1-varmuuskopio", + "account_delete_title": "Poista tili", + "publish_dialog_attached_file_remove": "Poista liitetiedosto", + "nav_button_connecting": "yhdistää", + "account_delete_dialog_label": "Salasana", + "subscribe_dialog_login_button_login": "Kirjaudu", + "account_upgrade_dialog_tier_features_no_reservations": "Ei varattuja topikkeja", + "message_bar_type_message": "Kirjoita viesti tähän", + "publish_dialog_base_url_label": "Palvelun URL", + "signup_form_confirm_password": "Vahvista salasana", + "prefs_users_table_cannot_delete_or_edit": "Kirjautunutta käyttäjää ei voi poistaa tai muokata", + "account_basics_tier_admin_suffix_with_tier": "(mukana {{tier}} tier)", + "prefs_notifications_delete_after_three_hours_description": "Ilmoitukset poistetaan automaattisesti kolmen tunnin kuluttua", + "publish_dialog_chip_email_label": "Lähetä sähköpostiin", + "publish_dialog_attach_label": "Liitteen URL-osoite", + "signup_form_username": "Käyttäjätunnus", + "prefs_notifications_delete_after_three_hours": "Kolmen tunnin jälkeen", + "nav_button_muted": "Ilmoitukset mykistetty", + "action_bar_profile_settings": "Asetukset", + "signup_error_creation_limit_reached": "Tilin lisäämisraja saavutettu", + "notifications_attachment_open_title": "Siirry osoitteeseen {{url}}", + "prefs_notifications_min_priority_description_x_or_higher": "Näytä ilmoitukset, jos prioriteetti on {{number}} ({{name}}) tai suurempi", + "reservation_delete_dialog_description": "Varauksen poistaminen luopuu topikin omistajuudesta ja antaa muiden varata sen. Voit säilyttää tai poistaa olemassa olevia viestejä ja liitteitä.", + "subscribe_dialog_login_username_label": "Käyttäjätunnus, esim. pentti", + "subscribe_dialog_error_user_not_authorized": "Käyttäjää {{username}} ei ole valtuutettu", + "prefs_reservations_table_everyone_read_write": "Jokainen voi julkaista ja tilata", + "prefs_reservations_dialog_title_delete": "Poista topikin varaus", + "prefs_users_table": "Käyttäjä taulukko", + "prefs_reservations_table_topic_header": "Topikki", + "action_bar_toggle_mute": "Hiljennä/poista hiljennys", + "reservation_delete_dialog_submit_button": "Poista varaus", + "account_basics_title": "Tili", + "nav_button_documentation": "Käyttäjä oppaat", + "prefs_reservations_limit_reached": "Olet saavuttanut varattujen topikkien rajan.", + "account_upgrade_dialog_interval_monthly": "Kuukausittain", + "prefs_users_add_button": "Lisää käyttäjä", + "account_upgrade_dialog_tier_features_messages_other": "{{messages}} päivittäisiä viestejä", + "publish_dialog_delay_reset": "Poista viivästetty toimitus", + "account_basics_phone_numbers_no_phone_numbers_yet": "Ei puhelinnumeroita vielä", + "action_bar_toggle_action_menu": "Avaa/sulje toiminto valikko", + "subscribe_dialog_subscribe_button_generate_topic_name": "Luo nimi", + "notifications_list_item": "Ilmoitus", + "prefs_appearance_language_title": "Kieli", + "notifications_attachment_link_expired": "latauslinkki vanhentunut", + "subscribe_dialog_login_password_label": "Salasana", + "prefs_notifications_delete_after_one_day_description": "Ilmoitukset poistetaan automaattisesti yhden päivän kuluttua", + "subscribe_dialog_subscribe_button_subscribe": "Tilaa", + "account_tokens_table_never_expires": "Ei vanhene koskaan", + "account_tokens_delete_dialog_title": "Poista käyttöoikeustunnus", + "prefs_notifications_delete_after_one_month": "Kuukauden kuluttua", + "publish_dialog_chip_call_label": "Puhelu", + "account_basics_phone_numbers_dialog_title": "Lisää puhelinnumero", + "account_tokens_delete_dialog_description": "Ennen kuin poistat käyttöoikeustunnuksen, varmista, että mikään sovellus tai komentosarja ei käytä sitä aktiivisesti. Tätä toimintoa ei voi kumota.", + "nav_button_all_notifications": "Kaikki ilmoitukset", + "account_upgrade_dialog_button_cancel": "Peruuta", + "notifications_attachment_image": "Liitekuva", + "account_tokens_table_label_header": "Merkki", + "notifications_attachment_file_document": "muu asiakirja", + "publish_dialog_button_cancel": "Peruuta", + "account_upgrade_dialog_billing_contact_website": "Laskutukseen liittyvissä kysymyksissä käy website.", + "signup_form_button_submit": "Kirjaudu linkki", + "account_basics_username_admin_tooltip": "Olet pääkäyttäjä", + "prefs_notifications_delete_after_never_description": "Ilmoituksia eivät koskaan poisteta automaattisesti", + "account_delete_dialog_description": "Tämä poistaa pysyvästi tilisi, mukaan lukien kaikki palvelimelle tallennetut tiedot. Poistamisen jälkeen käyttäjätunnuksesi on poissa käytöstä 7 päivään. Jos todella haluat jatkaa, vahvista salasanasi alla olevaan kenttään.", + "publish_dialog_email_reset": "Poista sähköpostin edelleenlähetys", + "account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} varatut topikit", + "account_usage_reservations_none": "Tälle tilille ei ole varattu topikkeja", + "prefs_notifications_sound_description_none": "Ilmoitukset eivät toista ääntä saapuessaan", + "account_tokens_description": "Käytä käyttjätunnuksia, kun julkaiset ja tilaat ntfy API:n kautta, jotta sinun ei tarvitse lähettää tilisi tunnistetietoja. Katso lisätietoja documentation.", + "common_back": "Takaisin", + "prefs_reservations_table": "Varattujen topikkien taulukko", + "emoji_picker_search_placeholder": "Etsi emoji", + "subscribe_dialog_subscribe_topic_placeholder": "Topikin nimi, esim. pentin_hälyt", + "account_upgrade_dialog_button_cancel_subscription": "Peruuta tilaus", + "notifications_attachment_file_audio": "äänitiedosto", + "account_upgrade_dialog_tier_features_emails_one": "{{emails}} päivittäisiä emaileja", + "action_bar_sign_up": "Kirjautuminen", + "account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} tiedostokoko", + "notifications_mark_read": "Merkitse luetuksi", + "prefs_reservations_description": "Voit varata topikien nimiä henkilökohtaiseen käyttöön täältä. Aiheen varaaminen antaa sinulle topikin omistajuuden ja voit määrittää topikkiin liittyviä käyttöoikeuksia muille käyttäjille.", + "notifications_attachment_copy_url_title": "Kopioi liitteen URL-osoite leikepöydälle", + "account_usage_title": "Käytössä", + "account_basics_tier_upgrade_button": "Päivitä Pro versioon", + "prefs_users_description_no_sync": "Käyttäjiä ja salasanoja ei ole synkronoitu tiliisi.", + "account_tokens_dialog_title_edit": "Muokkaa käyttöoikeustunnusta", + "nav_button_publish_message": "Julkaisutiedot", + "prefs_users_table_base_url_header": "Palvelin URL", + "notifications_click_copy_url_title": "Kopioi linkin URL-osoite leikepöydälle", + "publish_dialog_attach_reset": "Poista liitteen URL-osoite", + "account_upgrade_dialog_tier_features_messages_one": "{{messages}} päivittäisiä viestejä", + "account_upgrade_dialog_reservations_warning_one": "Valittu taso sallii vähemmän varattuja topikeita kuin nykyinen tasosi. Ennen kuin muutat tasosi, poista vähintään yksi varaus. Voit poistaa varauksia Asetuksista.", + "common_copy_to_clipboard": "Kopioi leikkelepöydälle", + "alert_not_supported_description": "Selaimesi ei tue ilmoituksia.", + "subscribe_dialog_error_topic_already_reserved": "Topikki on jo varattu", + "message_bar_publish": "Julkaise viesti", + "alert_grant_description": "Myönnä selaimelle lupa näyttää työpöytäilmoituksia.", + "prefs_users_table_user_header": "Käyttäjä", + "error_boundary_stack_trace": "Pinon jälki", + "prefs_users_dialog_password_label": "Salasana", + "prefs_notifications_delete_after_one_week": "Viikon kuluttua", + "publish_dialog_priority_low": "Matala tärkeys", + "publish_dialog_priority_label": "Prioriteetti", + "prefs_reservations_delete_button": "Poista topikin oikeudet", + "account_basics_tier_admin_suffix_no_tier": "(no tier)", + "prefs_notifications_delete_after_one_week_description": "Ilmoitukset poistetaan automaattisesti viikon kuluttua", + "error_boundary_unsupported_indexeddb_description": "Ntfy-verkkosovellus tarvitsee IndexedDB:n toimiakseen, eikä selaimesi tue IndexedDB:tä yksityisessä selaustilassa.

Vaikka tämä on valitettavaa, ntfy-verkon käyttäminen ei myöskään ole kovin järkevää yksityisessä selaustilassa, koska kaikki on tallennettu selaimen tallennustilaan. Voit lukea siitä lisää tästä GitHub-numerosta tai puhua meille Discordissa tai Matrixissa.", + "subscribe_dialog_subscribe_button_cancel": "Peruuta", + "notifications_attachment_copy_url_button": "Kopioi URL", + "account_basics_tier_payment_overdue": "Maksusi on myöhässä. Päivitä maksutapasi, tai tilisi poistetaan pian.", + "publish_dialog_title_placeholder": "Ilmoituksen otsikko, esim. Levytilan hälytys", + "account_basics_tier_description": "Tilisi taso", + "account_basics_phone_numbers_description": "Puheluilmoituksia varten", + "prefs_reservations_dialog_title_add": "Varaa topikki", + "account_basics_tier_free": "Vapaa", + "account_upgrade_dialog_cancel_warning": "Tämä peruuttaa tilauksesi ja alentaa tilisi {{date}}. Tuona päivänä topikit sekä palvelimen välimuistissa olevat viestit poistetaan.", + "notifications_click_copy_url_button": "Kopioi linkki", + "account_basics_tier_admin": "Admin", + "subscribe_dialog_subscribe_title": "Tilaa topikki", + "nav_topics_title": "Tilatut topikit", + "prefs_notifications_sound_title": "Ilmoitusääni", + "prefs_notifications_min_priority_default_and_higher": "Oletusprioriteetti ja korkeammat", + "prefs_reservations_table_access_header": "Oikeudet", + "action_bar_show_menu": "Näytä menu", + "action_bar_settings": "Asetukset", + "notifications_copied_to_clipboard": "Kopioitu leikepöydälle", + "account_delete_dialog_button_cancel": "Peruuta", + "publish_dialog_delay_placeholder": "Toimituksen viivästyminen, esim. {{unixTimestamp}}, {{relativeTime}} tai \"{{naturalLanguage}}\" (vain englanti)", + "account_tokens_table_copied_to_clipboard": "Käyttöoikeustunnus kopioitu", + "alert_grant_title": "Ilmoitukset on poistettu käytöstä", + "account_tokens_dialog_expires_x_hours": "Tunnus vanhenee {{hours}} tunnin kuluttua", + "prefs_users_edit_button": "Muokkaa käyttäjää", + "account_upgrade_dialog_title": "Muuta tilitasoa", + "publish_dialog_chip_call_no_verified_numbers_tooltip": "Ei vahvistettuja puhelinnumeroita", + "priority_low": "matala", + "prefs_reservations_table_click_to_subscribe": "Tilaa napsauttamalla", + "account_basics_password_description": "Vaihda tilisi salasana", + "publish_dialog_call_label": "Puhelu", + "account_usage_calls_title": "Soitetut puhelut", + "error_boundary_description": "Näin ei selvästikään pitäisi tapahtua. Pahoittelut tästä.
Jos sinulla on hetki aikaa, ilmoita tästä GitHubissa tai ilmoita meille Discordin tai Matrix kautta.", + "signup_form_toggle_password_visibility": "Vaihda salasanan näkyvyys", + "login_link_signup": "Kirjaudu linkki", + "publish_dialog_message_label": "Viesti", + "publish_dialog_attached_file_title": "Liitetiedosto:", + "priority_min": "min", + "action_bar_sign_in": "Kirjaudu sisään", + "action_bar_unsubscribe": "Peruuta tilaus", + "account_basics_tier_basic": "Perus", + "signup_title": "Lisää ntfy tili", + "prefs_notifications_min_priority_description_any": "Näytetään kaikki ilmoitukset tärkeydestä riippumatta", + "error_boundary_gathering_info": "Kerää lisätietoja…", + "publish_dialog_priority_max": "Max. prioriteetti", + "error_boundary_unsupported_indexeddb_title": "Yksityistä selaamista ei tueta", + "prefs_notifications_delete_after_one_day": "Yhden päivän jälkeen", + "error_boundary_title": "Voi ei, ntfy kaatui", + "action_bar_change_display_name": "Näyttönimen vaihtaminen", + "notifications_attachment_file_app": "Android-sovellustiedosto", + "alert_not_supported_context_description": "Ilmoituksia tuetaan vain HTTPS:n kautta. Tämä on Ilmoitussovellusliittymän rajoitus.", + "reservation_delete_dialog_action_keep_description": "Palvelimelle välimuistiin tallennetut viestit ja liitteet tulevat julkiseksi topikin nimen tietävälle henkilölle.", + "prefs_reservations_add_button": "Lisää varattu topik", + "prefs_reservations_title": "Varatut topikit", + "account_basics_phone_numbers_copied_to_clipboard": "Puhelinnumero kopioitu leikepöydälle", + "prefs_reservations_dialog_description": "Topikin varaaminen antaa sinulle aiheen omistajuuden ja voit määrittää aiheeseen liittyviä käyttöoikeuksia muille käyttäjille.", + "account_basics_tier_title": "Tilin tyyppi", + "account_usage_cannot_create_portal_session": "Laskutusportaalin avaaminen epäonnistui", + "account_tokens_delete_dialog_submit_button": "Poista tunnus pysyvästi", + "account_delete_description": "Poista tilisi pysyvästi", + "account_basics_phone_numbers_dialog_number_placeholder": "esim. +35812345678", + "account_basics_phone_numbers_dialog_code_placeholder": "esim. 123456", + "prefs_notifications_title": "Ilmoitukset", + "account_basics_tier_manage_billing_button": "Hallinnoi laskutusta", + "account_tokens_title": "Käyttöoikeudet", + "publish_dialog_email_label": "Email", + "account_basics_username_description": "Hei, se olet sinä ❤", + "prefs_reservations_dialog_topic_label": "Topik", + "account_basics_password_dialog_confirm_password_label": "Vahvista salasana", + "action_bar_reservation_edit": "Muokkaa varatopikkia", + "publish_dialog_base_url_placeholder": "Palvelun URL-osoite, esim. https://example.com", + "prefs_users_title": "Hallinnoi käyttäjiä", + "account_basics_tier_interval_yearly": "vuosittain", + "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} Laskutetaan kuukausittain.", + "action_bar_clear_notifications": "Poista kaikki ilmoitukset", + "account_delete_dialog_button_submit": "Poista tili pysyvästi", + "account_basics_phone_numbers_dialog_channel_call": "Soitto", + "account_basics_password_title": "Salasana", + "account_basics_password_dialog_new_password_label": "Uusi salasana", + "nav_upgrade_banner_label": "Päivitä ntfy Prohon", + "account_tokens_dialog_expires_unchanged": "Jätä viimeinen käyttöpäivä ennalleen", + "publish_dialog_delay_label": "Viive", + "error_boundary_button_copy_stack_trace": "Kopioi pinon jälki", + "publish_dialog_button_send": "Lähetä", + "action_bar_reservation_delete": "Poista varatopikit", + "publish_dialog_button_cancel_sending": "Peruuta lähetys", + "account_tokens_dialog_title_delete": "Poista käyttöoikeustunnus", + "account_usage_of_limit": "limiitistä {{limit}}", + "publish_dialog_attach_placeholder": "Liitä tiedosto URL-osoitteen mukaan, esim. https://f-droid.org/F-Droid.apk", + "publish_dialog_email_placeholder": "Osoite, johon ilmoitus välitetään, esim. urpo@example.com", + "notifications_attachment_link_expires": "linkki vanhenee {{date}}", + "action_bar_send_test_notification": "Lähetä testi ilmoitus", + "reservation_delete_dialog_action_keep_title": "Säilytä välimuistissa olevat viestit ja liitteet", + "prefs_notifications_sound_no_sound": "Ei ääntä", + "account_upgrade_dialog_interval_yearly": "Vuosittain", + "publish_dialog_tags_label": "Tagit", + "signup_form_password": "Salasana", + "action_bar_reservation_limit_reached": "Varatopikien raja", + "account_upgrade_dialog_button_redirect_signup": "Kirjaudu nyt", + "publish_dialog_click_placeholder": "URL-osoite, joka avautuu, kun ilmoitusta napsautetaan", + "alert_not_supported_title": "Ilmoituksia ei tueta", + "account_tokens_dialog_button_cancel": "Peruuta", + "subscribe_dialog_error_user_anonymous": "Anonyymi", + "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} laskutetaan vuosittain. Tallenna {{save}}.", + "prefs_notifications_min_priority_high_and_higher": "Korkea prioriteetti ja korkeammat", + "account_usage_basis_ip_description": "Tämän tilin käyttötilastot ja rajoitukset perustuvat IP-osoitteeseesi, joten ne voidaan jakaa muiden käyttäjien kanssa. Yllä esitetyt rajat ovat likimääräisiä perustuen olemassa oleviin rajoituksiin.", + "publish_dialog_priority_high": "Korkea prioriteetti", + "login_form_button_submit": "Kirjaudu", + "account_basics_password_dialog_title": "Vaihda salasana", + "priority_max": "max", + "notifications_attachment_file_image": "kuvatiedosto", + "account_usage_limits_reset_daily": "Käyttörajat nollataan päivittäin keskiyöllä (UTC)", + "account_usage_unlimited": "Rajoittamaton", + "prefs_users_delete_button": "Poista käyttäjä", + "publish_dialog_click_label": "Napsauta URL-osoitetta", + "prefs_notifications_min_priority_any": "Kaikki prioriteetit", + "account_tokens_dialog_expires_label": "Käyttöoikeustunnus vanhenee", + "publish_dialog_filename_label": "Tiedostonimi", + "publish_dialog_chip_attach_file_label": "Liitä paikallinen tiedosto", + "account_basics_phone_numbers_title": "Puhelinnumerot", + "prefs_notifications_delete_after_title": "Poista ilmoitukset", + "account_upgrade_dialog_interval_yearly_discount_save": "säästä {{discount}}%", + "signup_disabled": "Kirjautuminen estetty", + "publish_dialog_drop_file_here": "Pudota tiedosto tähän", + "prefs_users_dialog_title_edit": "Muokkaa käyttäjää", + "account_basics_password_dialog_current_password_label": "Nykyinen salasana", + "prefs_notifications_min_priority_low_and_higher": "Matala prioriteetti ja korkeammat", + "action_bar_profile_title": "Profiili", + "account_tokens_dialog_button_update": "Päivitä tunnus", + "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} lopullinen tiedostokoko", + "publish_dialog_title_label": "Otsikko", + "prefs_reservations_table_everyone_write_only": "Minä voin julkaista ja tilata, kaikki voivat julkaista", + "prefs_appearance_title": "Näkymä", + "publish_dialog_topic_reset": "Resetoi topikki", + "account_tokens_table_cannot_delete_or_edit": "Nykyistä istuntotunnusta ei voi muokata tai poistaa", + "notifications_tags": "Tagit", + "prefs_notifications_sound_play": "Toista valittu ääni", + "account_tokens_table_last_access_header": "Viimeinen käyty", + "action_bar_profile_logout": "Kirjaudu ulos", + "publish_dialog_attached_file_filename_placeholder": "Liitetiedoston nimi", + "publish_dialog_priority_default": "Oletusprioriteetti", + "subscribe_dialog_subscribe_base_url_label": "Palvelimen URL", + "account_tokens_table_last_origin_tooltip": "Napsauta IP-osoitteesta {{ip}}, etsiäksesi", + "account_usage_reservations_title": "Varatut topikit", + "account_upgrade_dialog_tier_price_per_month": "Kuukausi", + "message_bar_show_dialog": "Näytä julkaisu dialogi", + "publish_dialog_chip_attach_url_label": "Liitä tiedosto URL-osoitteen mukaan", + "account_usage_calls_none": "Tällä tilillä ei voi soittaa puheluita", + "notifications_click_open_button": "Avaa linkki", + "account_tokens_table_current_session": "Nykyinen selainistunto", + "account_upgrade_dialog_button_pay_now": "Maksa nyt ja tilaa", + "nav_upgrade_banner_description": "Varaa aiheita, lisää viestejä ja sähköposteja sekä suurempia liitteitä", + "publish_dialog_call_reset": "Poista puhelu", + "publish_dialog_other_features": "Muut ominaisuudet:", + "subscribe_dialog_subscribe_use_another_label": "Käytä toista palvelinta", + "reservation_delete_dialog_action_delete_title": "Poista välimuistissa olevat viestit ja liitteet", + "signup_error_username_taken": "Käyttäjätunnus {{username}} on jo varattu", + "account_basics_phone_numbers_dialog_code_label": "Vahvistuskoodi", + "nav_button_subscribe": "Tilaa topik", + "publish_dialog_topic_label": "Topikin nimi", + "reservation_delete_dialog_action_delete_description": "Välimuistissa olevat viestit ja liitteet poistetaan pysyvästi. Tätä toimintoa ei voi kumota.", + "alert_grant_button": "Myönnä nyt", + "account_basics_tier_paid_until": "Tilaus maksettu {{date}} asti, ja se uusitaan automaattisesti", + "account_usage_attachment_storage_description": "{{tiedostokoko}} per tiedosto, poistettu {{expiry}} jälkeen", + "publish_dialog_chip_click_label": "Napsauta URL-osoitetta", + "prefs_notifications_delete_after_one_month_description": "Ilmoitukset poistetaan automaattisesti kuukauden kuluttua", + "common_cancel": "Peruuta", + "account_basics_phone_numbers_dialog_verify_button_call": "Soita minulle", + "signup_already_have_account": "Onko sinulla jo tili ? Kirjaudu sisään !", + "publish_dialog_call_item": "Soita puhelinnumeroon {{number}}", + "nav_button_account": "Tili", + "publish_dialog_click_reset": "Poista napsautettava URL-osoite", + "login_title": "Kirjaudu sisään ntfy-tilillesi", + "notifications_list": "Ilmoitusluettelo", + "common_save": "Tallenna", + "prefs_users_dialog_base_url_label": "Palvelin URL, esim. https://ntfy.sh", + "account_usage_emails_title": "Sähköpostit lähetetty", + "account_basics_phone_numbers_dialog_channel_sms": "SMS", + "action_bar_reservation_add": "Varalla oleva topikki", + "account_upgrade_dialog_tier_selected_label": "Valittu", + "account_upgrade_dialog_button_update_subscription": "Päivitä tilaus", + "notifications_attachment_file_video": "videotiedosto", + "priority_high": "korkea", + "notifications_priority_x": "Prioriteetti {{priority}}", + "account_delete_dialog_billing_warning": "Tilin poistaminen peruuttaa myös laskutustilauksesi välittömästi. Et voi enää käyttää laskutuksen hallintapaneelia.", + "prefs_notifications_min_priority_description_max": "Näytä ilmoitukset, jos prioriteetti on 5 (max)", + "subscribe_dialog_login_description": "Tämä Topikki on suojattu salasanalla. Anna käyttäjätunnus ja salasana.", + "account_upgrade_dialog_reservations_warning_other": "Valittu taso sallii vähemmän varattuja topikkeja kuin nykyinen tasosi. Ennen kuin muutat tasosi, poista vähintään {{count}} varausta. Voit poistaa varauksia Asetuksista.", + "prefs_users_dialog_title_add": "Lisää käyttäjä", + "account_tokens_dialog_button_create": "Luo tunnus", + "nav_button_settings": "Asetukset", + "publish_dialog_priority_min": "Min. etusijalla", + "account_tokens_table_create_token_button": "Luo käyttöoikeustunnus", + "notifications_delete": "Poista", + "notifications_actions_not_supported": "Toimintoa ei tueta verkkosovelluksessa", + "notifications_actions_open_url_title": "Siirry osoitteeseen {{url}}", + "notifications_none_for_any_title": "Et ole saanut ilmoituksia.", + "notifications_none_for_topic_description": "Jos haluat lähettää ilmoituksia tähän topikkiin, PUT tai POST topikin URL-osoitteeseen.", + "notifications_none_for_any_description": "Jos haluat lähettää ilmoituksia topikkiin, PUT tai POST topikin URL-osoitteeseen. Tässä on esimerkki yhden topikin käyttämisestä.", + "notifications_no_subscriptions_title": "Näyttää siltä, että sinulla ei ole vielä tilauksia.", + "notifications_none_for_topic_title": "Et ole vielä saanut ilmoituksia tästä topikista.", + "notifications_actions_http_request_title": "Lähetä HTTP {{method}} to {{url}}" +} diff --git a/web/public/static/langs/fr.json b/web/public/static/langs/fr.json index a24ece08..096b62af 100644 --- a/web/public/static/langs/fr.json +++ b/web/public/static/langs/fr.json @@ -50,9 +50,9 @@ "publish_dialog_attachment_limits_file_reached": "Dépasse la limite du fichier {{fileSizeLimit}}", "nav_button_subscribe": "S'abonner au sujet", "notifications_no_subscriptions_description": "Cliquez sur le lien « {{linktext}} » pour créer ou vous abonner à un sujet. Après cela, vous pouvez envoyer des messages via PUT ou POST et vous recevrez des notifications ici.", - "alert_grant_title": "Les notifications sont désactivées", - "alert_grant_description": "Autorisez votre navigateur à afficher les notifications du bureau.", - "alert_grant_button": "Accorder maintenant", + "alert_notification_permission_required_title": "Les notifications sont désactivées", + "alert_notification_permission_required_description": "Autorisez votre navigateur à afficher les notifications du bureau.", + "alert_notification_permission_required_button": "Accorder maintenant", "notifications_none_for_any_title": "Vous n'avez reçu aucune notification.", "publish_dialog_title_topic": "Publier vers {{topic}}", "publish_dialog_title_no_topic": "Publier la notification", @@ -106,7 +106,7 @@ "prefs_notifications_title": "Notifications", "prefs_notifications_delete_after_title": "Supprimer les notifications", "prefs_users_add_button": "Ajouter un utilisateur", - "subscribe_dialog_login_button_back": "Retour", + "common_back": "Retour", "subscribe_dialog_error_user_anonymous": "anonyme", "prefs_notifications_sound_no_sound": "Aucun son", "prefs_notifications_min_priority_title": "Priorité minimum", @@ -272,7 +272,7 @@ "account_delete_dialog_button_submit": "Supprimer définitivement le compte", "account_delete_dialog_billing_warning": "Supprimer votre compte annule aussi immédiatement votre facturation. Vous n'aurez plus accès à votre tableau de bord de facturation.", "account_upgrade_dialog_title": "Changer le tarif du compte", - "account_upgrade_dialog_proration_info": "Facturation : Lors d'un changement entre un plan payant et un autre, la différence de prix sera créditée ou remboursée sur la prochaine facture. Vous ne recevrez pas d'autre facture avant la fin de la prochaine période de facturation.", + "account_upgrade_dialog_proration_info": "Facturation : Lors d'un changement vers un tiers payant, la différence de prix sera débitée immédiatement. En passant d'un tiers payant a gratuit, votre solde sera utilisé pour payer de futur factures.", "account_upgrade_dialog_reservations_warning_other": "Le tarif sélectionné autorise moins de sujets réservés que votre tarif actuel. Avant de changer de tarif, veuillez supprimer au moins {{count}} sujets réservés. Vous pouvez supprimer des sujets réservés dans les Paramètres.", "account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} sujets réservés", "account_upgrade_dialog_tier_features_messages_other": "{{messages}} messages journaliers", @@ -293,7 +293,7 @@ "account_tokens_table_expires_header": "Expire", "account_tokens_table_never_expires": "N'expire jamais", "account_tokens_table_current_session": "Session de navigation actuelle", - "account_tokens_table_copy_to_clipboard": "Copier dans le presse-papier", + "common_copy_to_clipboard": "Copier dans le presse-papier", "account_tokens_table_copied_to_clipboard": "Jeton d'accès copié", "account_tokens_table_create_token_button": "Créer un jeton d'accès", "account_tokens_table_last_origin_tooltip": "Depuis l'adresse IP {{ip}}, cliquer pour rechercher", @@ -352,5 +352,33 @@ "account_upgrade_dialog_interval_yearly_discount_save_up_to": "économisez jusqu'à {{discount}}%", "account_upgrade_dialog_tier_price_per_month": "mois", "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} prélevé annuellement. Économisez {{save}}.", - "account_upgrade_dialog_billing_contact_email": "Pour des questions concernant la facturation, merci de nous contacter directement." + "account_upgrade_dialog_billing_contact_email": "Pour des questions concernant la facturation, merci de nous contacter directement.", + "publish_dialog_call_label": "Appel téléphonique", + "account_basics_phone_numbers_title": "Numéros de téléphone", + "account_basics_phone_numbers_dialog_description": "Pour utiliser la fonctionnalité de notification par appels, vous devez ajouter et vérifier au moins un numéro de téléphone. La vérification peut se faire par SMS ou appel téléphonique.", + "account_basics_phone_numbers_description": "Pour des notifications par appel téléphoniques", + "account_basics_phone_numbers_no_phone_numbers_yet": "Pas encore de numéros de téléphone", + "account_basics_phone_numbers_copied_to_clipboard": "Numéro de téléphone copié dans le presse-papier", + "account_basics_phone_numbers_dialog_title": "Ajouter un numéro de téléphone", + "account_basics_phone_numbers_dialog_number_label": "Numéro de téléphone", + "account_basics_phone_numbers_dialog_number_placeholder": "Ex : +33701020304", + "account_basics_phone_numbers_dialog_verify_button_sms": "Envoyer un SMS", + "account_basics_phone_numbers_dialog_verify_button_call": "Appelez moi", + "account_basics_phone_numbers_dialog_code_label": "Code de vérification", + "account_basics_phone_numbers_dialog_code_placeholder": "Ex : 123456", + "account_basics_phone_numbers_dialog_check_verification_button": "Code de confirmarion", + "account_basics_phone_numbers_dialog_channel_sms": "SMS", + "account_basics_phone_numbers_dialog_channel_call": "Appeler", + "account_usage_calls_none": "Aucun appels téléphoniques ne peut être fait avec ce compte", + "publish_dialog_call_reset": "Supprimer les appels téléphoniques", + "publish_dialog_chip_call_label": "Appel téléphonique", + "account_upgrade_dialog_tier_features_messages_one": "{{messages}} message journalier", + "account_upgrade_dialog_tier_features_emails_one": "{{emails}} mail journalier", + "account_upgrade_dialog_tier_features_calls_other": "{{calls}} appels journaliers", + "account_upgrade_dialog_tier_features_no_calls": "Aucun appel", + "publish_dialog_call_item": "Appeler le numéro {{number}}", + "publish_dialog_chip_call_no_verified_numbers_tooltip": "Aucun numéro de téléphone vérifié", + "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} sujet réservé", + "account_upgrade_dialog_tier_features_calls_one": "{{calls}} appels journaliers", + "account_usage_calls_title": "Appels téléphoniques passés" } diff --git a/web/public/static/langs/gl.json b/web/public/static/langs/gl.json new file mode 100644 index 00000000..92d35610 --- /dev/null +++ b/web/public/static/langs/gl.json @@ -0,0 +1,384 @@ +{ + "common_cancel": "Cancelar", + "common_save": "Gardar", + "common_add": "Engadir", + "signup_disabled": "O rexistro está desactivado", + "signup_error_username_taken": "O identificador {{username}} xa está collido", + "login_title": "Accede á túa conta ntfy", + "action_bar_send_test_notification": "Enviar notificación de proba", + "action_bar_clear_notifications": "Limpar todas as notificacións", + "action_bar_unsubscribe": "Retirar subscrición", + "action_bar_profile_settings": "Axustes", + "message_bar_type_message": "Escribe aquí a mensaxe", + "notifications_copied_to_clipboard": "Copiada ao portapapeis", + "notifications_attachment_image": "Imaxe anexa", + "notifications_attachment_copy_url_title": "Copiar URL do anexo ao portapapeis", + "notifications_attachment_copy_url_button": "Copiar URL", + "notifications_attachment_open_title": "Ir a {{url}}", + "notifications_attachment_file_audio": "ficheiro de audio", + "notifications_attachment_file_app": "ficheiro de app Android", + "notifications_attachment_file_document": "outro documento", + "notifications_click_copy_url_title": "Copiar URL da ligazón ao portapapeis", + "notifications_click_copy_url_button": "Copiar ligazón", + "notifications_actions_open_url_title": "Ir a {{url}}", + "notifications_none_for_topic_description": "Para enviar notificacións a este tema, simplemente usa PUT ou POST co URL do tema.", + "notifications_no_subscriptions_description": "Preme en \"{{linktext}} para crear ou subscribirte a un tema. Após, podes enviar mensaxes vía PUT ou POST e recibirás aquí as notificacións.", + "display_name_dialog_description": "Establecer un nome alternativo para o tema que será mostrado na lista de subscrición. Isto axudará a identificar os temas que teñan nomes complicados.", + "publish_dialog_tags_label": "Etiquetas", + "publish_dialog_tags_placeholder": "Lista de etiquetas separadas por vírgulas, ex. aviso, tarefa1", + "publish_dialog_priority_label": "Prioridade", + "publish_dialog_click_label": "URL a premer", + "publish_dialog_click_placeholder": "URL que se abre ao premer na notificación", + "publish_dialog_click_reset": "Desbotar o URL a premer", + "common_back": "Atrás", + "common_copy_to_clipboard": "Copiar ao portapapeis", + "signup_title": "Crear unha conta ntfy", + "signup_form_username": "Identificador", + "signup_form_password": "Contrasinal", + "signup_form_confirm_password": "Confirmar contrasinal", + "signup_form_button_submit": "Crear conta", + "login_form_button_submit": "Acceder", + "login_link_signup": "Crear conta", + "login_disabled": "O acceso está desactivado", + "action_bar_show_menu": "Mostrar menú", + "action_bar_toggle_mute": "Acalar/Reactivar as notificacións", + "message_bar_error_publishing": "Erro ao publicar a notificación", + "message_bar_publish": "Publicar mensaxe", + "nav_topics_title": "Temas subscritos", + "nav_button_documentation": "Documentación", + "nav_button_publish_message": "Publicar notificación", + "nav_button_subscribe": "Subscribirse ao tema", + "nav_button_muted": "Notificacións acaladas", + "nav_button_connecting": "conectando", + "nav_upgrade_banner_label": "Mellorar a ntfy Pro", + "alert_not_supported_description": "O teu navegador non ten soporte para notificacións.", + "notifications_priority_x": "Prioridade {{priority}}", + "notifications_attachment_link_expires": "a ligazón caduca o {{date}}", + "notifications_attachment_link_expired": "a ligazón de descarga caducou", + "notifications_attachment_file_image": "ficheiro de imaxe", + "notifications_attachment_file_video": "ficheiro de vídeo", + "notifications_actions_not_supported": "Acción non soportada na aplicación web", + "notifications_actions_http_request_title": "Enviar HTTP {{method}} a {{url}}", + "notifications_none_for_topic_title": "Aínda non recibiches ningunha notificación para este tema.", + "reserve_dialog_checkbox_label": "Reservar tema e configurar acceso", + "notifications_loading": "Cargando notificacións…", + "publish_dialog_base_url_placeholder": "URL de servizo, ex. https://exemplo.com", + "publish_dialog_topic_label": "Nome do tema", + "publish_dialog_topic_placeholder": "Nome do tema, ex. alertas_equipo", + "publish_dialog_topic_reset": "Restablecer tema", + "publish_dialog_title_label": "Título", + "publish_dialog_title_placeholder": "Título das notificacións, ex. Alerta de reunión", + "publish_dialog_message_label": "Mensaxe", + "publish_dialog_message_placeholder": "Escribe aquí a mensaxe", + "publish_dialog_email_label": "Correo electrónico", + "signup_form_toggle_password_visibility": "Cambiar visibilidade do contrasinal", + "signup_already_have_account": "Xa tes unha conta? Accede!", + "signup_error_creation_limit_reached": "Acadouse o límite de creación de contas", + "action_bar_logo_alt": "logo ntfy", + "action_bar_settings": "Axustes", + "action_bar_account": "Conta", + "action_bar_change_display_name": "Cambiar nome público", + "action_bar_reservation_add": "Reservar tema", + "action_bar_reservation_edit": "Cambiar a reserva", + "action_bar_reservation_delete": "Desbotar a reserva", + "action_bar_reservation_limit_reached": "Acadouse o límite", + "action_bar_toggle_action_menu": "Abrir/Pechar menú de accións", + "action_bar_profile_title": "Perfil", + "action_bar_profile_logout": "Pechar sesión", + "action_bar_sign_in": "Acceder", + "action_bar_sign_up": "Crear conta", + "message_bar_show_dialog": "Mostrar diálogo para publicar", + "nav_button_all_notifications": "Todas as notificacións", + "nav_button_account": "Conta", + "nav_button_settings": "Axustes", + "nav_upgrade_banner_description": "Reserva temas, máis mensaxes e correos electrónicos así como anexos máis grandes", + "alert_grant_title": "As notificacións están desactivadas", + "alert_grant_description": "Concede permiso no navegador para mostrar notificacións de escritorio.", + "alert_grant_button": "Conceder agora", + "alert_not_supported_title": "Non hai soporte para notificacións", + "alert_not_supported_context_description": "Só hai soporte para notificacións ao usar HTTPS. Esta é unha limitación da API de Notificacións.", + "notifications_list": "Lista de notificacións", + "notifications_list_item": "Notificación", + "notifications_mark_read": "Marcar como lida", + "notifications_delete": "Eliminar", + "notifications_tags": "Etiquetas", + "notifications_new_indicator": "Nova notificación", + "notifications_attachment_open_button": "Abrir anexo", + "notifications_click_open_button": "Abrir ligazón", + "notifications_none_for_any_title": "Non recibiches ningunha notificación.", + "notifications_none_for_any_description": "Para enviar notificacións ao tema, simplemente usa PUT ou POST ao URL do tema. Aquí tes un exemplo usando un dos teus temas.", + "notifications_no_subscriptions_title": "Semella que aínda non tes subscricións.", + "notifications_example": "Exemplo", + "display_name_dialog_title": "Cambiar nonme público", + "display_name_dialog_placeholder": "Nome público", + "publish_dialog_title_topic": "Publicar en {{topic}}", + "publish_dialog_title_no_topic": "Publicar notificación", + "publish_dialog_progress_uploading": "Enviando…", + "publish_dialog_progress_uploading_detail": "Enviando {{loaded}}/{{total}} ({{percent}}%) …", + "publish_dialog_message_published": "Notificación publicada", + "publish_dialog_attachment_limits_file_and_quota_reached": "supera o límite de ficheiros e cota {{fileSizeLimit}}, quedan {{remainingBytes}}", + "publish_dialog_attachment_limits_file_reached": "supera o límite para ficheiros {{fileSizeLimit}}", + "publish_dialog_attachment_limits_quota_reached": "supera a cota, quedan {{remainingBytes}}", + "publish_dialog_emoji_picker_show": "Elixe emoji", + "publish_dialog_priority_min": "Prioridade Mínima", + "publish_dialog_priority_low": "Prioridade baixa", + "publish_dialog_priority_default": "Prioridade por defecto", + "publish_dialog_priority_high": "Prioridade alta", + "publish_dialog_priority_max": "Prioridade Máxima", + "publish_dialog_base_url_label": "URL do servizo", + "notifications_more_details": "Para máis información, visita o sitio web ou le a documentación.", + "publish_dialog_call_label": "Chamada de teléfono", + "publish_dialog_call_reset": "Retirar chamada de teléfono", + "publish_dialog_delay_placeholder": "Adiar a entrega, ex. {{unixTimestamp}}, {{relativeTime}}, ou \"{{naturalLanguage}}\" (Só en inglés)", + "publish_dialog_other_features": "Outras características:", + "publish_dialog_chip_click_label": "Premer en URL", + "publish_dialog_chip_email_label": "Reenvío por correo", + "publish_dialog_chip_call_label": "Chamada de teléfono", + "publish_dialog_chip_attach_url_label": "Anexar ficheiro por URL", + "publish_dialog_button_cancel_sending": "Cancelar o envío", + "publish_dialog_button_cancel": "Cancelar", + "publish_dialog_button_send": "Enviar", + "publish_dialog_attached_file_title": "Ficheiro anexo:", + "publish_dialog_attached_file_filename_placeholder": "Nome do ficheiro anexo", + "publish_dialog_drop_file_here": "Soltar aquí o ficheiro", + "emoji_picker_search_placeholder": "Buscar emoji", + "subscribe_dialog_subscribe_title": "Subscribirse a un tema", + "publish_dialog_call_item": "Número de teléfono {{number}}", + "publish_dialog_email_placeholder": "Enderezo ao que reenviar a notificación, ex. xoana@exemplo.com", + "publish_dialog_email_reset": "Retirar reenvío ao correo", + "publish_dialog_attach_label": "URL do anexo", + "publish_dialog_attach_placeholder": "Anexa un ficheiro por URL, ex. https://f-droid.org/F-Droid.apk", + "publish_dialog_attach_reset": "Retirar URL do anexo", + "publish_dialog_filename_placeholder": "Nome do ficheiro anexo", + "publish_dialog_filename_label": "Nome do ficheiro", + "publish_dialog_delay_label": "Adiar", + "publish_dialog_delay_reset": "Retirar o adiadamento da entrega", + "publish_dialog_chip_attach_file_label": "Anexar ficheiro local", + "publish_dialog_chip_delay_label": "Entrega adiada", + "publish_dialog_chip_topic_label": "Cambiar tema", + "publish_dialog_details_examples_description": "Para ver exemplos e unha descrición polo miúdo das ferramentas de envío, le a documentación.", + "publish_dialog_checkbox_publish_another": "Publicar outra", + "emoji_picker_search_clear": "Limpar busca", + "publish_dialog_chip_call_no_verified_numbers_tooltip": "Números de teléfono non verificados", + "publish_dialog_attached_file_remove": "Retirar ficheiro anexo", + "account_upgrade_dialog_tier_features_no_calls": "Sen chamadas", + "account_upgrade_dialog_billing_contact_email": "Para preguntas sobre pagamentos, contacta con nós directamente.", + "account_tokens_dialog_title_create": "Crear token de acceso", + "prefs_reservations_dialog_title_edit": "Editar tema reservado", + "priority_default": "por defecto", + "prefs_notifications_min_priority_title": "Prioridade mínima", + "account_upgrade_dialog_tier_features_calls_one": "{{calls}} chamadas de teléfono diarias", + "account_upgrade_dialog_tier_current_label": "Actual", + "account_tokens_table_token_header": "Token", + "prefs_notifications_delete_after_never": "Nunca", + "prefs_users_description": "Engadir/eliminar usuarias dos temas protexidos. Ten en conta que as credenciais gárdanse na almacenaxe local do navegador.", + "subscribe_dialog_subscribe_description": "Os temas poderían non estar proxetidos con contrasinal, así que elixe un nome complicado de adiviñar. Unha vez subscrita, podes PUT/POST notificacións.", + "account_upgrade_dialog_interval_yearly_discount_save_up_to": "aforro ata un {{discount}}%", + "account_tokens_dialog_label": "Etiqueta, ex. notificación de Radarr", + "account_tokens_table_expires_header": "Caducidade", + "account_upgrade_dialog_proration_info": "Axuste: ao mellorar a un plan de pagamento superior, a diferencia vaise cobrar inmediatamente. Se degradas a conta a un plan inferior a diferencia usarase para pagar futuros períodos de pagamento.", + "prefs_reservations_dialog_access_label": "Acceso", + "account_usage_attachment_storage_title": "Almacenaxe dos anexos", + "prefs_users_dialog_username_label": "Identificador, ex. xoana", + "prefs_reservations_table_not_subscribed": "Non subscrita", + "account_upgrade_dialog_tier_features_emails_other": "{{emails}} correos diarios", + "prefs_notifications_min_priority_max_only": "Só prioridade máxima", + "account_upgrade_dialog_tier_features_calls_other": "{{calls}} chamadas de teléfono diarias", + "prefs_notifications_sound_description_some": "As notificacións sonan co ton {{sound}} ao chegar", + "prefs_reservations_edit_button": "Editar acceso ao tema", + "account_tokens_dialog_expires_never": "O token non caduca", + "subscribe_dialog_login_title": "Require inciar sesión", + "account_tokens_dialog_expires_x_days": "O token caduca en {{days}} días", + "prefs_reservations_table_everyone_read_only": "Podo publicar e subscribirme, calquera pode subscribirse", + "prefs_reservations_table_everyone_deny_all": "Só eu podo publicar e subscribirme", + "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} tema reservado", + "subscribe_dialog_login_button_login": "Acceder", + "account_upgrade_dialog_tier_features_no_reservations": "Sen temas reservados", + "prefs_users_table_cannot_delete_or_edit": "Non se pode eliminar ou editar unha usuaria coa sesión iniciada", + "prefs_notifications_delete_after_three_hours_description": "As notificacións autoelimínanse após tres horas", + "prefs_notifications_delete_after_three_hours": "Após tres horas", + "prefs_notifications_min_priority_description_x_or_higher": "Mostrar as notificacións se a prioridade é {{number}} {{name}} ou superior", + "reservation_delete_dialog_description": "Ao eliminar a reserva cedes a propiedade do tema, e permites que outras persoas poidan reservalo. Podes manter ou eliminar as mensaxes e anexos existentes.", + "prefs_reservations_table_everyone_read_write": "Calquera pode publicar e subscribirse", + "prefs_reservations_dialog_title_delete": "Eliminar a reserva do tema", + "prefs_users_table": "Táboa de usuarias", + "prefs_reservations_table_topic_header": "Tema", + "reservation_delete_dialog_submit_button": "Eliminar a reserva", + "prefs_reservations_limit_reached": "Acadaches o límite de temas que podes reservar.", + "account_upgrade_dialog_interval_monthly": "Mensual", + "prefs_users_add_button": "Engadir usuaria", + "account_upgrade_dialog_tier_features_messages_other": "{{messages}} mensaxes diarias", + "prefs_appearance_language_title": "Idioma", + "prefs_notifications_delete_after_one_day_description": "As notificacións autoelimínanse após un día", + "account_tokens_table_never_expires": "Non caduca", + "account_tokens_delete_dialog_title": "Desbotar token de acceso", + "prefs_notifications_delete_after_one_month": "Após un mes", + "account_tokens_delete_dialog_description": "Antes de borrar o token de acceso mira que ningunha aplicación ou programa o está usando. Esta acción non pode desfacerse.", + "account_upgrade_dialog_button_cancel": "Cancelar", + "account_tokens_table_label_header": "Etiqueta", + "account_upgrade_dialog_billing_contact_website": "Para preguntas sobre pagamentos, vai ao noso sitiio web.", + "prefs_notifications_delete_after_never_description": "As notificacións non se eliminarán nunca automáticamente", + "account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} temas reservados", + "prefs_notifications_sound_description_none": "As notificacións non reproducen un ton ao chegar", + "account_tokens_description": "Usar tokens de acceso ao publicar e subscribirte a través da API de ntfy, así non tes que enviar as credenciais. Le a documentación para saber máis.", + "prefs_reservations_table": "Táboa cos temas reservados", + "account_upgrade_dialog_button_cancel_subscription": "Cancelar subscrición", + "account_upgrade_dialog_tier_features_emails_one": "{{emails}} correo diario", + "account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} por ficheiro", + "prefs_reservations_description": "Podes reservar nomes de temas para uso personal. Ao reservar un tema tes a propiedade sobre del, e permíteche definir os permisos de acceso para outras usuarias sobre o tema.", + "prefs_users_description_no_sync": "Usuarias e contrasinais non están sincronizados coa túa conta.", + "account_tokens_dialog_title_edit": "Editar token de acceso", + "prefs_users_table_base_url_header": "URL do servizo", + "account_upgrade_dialog_tier_features_messages_one": "{{mensaxes}} mensaxe diaria", + "account_upgrade_dialog_reservations_warning_one": "O nivel seleccionado permite reservar menos temas que o nivel actual. Antes de cambiar de nivel, elimina unha reserva polo menos. Podes eliminar as reservas nos Axustes.", + "prefs_users_table_user_header": "Usuaria", + "error_boundary_stack_trace": "Trazas do problema", + "prefs_users_dialog_password_label": "Contrasinal", + "prefs_notifications_delete_after_one_week": "Após unha semana", + "prefs_reservations_delete_button": "Restablecer acceso ao tema", + "prefs_notifications_delete_after_one_week_description": "As notificacións autoelimínanse após unha semana", + "error_boundary_unsupported_indexeddb_description": "A app ntfy web precisa a función IndexedDB, e o teu navegador non ten soporte para IndexedDB no modo privado.

Aínda que é unha mágoa, tampouco ten moito senso usar a app ntfy web en modo privado, porque todo se garda na almacenaxe do navegador. Podes aprender máis sobre isto neste tema de GitHub, ou comentarnos o que che parece en Discord ou Matrix.", + "subscribe_dialog_subscribe_button_cancel": "Cancelar", + "account_basics_tier_description": "O nivel da túa conta", + "prefs_reservations_dialog_title_add": "Reservar tema", + "account_upgrade_dialog_cancel_warning": "Isto vai cancelar a túa subscrición, e degradar a túa conta o {{date}}. Nesa data, as reservas de temas así como as mensaxes na caché do servidor van ser eliminadas.", + "prefs_notifications_sound_title": "Ton da notificación", + "prefs_notifications_min_priority_default_and_higher": "Prioridade por defecto e superior", + "prefs_reservations_table_access_header": "Acceso", + "account_tokens_table_copied_to_clipboard": "Copiouse o token de acceso", + "account_tokens_dialog_expires_x_hours": "O token caduca en {{hours}} horas", + "prefs_users_edit_button": "Editar usuaria", + "account_upgrade_dialog_title": "Cambiar facturación da conta", + "priority_low": "baixa", + "prefs_reservations_table_click_to_subscribe": "Preme para subscribirte", + "error_boundary_description": "Isto non debería pasar. Lamentámolo.
Se tes un minuto, informa en GitHub, ou fáinolo saber en Discord ou Matrix.", + "priority_min": "min", + "prefs_notifications_min_priority_description_any": "Mostrar todas as notificacións, obviando a prioridade", + "error_boundary_gathering_info": "Obter máis info…", + "error_boundary_unsupported_indexeddb_title": "Non hai soporte para a navegación privada", + "prefs_notifications_delete_after_one_day": "Após un día", + "error_boundary_title": "vaite!, ntfy fallou", + "reservation_delete_dialog_action_keep_description": "As mensaxes e anexos que están no servidor serán visibles públicamente para quen saiba o nome do tema.", + "prefs_reservations_add_button": "Engadir tema reservado", + "prefs_reservations_title": "Temas reservados", + "prefs_reservations_dialog_description": "Ao reservar un tema tes a propiedade sobre el, e permíteche definir os permisos de acceso para outras usuarias.", + "account_tokens_delete_dialog_submit_button": "Eliminar definitivamente o token", + "prefs_notifications_title": "Notificacións", + "account_tokens_title": "Tokens de acceso", + "prefs_reservations_dialog_topic_label": "Tema", + "prefs_users_title": "Xestionar usuarias", + "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} anual. Pagamento mensual.", + "account_tokens_dialog_expires_unchanged": "Deixar a data de caducidade sen cambiar", + "error_boundary_button_copy_stack_trace": "Copiar trazas do problema", + "account_tokens_dialog_title_delete": "Eliminar token de acceso", + "reservation_delete_dialog_action_keep_title": "Manter as mensaxes e anexos gardados", + "prefs_notifications_sound_no_sound": "Sen ton", + "account_upgrade_dialog_interval_yearly": "Anual", + "account_upgrade_dialog_button_redirect_signup": "Crea unha conta", + "account_tokens_dialog_button_cancel": "Cancelar", + "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} cobrado anualmente. Aforro {{save}}.", + "prefs_notifications_min_priority_high_and_higher": "Prioridade alta e superior", + "priority_max": "máx", + "prefs_users_delete_button": "Eliminar usuaria", + "prefs_notifications_min_priority_any": "Calquera prioridade", + "account_tokens_dialog_expires_label": "O token caduca o", + "prefs_notifications_delete_after_title": "Desbotar notificacións", + "account_upgrade_dialog_interval_yearly_discount_save": "aforro {{discount}}%", + "prefs_users_dialog_title_edit": "Editar usuaria", + "prefs_notifications_min_priority_low_and_higher": "Prioridade baixa e superior", + "account_tokens_dialog_button_update": "Actualizar token", + "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} almacenaxe total", + "prefs_reservations_table_everyone_write_only": "Podo publicar e subscribirme, calquera pode publicar", + "prefs_appearance_title": "Aparencia", + "account_tokens_table_cannot_delete_or_edit": "Non se pode editar ou desbotar o token da sesión actual", + "prefs_notifications_sound_play": "Reproducir ton seleccionado", + "account_tokens_table_last_access_header": "Último acceso", + "account_tokens_table_last_origin_tooltip": "Desde o enderezo IP {{ip}}, preme para detalles", + "account_upgrade_dialog_tier_price_per_month": "mes", + "account_tokens_table_current_session": "Sesión do navegador actual", + "account_upgrade_dialog_button_pay_now": "Paga e subscríbete", + "reservation_delete_dialog_action_delete_title": "Eliminar mensaxes e anexos gardados", + "reservation_delete_dialog_action_delete_description": "As mensaxes e anexos vanse borrar definitivamente. Esta acción non ten volta.", + "prefs_notifications_delete_after_one_month_description": "As notificacións autoelimínanse após un mes", + "prefs_users_dialog_base_url_label": "URL do servizo, ex. https://ntfy.sh", + "account_upgrade_dialog_tier_selected_label": "Seleccionado", + "account_upgrade_dialog_button_update_subscription": "Actualizar subscrición", + "priority_high": "alta", + "account_delete_dialog_billing_warning": "Ao eliminar a conta tamén cancelas o pagamento das subscricións. Non poderás volver acceder ao taboleiro de pagamentos.", + "prefs_notifications_min_priority_description_max": "Mostrar notificacións se a prioridade é 5 (máx)", + "account_upgrade_dialog_reservations_warning_other": "O nivel seleccionado permite reservar menos temas que o nivel actual. Antes de cambiar de nivel, elimina {{count}} reservas polo menos. Podes eliminar as reservas nos Axustes.", + "prefs_users_dialog_title_add": "Engadir usuaria", + "account_tokens_dialog_button_create": "Crear token", + "account_tokens_table_create_token_button": "Crear token de acceso", + "account_basics_tier_interval_monthly": "mensual", + "account_basics_tier_canceled_subscription": "A sua suscripción foi cancelada e vostede será degradado a unha conta gratuita o {{date}}.", + "account_basics_password_dialog_current_password_incorrect": "Contrasinal incorrecto", + "account_basics_phone_numbers_dialog_number_label": "Número de teléfono", + "account_basics_password_dialog_button_submit": "Modificar contrasinal", + "account_basics_username_title": "Usuario", + "account_basics_phone_numbers_dialog_check_verification_button": "Código de confirmación", + "account_usage_messages_title": "Mesaxes publicados", + "account_basics_phone_numbers_dialog_verify_button_sms": "Enviar SMS", + "account_basics_tier_change_button": "Cambiar", + "account_basics_phone_numbers_dialog_description": "Para usar a característica de chamadas de teléfono, vostede debe engadir e verificar ao menos un número de teléfono. A verificación pode ser realizada vía SMS ou a través de chamada.", + "account_delete_title": "Borrar conta", + "account_delete_dialog_label": "Contrasinal", + "account_basics_tier_admin_suffix_with_tier": "(con tier {{tier}})", + "subscribe_dialog_login_username_label": "Nome de usuario, ex. phil", + "subscribe_dialog_error_user_not_authorized": "Usuario {{username}} non autorizado", + "account_basics_title": "Conta", + "account_basics_phone_numbers_no_phone_numbers_yet": "Aínda non hay números de teléfono", + "subscribe_dialog_subscribe_button_generate_topic_name": "Xerar nome", + "subscribe_dialog_login_password_label": "Contrasinal", + "subscribe_dialog_subscribe_button_subscribe": "Subscribirse", + "account_basics_phone_numbers_dialog_title": "Engadir número de teléfono", + "account_basics_username_admin_tooltip": "É vostede Admin", + "account_delete_dialog_description": "Isto borrará permanentemente a túa conta, incluido todos os datos almacenados no servidor. Despois do borrado, o teu nome de usuario non estará dispoñible durante 7 días. Se realmente queres proceder, por favor confirme co seu contrasinal na caixa inferior.", + "account_usage_reservations_none": "Non hai temas reservados para esta conta", + "subscribe_dialog_subscribe_topic_placeholder": "Nome do tema, ex. phil_alertas", + "account_usage_title": "Uso", + "account_basics_tier_upgrade_button": "Mexorar a Pro", + "subscribe_dialog_error_topic_already_reserved": "Tema xa reservado", + "account_basics_tier_admin_suffix_no_tier": "(sen tier)", + "account_basics_tier_payment_overdue": "O pago está retrasado. Por favor, revise o seu método de pago o a súa conta será degradada pronto.", + "account_basics_phone_numbers_description": "Para notificacións telefónicas", + "account_basics_tier_free": "De balde", + "account_basics_tier_admin": "Admin", + "account_delete_dialog_button_cancel": "Cancelar", + "account_basics_password_description": "Modificar o contrasinal da conta", + "account_usage_calls_title": "Chamadas realizadas", + "account_basics_tier_basic": "Básico", + "account_basics_phone_numbers_copied_to_clipboard": "Número de teléfono copiado no portapapeis", + "account_basics_tier_title": "Tipo de conta", + "account_usage_cannot_create_portal_session": "Non foi posible abrir o portal de pagos", + "account_delete_description": "Borrar permanentemente a túa conta", + "account_basics_phone_numbers_dialog_number_placeholder": "ex. +1222333444", + "account_basics_phone_numbers_dialog_code_placeholder": "ex. 123456", + "account_basics_tier_manage_billing_button": "Xestionar pagos", + "account_basics_username_description": "Ei, ese eres ti ❤", + "account_basics_password_dialog_confirm_password_label": "Confirmar contrasinal", + "account_basics_tier_interval_yearly": "anual", + "account_delete_dialog_button_submit": "Borrar permanentemente a conta", + "account_basics_phone_numbers_dialog_channel_call": "Chamada", + "account_basics_password_title": "Contrasinal", + "account_basics_password_dialog_new_password_label": "Novo contrasinal", + "account_usage_of_limit": "de {{limit}}", + "subscribe_dialog_error_user_anonymous": "anónimo", + "account_usage_basis_ip_description": "Estadísticas de uso e límites para esta conta están basados na sua IP, polo que poden estar compartidos con outros usuarios. Os limites mostrados son aproximados, basados nos ratios de limite existentes.", + "account_basics_password_dialog_title": "Modificar contrasinal", + "account_usage_limits_reset_daily": "Límite de uso é reiniciado diariamente a medianoite (UTC(", + "account_usage_unlimited": "Sen límites", + "account_basics_phone_numbers_title": "Números de teléfono", + "account_basics_password_dialog_current_password_label": "Contrasinal actual", + "subscribe_dialog_subscribe_base_url_label": "URL do servizo", + "account_usage_reservations_title": "Temas reservados", + "account_usage_calls_none": "Non se poden realizar chamadas con esta conta", + "subscribe_dialog_subscribe_use_another_label": "Usar outro servidor", + "account_basics_phone_numbers_dialog_code_label": "Código de verificación", + "account_basics_tier_paid_until": "Suscripción pagada ata {{date}}, e vaise auto-renovar", + "account_usage_attachment_storage_description": "{{filesize}} por arquivo, borrado despois de {{expiry}}", + "account_basics_phone_numbers_dialog_verify_button_call": "Chámame", + "account_usage_emails_title": "Emails enviados", + "account_basics_phone_numbers_dialog_channel_sms": "SMS", + "subscribe_dialog_login_description": "Este tema está protexido por contrasinal. Por favor, introduza o usuario e contrasinal para subscribirse." +} diff --git a/web/public/static/langs/hu.json b/web/public/static/langs/hu.json index 975d8d97..e4e0b85b 100644 --- a/web/public/static/langs/hu.json +++ b/web/public/static/langs/hu.json @@ -8,12 +8,12 @@ "message_bar_error_publishing": "Hiba történt az értesítés elküldése közben", "nav_button_all_notifications": "Összes értesítés", "nav_topics_title": "Feliratkozott témák", - "alert_grant_title": "Az értesítések le vannak tiltva", - "alert_grant_description": "Engedélyezd a böngészőnek, hogy asztali értesítéseket jeleníttessen meg.", + "alert_notification_permission_required_title": "Az értesítések le vannak tiltva", + "alert_notification_permission_required_description": "Engedélyezd a böngészőnek, hogy asztali értesítéseket jeleníttessen meg.", "nav_button_settings": "Beállítások", "nav_button_documentation": "Dokumentáció", "nav_button_publish_message": "Értesítés küldése", - "alert_grant_button": "Engedélyezés", + "alert_notification_permission_required_button": "Engedélyezés", "alert_not_supported_title": "Nem támogatott funkció", "notifications_copied_to_clipboard": "Másolva a vágólapra", "notifications_tags": "Címkék", @@ -84,7 +84,7 @@ "subscribe_dialog_login_description": "Ez a téma jelszóval védett. Jelentkezz be a feliratkozáshoz.", "subscribe_dialog_login_username_label": "Felhasználónév, pl: jozsi", "subscribe_dialog_login_password_label": "Jelszó", - "subscribe_dialog_login_button_back": "Vissza", + "common_back": "Vissza", "subscribe_dialog_login_button_login": "Belépés", "subscribe_dialog_error_user_anonymous": "névtelen", "subscribe_dialog_error_user_not_authorized": "A(z) {{username}} felhasználónak nincs hozzáférése", diff --git a/web/public/static/langs/id.json b/web/public/static/langs/id.json index b442a220..b9732bae 100644 --- a/web/public/static/langs/id.json +++ b/web/public/static/langs/id.json @@ -22,8 +22,8 @@ "common_add": "Tambahkan", "nav_topics_title": "Topik yang dilanggani", "nav_button_subscribe": "Berlangganan ke topik", - "alert_grant_title": "Notifikasi dinonaktifkan", - "alert_grant_description": "Berikan izin ke peramban untuk menampilkan notifikasi desktop.", + "alert_notification_permission_required_title": "Notifikasi dinonaktifkan", + "alert_notification_permission_required_description": "Berikan izin ke peramban untuk menampilkan notifikasi desktop.", "alert_not_supported_description": "Notifikasi tidak didukung dalam peramban Anda.", "notifications_attachment_open_title": "Pergi ke {{url}}", "notifications_attachment_open_button": "Buka lampiran", @@ -33,7 +33,7 @@ "notifications_click_open_button": "Buka tautan", "publish_dialog_topic_placeholder": "Nama topik, mis. pemberitahuan_andi", "nav_button_publish_message": "Publikasikan notifikasi", - "alert_grant_button": "Berikan sekarang", + "alert_notification_permission_required_button": "Berikan sekarang", "notifications_copied_to_clipboard": "Disalin ke papan klip", "notifications_tags": "Tanda", "notifications_attachment_copy_url_title": "Salin URL lampiran ke papan klip", @@ -116,7 +116,7 @@ "common_save": "Simpan", "prefs_appearance_title": "Tampilan", "subscribe_dialog_login_password_label": "Kata sandi", - "subscribe_dialog_login_button_back": "Kembali", + "common_back": "Kembali", "prefs_notifications_sound_title": "Suara notifikasi", "prefs_notifications_min_priority_low_and_higher": "Prioritas rendah dan lebih tinggi", "prefs_notifications_min_priority_default_and_higher": "Prioritas bawaan dan lebih tinggi", @@ -278,7 +278,7 @@ "account_tokens_table_expires_header": "Kedaluwarsa", "account_tokens_table_never_expires": "Tidak pernah kedaluwarsa", "account_tokens_table_current_session": "Sesi peramban saat ini", - "account_tokens_table_copy_to_clipboard": "Salin ke papan klip", + "common_copy_to_clipboard": "Salin ke papan klip", "account_tokens_table_copied_to_clipboard": "Token akses disalin", "account_tokens_table_cannot_delete_or_edit": "Tidak dapat menyunting atau menghapus token sesi saat ini", "account_tokens_table_create_token_button": "Buat token akses", @@ -352,5 +352,34 @@ "account_upgrade_dialog_tier_price_per_month": "bulan", "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} per bulan. Ditagih setiap bulan.", "account_upgrade_dialog_billing_contact_email": "Untuk pertanyaan penagihan, silakan hubungi kami secara langsung.", - "account_upgrade_dialog_billing_contact_website": "Untuk pertanyaan penagihan, silakan menuju ke situs web kami." + "account_upgrade_dialog_billing_contact_website": "Untuk pertanyaan penagihan, silakan menuju ke situs web kami.", + "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} topik yang direservasi", + "account_upgrade_dialog_tier_features_emails_one": "{{emails}} surel harian", + "account_upgrade_dialog_tier_features_messages_one": "{{messages}} pesan harian", + "publish_dialog_call_label": "Panggilan telepon", + "publish_dialog_call_placeholder": "Nomor telepon untuk dipanggil dengan pesan, mis. +622223334444, atau 'yes'", + "account_basics_phone_numbers_title": "Nomor telepon", + "account_basics_phone_numbers_dialog_description": "Untuk menggunakan fitur notifikasi telepon, Anda perlu menambahkan dan memverifikasi setidaknya satu nomor telepon. Verifikasi dapat dilakukan melalui SMS atau panggilan telepon.", + "account_basics_phone_numbers_no_phone_numbers_yet": "Belum ada nomor telepon", + "account_basics_phone_numbers_dialog_title": "Tambahkan nomor telepon", + "account_basics_phone_numbers_dialog_number_label": "Nomor telepon", + "account_basics_phone_numbers_dialog_number_placeholder": "mis. +62222333444", + "account_basics_phone_numbers_dialog_verify_button_sms": "Kirim SMS", + "account_basics_phone_numbers_dialog_channel_call": "Panggil", + "account_usage_calls_title": "Panggilan telepon dilakukan", + "account_usage_calls_none": "Tidak ada panggilan telepon yang dapat dilakukan dengan akun ini", + "account_upgrade_dialog_tier_features_calls_other": "{{calls}} panggilan telepon harian", + "publish_dialog_call_reset": "Hapus panggilan telepon", + "account_basics_phone_numbers_description": "Untuk notifikasi panggilan telepon", + "account_basics_phone_numbers_copied_to_clipboard": "Nomor telepon disalin ke papan klip", + "publish_dialog_chip_call_label": "Panggilan telepon", + "account_basics_phone_numbers_dialog_verify_button_call": "Panggil saya", + "account_basics_phone_numbers_dialog_code_placeholder": "mis. 123456", + "account_basics_phone_numbers_dialog_check_verification_button": "Konfirmasi kode", + "account_basics_phone_numbers_dialog_channel_sms": "SMS", + "account_upgrade_dialog_tier_features_calls_one": "{{calls}} panggilan telepon harian", + "account_upgrade_dialog_tier_features_no_calls": "Tidak ada panggilan telepon", + "account_basics_phone_numbers_dialog_code_label": "Kode verifikasi", + "publish_dialog_call_item": "Panggil nomor telepon {{number}}", + "publish_dialog_chip_call_no_verified_numbers_tooltip": "Tidak ada nomor telepon terverifikasi" } diff --git a/web/public/static/langs/it.json b/web/public/static/langs/it.json index 87ea04a4..807689a8 100644 --- a/web/public/static/langs/it.json +++ b/web/public/static/langs/it.json @@ -15,8 +15,8 @@ "nav_button_subscribe": "Iscriviti al topic", "nav_button_muted": "Notifiche disattivate", "nav_button_connecting": "connessione", - "alert_grant_title": "Le notifiche sono disabilitate", - "alert_grant_button": "Concedi ora", + "alert_notification_permission_required_title": "Le notifiche sono disabilitate", + "alert_notification_permission_required_button": "Concedi ora", "notifications_list": "Elenco notifiche", "notifications_list_item": "Notifiche", "notifications_mark_read": "Segna come letto", @@ -155,7 +155,7 @@ "alert_not_supported_description": "Le notifiche non sono supportate nel tuo browser.", "nav_button_documentation": "Documentazione", "notifications_actions_http_request_title": "Invia HTTP {{method}} a {{url}}", - "alert_grant_description": "Concedi al tuo browser l'autorizzazione a visualizzare le notifiche sul desktop.", + "alert_notification_permission_required_description": "Concedi al tuo browser l'autorizzazione a visualizzare le notifiche sul desktop.", "alert_not_supported_title": "Notifiche non supportate", "notifications_attachment_file_app": "file app Android", "notifications_no_subscriptions_description": "Fai clic sul link \"{{linktext}}\" per creare o iscriverti a un topic. Successivamente, puoi inviare messaggi tramite PUT o POST e riceverai le notifiche qui.", @@ -178,7 +178,7 @@ "prefs_notifications_sound_play": "Riproduci il suono selezionato", "prefs_notifications_min_priority_title": "Priorità minima", "subscribe_dialog_login_description": "Questo argomento è protetto da password. Per favore inserisci username e password per iscriverti.", - "subscribe_dialog_login_button_back": "Indietro", + "common_back": "Indietro", "subscribe_dialog_error_user_not_authorized": "Utente {{username}} non autorizzato", "prefs_notifications_title": "Notifiche", "prefs_notifications_delete_after_title": "Elimina le notifiche", @@ -256,5 +256,56 @@ "account_basics_tier_admin_suffix_no_tier": "(nessun livello)", "account_basics_tier_basic": "Base", "account_basics_tier_free": "Gratuito", - "account_usage_emails_title": "Email inviate" + "account_usage_emails_title": "Email inviate", + "account_usage_cannot_create_portal_session": "Impossibile aprire il portale di pagamento", + "account_delete_title": "Elimina account", + "account_basics_username_description": "Hey, sei tu ❤", + "publish_dialog_call_item": "Chiama numero {{number}}", + "common_copy_to_clipboard": "Copia negli appunti", + "publish_dialog_call_label": "Chiamata telefonica", + "publish_dialog_call_reset": "Rimuovi chiamata telefonica", + "publish_dialog_chip_call_label": "Chiamata telefonica", + "publish_dialog_chip_call_no_verified_numbers_tooltip": "Nessun numero verificato", + "account_basics_phone_numbers_title": "Numeri di telefono", + "account_basics_phone_numbers_dialog_description": "Per usare la funzionalità di notifica tramite chiamata telefonica, devi aggiungere e verificare almeno un numero di telefono. La verifica può essere fatta tramite SMS o chiamata telefonica.", + "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} topic riservato", + "account_upgrade_dialog_billing_contact_email": "Per domande di fatturazione, contattaci direttamente.", + "account_upgrade_dialog_tier_current_label": "Attuale", + "account_basics_phone_numbers_dialog_number_label": "Numero di telefono", + "account_basics_phone_numbers_dialog_check_verification_button": "Conferma codice", + "account_basics_phone_numbers_dialog_verify_button_sms": "Invia SMS", + "account_basics_phone_numbers_no_phone_numbers_yet": "Ancora nessun numero di telefono", + "account_basics_phone_numbers_dialog_title": "Aggiungi un numero di telefono", + "account_upgrade_dialog_button_cancel": "Cancella", + "account_upgrade_dialog_billing_contact_website": "Per domande di fatturazione, visita per favore in nostro sito.", + "account_upgrade_dialog_button_cancel_subscription": "Cancella iscrizione", + "account_basics_phone_numbers_description": "Per notifiche via chiamata", + "account_basics_phone_numbers_copied_to_clipboard": "Numero di telefono copiato negli appunti", + "account_basics_phone_numbers_dialog_number_placeholder": "p. e. +391234567890", + "account_basics_phone_numbers_dialog_code_placeholder": "p. e. 123456", + "account_tokens_title": "Token d'accesso", + "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} all'anno. Addebitato annualmente.", + "account_basics_phone_numbers_dialog_channel_call": "Chiama", + "account_upgrade_dialog_button_redirect_signup": "Iscriviti ora", + "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} addebitato annualmente. Risparmia {{save}}.", + "account_upgrade_dialog_tier_price_per_month": "mese", + "account_upgrade_dialog_button_pay_now": "Paga ora e isciviti", + "account_basics_phone_numbers_dialog_code_label": "Codice di verifica", + "account_basics_phone_numbers_dialog_verify_button_call": "Chiamami", + "account_basics_phone_numbers_dialog_channel_sms": "SMS", + "account_upgrade_dialog_tier_selected_label": "Selezionato", + "account_upgrade_dialog_button_update_subscription": "Aggiorna iscrizione", + "account_usage_attachment_storage_title": "Archivio allegati", + "account_delete_dialog_description": "Il tuo account sarà permanentemente cancellato assieme a tutti i tuoi dati presenti sul server. Dopo la cancellazione, la tua username non sarà disponibile per 7 giorni. Se desideri davvero procedere, inserisci la tua password nella seguente casella.", + "account_delete_dialog_button_cancel": "Annulla", + "account_usage_calls_title": "Chiamate effettuate", + "account_delete_description": "Elimina permanentemente il tuo account", + "account_delete_dialog_button_submit": "Elimina il tuo account permanentemente", + "account_usage_basis_ip_description": "Le statistiche di utilizzo e i limiti per questo account sono basati sul tuo indirizzo IP, quindi potrebbero essere in condivisione con altri utenti. I limiti mostrati sopra sono approssimazioni basate sui limiti esistenti.", + "account_usage_calls_none": "Questo account non può effettuare chiamate", + "account_delete_dialog_billing_warning": "Eliminando il tuo account perderai immediatamente il tuo abbonamento. Non potrai più accedere alla dashboard di fatturazione.", + "account_delete_dialog_label": "Password", + "account_upgrade_dialog_tier_features_no_reservations": "Nessun argomento riservato", + "account_upgrade_dialog_tier_features_messages_one": "{{messages}} messaggi giornalieri", + "account_upgrade_dialog_reservations_warning_one": "Il livello selezionato consente meno argomenti riservati rispetto al livello corrente. Prima di cambiare il livello, si prega di eliminare almeno una prenotazione. È possibile rimuovere le prenotazioni nel Impostazioni." } diff --git a/web/public/static/langs/ja.json b/web/public/static/langs/ja.json index 65a15982..84afc30b 100644 --- a/web/public/static/langs/ja.json +++ b/web/public/static/langs/ja.json @@ -20,7 +20,7 @@ "subscribe_dialog_login_description": "このトピックはログインする必要があります。ユーザー名とパスワードを入力してください。", "subscribe_dialog_login_username_label": "ユーザー名, 例) phil", "subscribe_dialog_login_password_label": "パスワード", - "subscribe_dialog_login_button_back": "戻る", + "common_back": "戻る", "subscribe_dialog_login_button_login": "ログイン", "prefs_notifications_min_priority_high_and_higher": "優先度高 およびそれ以上", "prefs_notifications_min_priority_max_only": "優先度最高のみ", @@ -28,13 +28,13 @@ "message_bar_type_message": "メッセージを入力してください", "nav_topics_title": "購読しているトピック", "nav_button_subscribe": "トピックを購読", - "alert_grant_description": "ブラウザのデスクトップ通知を許可してください。", - "alert_grant_button": "許可する", + "alert_notification_permission_required_description": "ブラウザのデスクトップ通知を許可してください。", + "alert_notification_permission_required_button": "許可する", "notifications_attachment_link_expires": "リンクは {{date}} に失効します", "notifications_click_copy_url_button": "リンクをコピー", "notifications_none_for_topic_description": "トピックに通知を送信するには、トピックのURLにPUTかPOSTしてください。", "nav_button_publish_message": "通知を送信", - "alert_grant_title": "通知は無効化されています", + "alert_notification_permission_required_title": "通知は無効化されています", "alert_not_supported_title": "通知機能はサポートされていません", "notifications_tags": "タグ", "notifications_attachment_copy_url_button": "URLをコピー", @@ -258,7 +258,7 @@ "account_tokens_table_expires_header": "期限", "account_tokens_table_never_expires": "無期限", "account_tokens_table_current_session": "現在のブラウザセッション", - "account_tokens_table_copy_to_clipboard": "クリップボードにコピー", + "common_copy_to_clipboard": "クリップボードにコピー", "account_tokens_table_copied_to_clipboard": "アクセストークンをコピーしました", "account_tokens_table_cannot_delete_or_edit": "現在のセッショントークンは編集または削除できません", "account_tokens_table_create_token_button": "アクセストークンを生成", @@ -352,5 +352,33 @@ "account_upgrade_dialog_tier_price_per_month": "月", "account_upgrade_dialog_tier_price_billed_monthly": "年間{{price}}。月毎の支払い。", "account_upgrade_dialog_tier_price_billed_yearly": "年間{{price}}の支払い。{{save}}節約。", - "account_upgrade_dialog_billing_contact_website": "支払いに関する質問は、ウェブサイトを参照して下さい。" + "account_upgrade_dialog_billing_contact_website": "支払いに関する質問は、ウェブサイトを参照して下さい。", + "account_upgrade_dialog_tier_features_messages_one": "毎日 {{messages}} メッセージ", + "account_upgrade_dialog_tier_features_reservations_one": "予約済みトピック {{reservations}} 件", + "account_upgrade_dialog_tier_features_emails_one": "毎日メール {{emails}} 件", + "publish_dialog_call_label": "電話", + "publish_dialog_call_item": "電話番号 {{number}}", + "account_basics_phone_numbers_title": "電話番号", + "account_usage_calls_none": "このアカウントからは電話を発信できません", + "account_usage_calls_title": "電話を発信しました", + "account_upgrade_dialog_tier_features_calls_one": "電話 1日 {{calls}} 回", + "account_upgrade_dialog_tier_features_no_calls": "電話なし", + "publish_dialog_call_reset": "電話番号を削除", + "publish_dialog_chip_call_label": "電話番号", + "account_basics_phone_numbers_dialog_description": "電話通知機能を使うには、最低ひとつの電話番号を追加して認証する必要があります。認証はSMSまたは電話で実施できます。", + "account_basics_phone_numbers_description": "電話通知", + "account_basics_phone_numbers_dialog_title": "電話番号を追加", + "account_basics_phone_numbers_no_phone_numbers_yet": "電話番号はまだありません", + "account_basics_phone_numbers_copied_to_clipboard": "電話番号がクリップボードにコピーされました", + "account_basics_phone_numbers_dialog_number_label": "電話番号", + "account_basics_phone_numbers_dialog_number_placeholder": "例 +1222333444", + "account_basics_phone_numbers_dialog_verify_button_sms": "SMSを送信", + "account_basics_phone_numbers_dialog_verify_button_call": "自分に電話する", + "account_basics_phone_numbers_dialog_code_label": "確認コード", + "account_basics_phone_numbers_dialog_code_placeholder": "例 123456", + "account_basics_phone_numbers_dialog_check_verification_button": "確認コード", + "account_upgrade_dialog_tier_features_calls_other": "電話 1日 {{calls}} 回", + "publish_dialog_chip_call_no_verified_numbers_tooltip": "認証済み電話番号がありません", + "account_basics_phone_numbers_dialog_channel_sms": "SMS", + "account_basics_phone_numbers_dialog_channel_call": "電話する" } diff --git a/web/public/static/langs/ko.json b/web/public/static/langs/ko.json index 67c31280..ed35db70 100644 --- a/web/public/static/langs/ko.json +++ b/web/public/static/langs/ko.json @@ -17,9 +17,9 @@ "nav_button_subscribe": "주제 구독하기", "nav_button_muted": "알림 음소거됨", "nav_button_connecting": "연결중", - "alert_grant_title": "알림이 비활성화되어 있습니다", - "alert_grant_description": "데스크톱 알림을 받기 위해서는 브라우저에서 권한을 부여해야 합니다.", - "alert_grant_button": "권한 부여하기", + "alert_notification_permission_required_title": "알림이 비활성화되어 있습니다", + "alert_notification_permission_required_description": "데스크톱 알림을 받기 위해서는 브라우저에서 권한을 부여해야 합니다.", + "alert_notification_permission_required_button": "권한 부여하기", "alert_not_supported_title": "알림이 지원되지 않습니다", "notifications_list_item": "알림", "notifications_mark_read": "읽음으로 표시", @@ -93,7 +93,7 @@ "subscribe_dialog_error_user_not_authorized": "사용자 {{username}} 은(는) 인증되지 않았습니다", "subscribe_dialog_login_username_label": "사용자 이름, 예를 들면 phil", "subscribe_dialog_login_password_label": "비밀번호", - "subscribe_dialog_login_button_back": "뒤로가기", + "common_back": "뒤로가기", "subscribe_dialog_login_button_login": "로그인", "prefs_notifications_title": "알림", "prefs_notifications_sound_title": "알림 효과음", diff --git a/web/public/static/langs/nb_NO.json b/web/public/static/langs/nb_NO.json index 312791da..13cd419e 100644 --- a/web/public/static/langs/nb_NO.json +++ b/web/public/static/langs/nb_NO.json @@ -9,7 +9,7 @@ "nav_button_settings": "Innstillinger", "nav_button_documentation": "Dokumentasjon", "nav_topics_title": "Abonnerte emner", - "alert_grant_title": "Merknader er avskrudd", + "alert_notification_permission_required_title": "Merknader er avskrudd", "alert_not_supported_title": "Merknader støttes ikke", "notifications_copied_to_clipboard": "Kopiert til utklippstavlen", "notifications_attachment_copy_url_title": "Kopier vedleggsnettadresse til utklippstavlen", @@ -98,7 +98,7 @@ "priority_default": "forvalg", "priority_high": "høy", "priority_max": "maks.", - "alert_grant_button": "Innvilg nå", + "alert_notification_permission_required_button": "Innvilg nå", "publish_dialog_topic_label": "Emnenavn", "prefs_notifications_delete_after_one_day_description": "Merknader slettes automatisk etter én dag", "notifications_click_copy_url_button": "Kopier lenke", @@ -113,7 +113,7 @@ "prefs_notifications_delete_after_one_week_description": "Merknader slettes automatisk etter én uke", "prefs_notifications_delete_after_one_month_description": "Merknader slettes automatisk etter én måned", "priority_min": "min.", - "subscribe_dialog_login_button_back": "Tilbake", + "common_back": "Tilbake", "prefs_notifications_delete_after_three_hours": "Etter tre timer", "prefs_users_table_base_url_header": "Tjeneste-nettadresse", "common_cancel": "Avbryt", @@ -133,7 +133,7 @@ "publish_dialog_chip_delay_label": "Forsink leveringen", "publish_dialog_details_examples_description": "For eksempler og en detaljert beskrivelse av alle sendefunksjoner, se dokumentasjonen.", "publish_dialog_base_url_placeholder": "Tjeneste-URL, f.eks. https://example.com", - "alert_grant_description": "Gi nettleseren din tillatelse til å vise skrivebordsvarsler.", + "alert_notification_permission_required_description": "Gi nettleseren din tillatelse til å vise skrivebordsvarsler.", "alert_not_supported_description": "Varsler støttes ikke i nettleseren din.", "notifications_attachment_file_app": "Android-app-fil", "notifications_no_subscriptions_description": "Klikk på \"{{linktext}}\"-koblingen for å opprette eller abonnere på et emne. Etter det kan du sende meldinger via PUT eller POST, og du vil motta varsler her.", @@ -190,5 +190,10 @@ "error_boundary_unsupported_indexeddb_title": "Privat surfing støttes ikke", "action_bar_account": "Konto", "action_bar_profile_settings": "Innstillinger", - "nav_button_account": "Konto" + "nav_button_account": "Konto", + "signup_title": "Opprett en ntfy konto", + "signup_form_username": "Brukernavn", + "signup_form_password": "Passord", + "signup_form_button_submit": "Meld deg på", + "signup_form_confirm_password": "Bekreft passord" } diff --git a/web/public/static/langs/nl.json b/web/public/static/langs/nl.json index 3c7adb49..65bd8eee 100644 --- a/web/public/static/langs/nl.json +++ b/web/public/static/langs/nl.json @@ -35,16 +35,16 @@ "nav_button_subscribe": "Abonneer op onderwerp", "nav_button_muted": "Notificaties gedempt", "nav_button_connecting": "verbinden", - "alert_grant_title": "Notificaties zijn uitgeschakeld", - "alert_grant_description": "Verleen je browser toestemming voor het weergeven van notificaties.", - "alert_grant_button": "Nu toestaan", + "alert_notification_permission_required_title": "Notificaties zijn uitgeschakeld", + "alert_notification_permission_required_description": "Verleen je browser toestemming voor het weergeven van notificaties.", + "alert_notification_permission_required_button": "Nu toestaan", "alert_not_supported_title": "Notificaties zijn niet ondersteund", "notifications_list": "Notificatielijst", "notifications_list_item": "Notificatie", "notifications_mark_read": "Markeer als gelezen", "notifications_delete": "Verwijder", "notifications_copied_to_clipboard": "Gekopieerd naar klembord", - "notifications_tags": "Tags", + "notifications_tags": "Labels", "notifications_priority_x": "Prioriteit {{priority}}", "notifications_new_indicator": "Nieuwe notificatie", "notifications_attachment_image": "Afbeelding bijlage", @@ -140,7 +140,7 @@ "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", + "common_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", @@ -226,7 +226,7 @@ "account_usage_unlimited": "Onbeperkt", "account_basics_tier_title": "Account type", "account_basics_tier_admin": "Beheerder", - "account_basics_tier_admin_suffix_with_tier": "", + "account_basics_tier_admin_suffix_with_tier": "(met {{tier}} niveau)", "account_basics_tier_basic": "Basis", "account_basics_tier_free": "Gratis", "account_basics_tier_change_button": "Wijzig", @@ -248,5 +248,137 @@ "subscribe_dialog_error_topic_already_reserved": "Onderwerp al gereserveerd", "account_basics_password_dialog_title": "Wijzig wachtwoord", "account_usage_limits_reset_daily": "Gebruikslimieten worden dagelijks om middernacht (UTC) gereset", - "account_basics_tier_upgrade_button": "Upgrade naar Pro" + "account_basics_tier_upgrade_button": "Upgrade naar Pro", + "account_upgrade_dialog_title": "Accountniveau wijzigen", + "account_upgrade_dialog_interval_yearly_discount_save": "bespaar {{discount}}%", + "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} jaarlijks gefactureerd. Bespaar {{save}}.", + "account_upgrade_dialog_cancel_warning": "Hiermee wordt uw abonnement opgezegd en wordt uw account gedowngraded op {{date}}. Op die datum worden onderwerpreserveringen en berichten in de cache op de server verwijderd .", + "account_tokens_dialog_button_update": "Token bijwerken", + "account_upgrade_dialog_proration_info": "Pro rata: Bij een upgrade tussen betaalde abonnementen wordt het prijsverschil onmiddellijk in rekening gebracht. Wanneer u downgradet naar een lager niveau, wordt het saldo gebruikt om toekomstige factureringsperioden te betalen.", + "account_upgrade_dialog_reservations_warning_one": "Het geselecteerde niveau staat minder gereserveerde onderwerpen toe dan uw huidige niveau. Voordat u uw niveau wijzigt, , moet u ten minste één reservering verwijderen . U kunt reserveringen verwijderen in de Instellingen.", + "account_upgrade_dialog_reservations_warning_other": "Het geselecteerde niveau staat minder gereserveerde onderwerpen toe dan uw huidige niveau. Voordat u uw niveau wijzigt, moet u ten minste {{count}} reserveringen verwijderen. U kunt reserveringen verwijderen in de Instellingen.", + "account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} gereserveerde onderwerpen", + "account_upgrade_dialog_billing_contact_email": "Neem voor vragen over facturering rechtstreeks contact met ons op.", + "account_tokens_table_token_header": "Token", + "account_tokens_table_never_expires": "Verloopt nooit", + "account_tokens_table_current_session": "Huidige browsersessie", + "prefs_reservations_table_everyone_read_only": "Ik kan publiceren en abonneren, iedereen kan zich abonneren", + "prefs_reservations_table_everyone_write_only": "Ik kan publiceren en abonneren, iedereen kan publiceren", + "account_usage_reservations_none": "Geen gereserveerde onderwerpen voor dit account", + "account_usage_attachment_storage_title": "Bijlage-opslag", + "account_usage_attachment_storage_description": "{{filesize}} per bestand, verwijderd na {{expiry}}", + "account_delete_dialog_description": "Hiermee wordt uw account definitief verwijderd, inclusief alle gegevens die op de server zijn opgeslagen. Na verwijdering is uw gebruikersnaam 7 dagen niet beschikbaar. Als u echt wilt doorgaan, bevestig dan met uw wachtwoord in het onderstaande vak.", + "account_delete_dialog_billing_warning": "Als u uw account verwijdert, wordt ook uw facturering onmiddellijk geannuleerd. U heeft dan geen toegang meer tot het factureringsdashboard.", + "account_tokens_dialog_button_cancel": "Annuleren", + "reservation_delete_dialog_submit_button": "Reservering verwijderen", + "prefs_reservations_table_everyone_deny_all": "Alleen ik kan publiceren en abonneren", + "reservation_delete_dialog_description": "Het verwijderen van een reservering geeft het eigendom van het onderwerp op en stelt anderen in staat het te reserveren. U kunt bestaande berichten en bijlagen behouden of verwijderen.", + "account_basics_tier_interval_monthly": "maandelijks", + "account_basics_tier_interval_yearly": "jaarlijks", + "account_usage_basis_ip_description": "Gebruiksstatistieken en -limieten voor dit account zijn gebaseerd op uw IP-adres en kunnen dus worden gedeeld met andere gebruikers. De hierboven weergegeven limieten zijn bij benadering gebaseerd op de bestaande limieten.", + "account_usage_cannot_create_portal_session": "Kan factureringsportaal niet openen", + "account_delete_title": "Account verwijderen", + "account_delete_description": "Verwijder uw account definitief", + "account_delete_dialog_label": "Wachtwoord", + "account_delete_dialog_button_cancel": "Annuleren", + "account_delete_dialog_button_submit": "Verwijder uw account definitief", + "account_upgrade_dialog_interval_monthly": "Maandelijks", + "account_upgrade_dialog_interval_yearly": "Jaarlijks", + "account_upgrade_dialog_interval_yearly_discount_save_up_to": "bespaar tot {{discount}}%", + "account_upgrade_dialog_tier_features_no_reservations": "Geen gereserveerde onderwerpen", + "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} totale opslag", + "account_upgrade_dialog_tier_current_label": "Huidig", + "account_upgrade_dialog_button_update_subscription": "Abonnement bijwerken", + "account_tokens_title": "Toegangstokens", + "account_tokens_description": "Gebruik toegangstokens bij het publiceren en abonneren via de ntfy API, zodat u uw accountgegevens niet hoeft op te sturen. Bekijk de documentatie voor meer informatie.", + "account_tokens_table_label_header": "Label", + "account_tokens_table_cannot_delete_or_edit": "Kan huidige sessietoken niet bewerken of verwijderen", + "account_tokens_dialog_expires_label": "Toegangstoken verloopt over", + "account_tokens_dialog_expires_unchanged": "Vervaldatum ongewijzigd laten", + "account_tokens_dialog_expires_x_hours": "Token verloopt over {{hours}} uur", + "account_tokens_dialog_expires_x_days": "Token verloopt over {{days}} dagen", + "account_tokens_dialog_expires_never": "Token verloopt nooit", + "account_tokens_delete_dialog_title": "Toegangstoken verwijderen", + "account_tokens_delete_dialog_description": "Voordat u een toegangstoken verwijdert, moet u ervoor zorgen dat er geen toepassingen of scripts actief gebruik van maken. Deze actie kan niet ongedaan worden gemaakt.", + "prefs_users_table_cannot_delete_or_edit": "Kan ingelogde gebruiker niet verwijderen of bewerken", + "prefs_reservations_title": "Gereserveerde onderwerpen", + "prefs_reservations_description": "U kunt hier onderwerpnamen reserveren voor persoonlijk gebruik. Door een onderwerp te reserveren, wordt u eigenaar van het onderwerp en kunt u toegangsmachtigingen voor andere gebruikers voor het onderwerp definiëren.", + "prefs_reservations_limit_reached": "Je hebt je limiet voor gereserveerde onderwerpen bereikt.", + "prefs_reservations_add_button": "Gereserveerd onderwerp toevoegen", + "prefs_reservations_table_click_to_subscribe": "Klik om je te abonneren", + "prefs_reservations_dialog_title_add": "Onderwerp reserveren", + "prefs_reservations_dialog_title_edit": "Gereserveerd onderwerp bewerken", + "prefs_reservations_dialog_title_delete": "Onderwerpreservering verwijderen", + "prefs_reservations_dialog_description": "Door een onderwerp te reserveren, wordt u eigenaar van het onderwerp en kunt u toegangsmachtigingen voor andere gebruikers voor het onderwerp definiëren.", + "prefs_reservations_dialog_topic_label": "Onderwerp", + "prefs_reservations_dialog_access_label": "Toegang", + "reservation_delete_dialog_action_keep_title": "Bewaar in de cache opgeslagen berichten en bijlagen", + "reservation_delete_dialog_action_keep_description": "Berichten en bijlagen die in de cache op de server zijn opgeslagen, worden publiekelijk zichtbaar voor mensen die de onderwerpnaam kennen.", + "reservation_delete_dialog_action_delete_description": "Berichten en bijlagen in de cache worden permanent verwijderd. Deze actie kan niet ongedaan gemaakt worden.", + "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} gereserveerd onderwerp", + "account_upgrade_dialog_tier_features_messages_one": "{{messages}} dagelijks bericht", + "account_upgrade_dialog_tier_features_messages_other": "{{messages}} dagelijkse berichten", + "account_upgrade_dialog_tier_features_emails_one": "{{emails}} dagelijkse e-mail", + "account_upgrade_dialog_tier_features_emails_other": "{{emails}} dagelijkse e-mails", + "account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} per bestand", + "account_upgrade_dialog_tier_price_per_month": "maand", + "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} per jaar. Maandelijks gefactureerd.", + "account_upgrade_dialog_tier_selected_label": "Geselecteerd", + "account_upgrade_dialog_billing_contact_website": "Raadpleeg voor vragen over facturering onze website.", + "account_upgrade_dialog_button_cancel": "Annuleren", + "account_upgrade_dialog_button_redirect_signup": "Nu aanmelden", + "account_upgrade_dialog_button_pay_now": "Nu betalen en inschrijven", + "account_upgrade_dialog_button_cancel_subscription": "Abonnement opzeggen", + "account_tokens_table_last_access_header": "Laatste toegang", + "account_tokens_table_expires_header": "Verloopt op", + "common_copy_to_clipboard": "Kopieer naar klembord", + "account_tokens_table_copied_to_clipboard": "Toegangstoken gekopieerd", + "account_tokens_delete_dialog_submit_button": "Token definitief verwijderen", + "prefs_users_description_no_sync": "Gebruikers en wachtwoorden worden niet gesynchroniseerd met uw account.", + "reservation_delete_dialog_action_delete_title": "Verwijder in de cache opgeslagen berichten en bijlagen", + "account_basics_tier_description": "Het niveau van uw account", + "account_basics_tier_admin_suffix_no_tier": "(geen niveau)", + "account_basics_tier_manage_billing_button": "Facturering beheren", + "account_usage_messages_title": "Gepubliceerde berichten", + "account_usage_emails_title": "E-mails verzonden", + "account_usage_reservations_title": "Gereserveerde onderwerpen", + "account_tokens_table_create_token_button": "Toegangstoken maken", + "account_tokens_table_last_origin_tooltip": "Vanaf IP-adres {{ip}}, klik om op te zoeken", + "account_tokens_dialog_title_create": "Toegangstoken maken", + "account_tokens_dialog_title_edit": "Toegangstoken bewerken", + "account_tokens_dialog_title_delete": "Toegangstoken verwijderen", + "account_tokens_dialog_label": "Label, bijv. Radarr-meldingen", + "account_tokens_dialog_button_create": "Token maken", + "prefs_reservations_edit_button": "Onderwerptoegang bewerken", + "prefs_reservations_delete_button": "Toegang tot onderwerp resetten", + "prefs_reservations_table": "Tabel met gereserveerde onderwerpen", + "prefs_reservations_table_topic_header": "Onderwerp", + "prefs_reservations_table_access_header": "Toegang", + "prefs_reservations_table_everyone_read_write": "Iedereen kan publiceren en abonneren", + "prefs_reservations_table_not_subscribed": "Niet geabonneerd", + "publish_dialog_call_label": "Telefoongesprek", + "publish_dialog_call_reset": "Telefoongesprek verwijderen", + "publish_dialog_chip_call_label": "Telefoongesprek", + "account_basics_phone_numbers_title": "Telefoonnummers", + "account_basics_phone_numbers_description": "Voor meldingen via telefoongesprekken", + "account_basics_phone_numbers_no_phone_numbers_yet": "Nog geen telefoonnummers", + "account_basics_phone_numbers_dialog_verify_button_call": "Bel me", + "account_upgrade_dialog_tier_features_calls_one": "{{calls}} dagelijkse telefoontjes", + "account_basics_phone_numbers_copied_to_clipboard": "Telefoonnummer gekopieerd naar klembord", + "publish_dialog_call_item": "Bel telefoonnummer {{nummer}}", + "account_basics_phone_numbers_dialog_check_verification_button": "Bevestig code", + "publish_dialog_chip_call_no_verified_numbers_tooltip": "Geen geverifieerde telefoonnummers", + "account_basics_phone_numbers_dialog_channel_call": "Telefoongesprek", + "account_basics_phone_numbers_dialog_number_label": "Telefoonnummer", + "account_basics_phone_numbers_dialog_channel_sms": "SMS", + "account_basics_phone_numbers_dialog_code_placeholder": "bijv. 123456", + "account_upgrade_dialog_tier_features_calls_other": "{{calls}} dagelijkse telefoontjes", + "account_upgrade_dialog_tier_features_no_calls": "Geen telefoontjes", + "account_basics_phone_numbers_dialog_description": "Als u de functie voor oproepmeldingen wilt gebruiken, moet u ten minste één telefoonnummer toevoegen en verifiëren. Verificatie kan worden gedaan via sms of een telefoontje.", + "account_basics_phone_numbers_dialog_title": "Telefoonnummer toevoegen", + "account_basics_phone_numbers_dialog_number_placeholder": "bijv. +1222333444", + "account_basics_phone_numbers_dialog_verify_button_sms": "Stuur SMS", + "account_basics_phone_numbers_dialog_code_label": "Verificatiecode", + "account_usage_calls_title": "Aantal telefoontjes", + "account_usage_calls_none": "Met dit account kan niet worden gebeld" } diff --git a/web/public/static/langs/pl.json b/web/public/static/langs/pl.json index 5e6bcbe5..8733345c 100644 --- a/web/public/static/langs/pl.json +++ b/web/public/static/langs/pl.json @@ -9,9 +9,9 @@ "nav_button_all_notifications": "Wszystkie powiadomienia", "nav_button_documentation": "Dokumentacja", "nav_button_muted": "Powiadomienia wyciszone", - "alert_grant_title": "Powiadomienia są wyłączone", - "alert_grant_description": "Udziel przeglądarce pozwolenia na wyświetlanie powiadomień na pulpicie.", - "alert_grant_button": "Pozwól teraz", + "alert_notification_permission_required_title": "Powiadomienia są wyłączone", + "alert_notification_permission_required_description": "Udziel przeglądarce pozwolenia na wyświetlanie powiadomień na pulpicie.", + "alert_notification_permission_required_button": "Pozwól teraz", "alert_not_supported_title": "Powiadomienia nie są obsługiwane", "alert_not_supported_description": "Powiadomienia nie są obsługiwane przez Twoją przeglądarkę.", "notifications_list": "Lista powiadomień", @@ -107,7 +107,7 @@ "subscribe_dialog_login_username_label": "Nazwa użytkownika, np. phil", "subscribe_dialog_login_password_label": "Hasło", "publish_dialog_button_cancel": "Anuluj", - "subscribe_dialog_login_button_back": "Powrót", + "common_back": "Powrót", "subscribe_dialog_login_button_login": "Zaloguj się", "subscribe_dialog_error_user_not_authorized": "Użytkownik {{username}} nie ma uprawnień", "subscribe_dialog_error_user_anonymous": "anonim", @@ -253,7 +253,7 @@ "account_tokens_table_expires_header": "Termin ważności", "account_tokens_table_never_expires": "Bezterminowy", "account_tokens_table_current_session": "Aktualna sesja przeglądarki", - "account_tokens_table_copy_to_clipboard": "Kopiuj do schowka", + "common_copy_to_clipboard": "Kopiuj do schowka", "account_tokens_table_copied_to_clipboard": "Token został skopiowany", "account_tokens_table_cannot_delete_or_edit": "Nie można edytować ani usunąć tokenu aktualnej sesji", "account_tokens_table_create_token_button": "Utwórz token dostępowy", diff --git a/web/public/static/langs/pt.json b/web/public/static/langs/pt.json index 196baf4f..898d6eed 100644 --- a/web/public/static/langs/pt.json +++ b/web/public/static/langs/pt.json @@ -15,8 +15,8 @@ "nav_button_subscribe": "Subscrever tópico", "nav_button_muted": "Notificações desativadas", "nav_button_connecting": "A ligar", - "alert_grant_title": "As notificações estão desativadas", - "alert_grant_description": "Conceder permissão ao seu navegador para mostrar notificações.", + "alert_notification_permission_required_title": "As notificações estão desativadas", + "alert_notification_permission_required_description": "Conceder permissão ao seu navegador para mostrar notificações.", "alert_not_supported_title": "Notificações não suportadas", "notifications_list": "Lista de notificações", "alert_not_supported_description": "As notificações não são suportadas pelo seu navegador.", @@ -119,7 +119,7 @@ "action_bar_logo_alt": "logótipo do ntfy", "action_bar_settings": "Configurações", "message_bar_show_dialog": "Mostrar caixa de publicação", - "alert_grant_button": "Conceder agora", + "alert_notification_permission_required_button": "Conceder agora", "publish_dialog_attachment_limits_file_reached": "excede o limite de ficheiro de {{fileSizeLimit}}", "publish_dialog_emoji_picker_show": "Escolher emoji", "publish_dialog_priority_max": "Prioridade máxima", @@ -144,7 +144,7 @@ "subscribe_dialog_login_description": "Esse tópico é protegido por palavra-passe. Por favor insira um nome de utilizador e palavra-passe para subscrever.", "subscribe_dialog_login_username_label": "Nome, por exemplo: \"filipe\"", "subscribe_dialog_login_password_label": "Palavra-passe", - "subscribe_dialog_login_button_back": "Voltar", + "common_back": "Voltar", "subscribe_dialog_login_button_login": "Autenticar", "subscribe_dialog_error_user_anonymous": "anónimo", "prefs_notifications_title": "Notificações", @@ -214,5 +214,17 @@ "login_link_signup": "Registar", "action_bar_reservation_add": "Reservar tópico", "action_bar_sign_up": "Registar", - "nav_button_account": "Conta" + "nav_button_account": "Conta", + "common_copy_to_clipboard": "Copiar", + "nav_upgrade_banner_label": "Atualizar para ntfy Pro", + "alert_not_supported_context_description": "Notificações são suportadas apenas sobre HTTPS. Essa é uma limitação da API de Notificações.", + "display_name_dialog_title": "Alterar nome mostrado", + "display_name_dialog_description": "Configura um nome alternativo ao tópico que é mostrado na lista de assinaturas. Isto ajuda a identificar tópicos com nomes complicados mais facilmente.", + "display_name_dialog_placeholder": "Nome exibido", + "reserve_dialog_checkbox_label": "Reservar tópico e configurar acesso", + "publish_dialog_call_label": "Chamada telefônica", + "publish_dialog_call_placeholder": "Número de telefone para ligar com a mensagem, ex: +12223334444, ou 'Sim'", + "publish_dialog_call_reset": "Remover chamada telefônica", + "publish_dialog_chip_call_label": "Chamada telefônica", + "subscribe_dialog_subscribe_button_generate_topic_name": "Gerar nome" } diff --git a/web/public/static/langs/pt_BR.json b/web/public/static/langs/pt_BR.json index 79622be3..79b2c14a 100644 --- a/web/public/static/langs/pt_BR.json +++ b/web/public/static/langs/pt_BR.json @@ -7,9 +7,9 @@ "nav_button_all_notifications": "Todas notificações", "nav_button_settings": "Configurações", "nav_button_subscribe": "Inscrever no tópico", - "alert_grant_title": "Notificações estão desativadas", - "alert_grant_description": "Conceder ao navegador permissão para mostrar notificações.", - "alert_grant_button": "Conceder agora", + "alert_notification_permission_required_title": "Notificações estão desativadas", + "alert_notification_permission_required_description": "Conceder ao navegador permissão para mostrar notificações.", + "alert_notification_permission_required_button": "Conceder agora", "alert_not_supported_title": "Notificações não são suportadas", "alert_not_supported_description": "Notificações não são suportadas pelo seu navagador.", "notifications_copied_to_clipboard": "Copiado para a área de transferência", @@ -93,7 +93,7 @@ "prefs_notifications_min_priority_low_and_higher": "Baixa prioridade e acima", "prefs_notifications_min_priority_default_and_higher": "Prioridade padrão e acima", "subscribe_dialog_login_password_label": "Senha", - "subscribe_dialog_login_button_back": "Voltar", + "common_back": "Voltar", "prefs_notifications_min_priority_high_and_higher": "Alta prioridade e acima", "prefs_notifications_min_priority_max_only": "Apenas prioridade máxima", "prefs_notifications_delete_after_title": "Apagar notificações", @@ -188,5 +188,36 @@ "prefs_users_edit_button": "Editar usuário", "prefs_users_delete_button": "Excluir usuário", "error_boundary_unsupported_indexeddb_title": "Navegação anônima não suportada", - "error_boundary_unsupported_indexeddb_description": "O ntfy web app precisa do IndexedDB para funcionar, e seu navegador não suporta IndexedDB no modo de navegação privada.

Embora isso seja lamentável, também não faz muito sentido usar o ntfy web app no modo de navegação privada de qualquer maneira, porque tudo é armazenado no armazenamento do navegador. Você pode ler mais sobre isso nesta edição do GitHub, ou falar conosco em Discord ou Matrix." + "error_boundary_unsupported_indexeddb_description": "O ntfy web app precisa do IndexedDB para funcionar, e seu navegador não suporta IndexedDB no modo de navegação privada.

Embora isso seja lamentável, também não faz muito sentido usar o ntfy web app no modo de navegação privada de qualquer maneira, porque tudo é armazenado no armazenamento do navegador. Você pode ler mais sobre isso nesta edição do GitHub, ou falar conosco em Discord ou Matrix.", + "action_bar_reservation_add": "Reserve topic", + "action_bar_reservation_edit": "Change reservation", + "signup_disabled": "Registrar está desativado", + "signup_error_username_taken": "Usuário {{username}} já existe", + "signup_error_creation_limit_reached": "Limite de criação de contas atingido", + "action_bar_reservation_delete": "Remover reserva", + "action_bar_account": "Conta", + "action_bar_change_display_name": "Change display name", + "common_copy_to_clipboard": "Copiar para área de transferência", + "login_link_signup": "Registrar", + "login_title": "Entrar na sua conta ntfy", + "login_form_button_submit": "Entrar", + "login_disabled": "Login está desabilitado", + "action_bar_reservation_limit_reached": "Limite atingido", + "action_bar_profile_title": "Perfil", + "action_bar_profile_settings": "Configurações", + "action_bar_profile_logout": "Sair", + "action_bar_sign_in": "Entrar", + "action_bar_sign_up": "Registrar", + "nav_button_account": "Conta", + "signup_title": "Criar uma conta ntfy", + "signup_form_username": "Usuário", + "signup_form_password": "Senha", + "signup_form_confirm_password": "Confirmar senha", + "signup_form_button_submit": "Registrar", + "account_basics_phone_numbers_title": "Telefones", + "signup_form_toggle_password_visibility": "Ativar visibilidade de senha", + "signup_already_have_account": "Já possui uma conta? Entrar!", + "nav_upgrade_banner_label": "Atualizar para ntfy Pro", + "account_basics_phone_numbers_dialog_description": "Para usar o recurso de notificação de chamada, é necessários adicionar e verificar pelo menos um número de telefone. A verificação pode ser feita por SMS ou chamada telefônica.", + "account_basics_phone_numbers_description": "Para notificações de chamada telefônica" } diff --git a/web/public/static/langs/ro.json b/web/public/static/langs/ro.json index d9cb66e3..bfb90b50 100644 --- a/web/public/static/langs/ro.json +++ b/web/public/static/langs/ro.json @@ -9,5 +9,97 @@ "message_bar_type_message": "Scrie un mesaj aici", "message_bar_error_publishing": "Eroare la publicarea notificării", "action_bar_profile_title": "Profil", - "action_bar_profile_settings": "Setări" + "action_bar_profile_settings": "Setări", + "nav_button_settings": "Setări", + "nav_button_connecting": "conectare", + "notifications_attachment_file_video": "fișier video", + "publish_dialog_priority_default": "Prioritate default", + "publish_dialog_priority_high": "Prioritate înaltă", + "publish_dialog_priority_max": "Max. prioritate", + "publish_dialog_message_placeholder": "Introdu un mesaj aici", + "nav_button_subscribe": "Abonează-te la topic", + "nav_upgrade_banner_label": "Upgrade la ntfy Pro", + "nav_upgrade_banner_description": "Rezervă topic-uri, mai multe mesaje și email-uri, și atașamente mai mari", + "common_back": "Înapoi", + "nav_button_account": "Cont", + "nav_button_documentation": "Documentație", + "nav_button_publish_message": "Publică notificarea", + "alert_notification_permission_required_title": "Notificările sunt dezactivate", + "alert_notification_permission_required_button": "Permite acum", + "alert_not_supported_title": "Notificările nu sunt acceptate", + "alert_not_supported_description": "Notificările nu sunt acceptate în browser.", + "alert_notification_permission_required_description": "Permite browser-ului să afișeze notificări.", + "notifications_list": "Lista de notificări", + "notifications_list_item": "Notificare", + "notifications_mark_read": "Marchează ca citit", + "notifications_delete": "Șterge", + "notifications_copied_to_clipboard": "Copiat în clipboard", + "notifications_tags": "Tag-uri", + "notifications_new_indicator": "Notificare nouă", + "notifications_attachment_image": "Imagine atașament", + "notifications_attachment_copy_url_title": "Copiază URL-ul atașamentului în clipboard", + "notifications_attachment_copy_url_button": "Copiază URL", + "notifications_attachment_open_title": "Mergi la {{url}}", + "notifications_attachment_link_expires": "link-ul expiră {{date}}", + "notifications_actions_not_supported": "Acțiune neacceptată în aplicația web", + "notifications_actions_http_request_title": "Trimite {{method}} HTTP la {{url}}", + "notifications_none_for_topic_title": "N-ați primit încă notificări pe acest subiect.", + "notifications_none_for_topic_description": "Pentru a trimite notificări pe acest subiect, setați PUT sau POST pe URL-ul subiectului.", + "notifications_none_for_any_title": "N-ați primit nici o notificare.", + "notifications_none_for_any_description": "Pentru a trimite notificări pe acest subiect, setează PUT sau POST pe URL-ul subiectului. Uite un exemplu cu unul dintre subiectele tale.", + "notifications_no_subscriptions_title": "Se pare că nu ai nici o înscriere.", + "notifications_no_subscriptions_description": "Click pe link-ul \"{{linktext}}\" ca sa creezi o înscriere la un subiect. După aceea, poți trimite mesaje via PUT sau POST și vei primi notificări aici.", + "notifications_example": "Exemplu", + "notifications_more_details": "Pentru mai multe informații, vezi site-ul web sau documentația.", + "display_name_dialog_title": "Schimbă numele afișat", + "display_name_dialog_description": "Setează un nume alternativ pentru subiect care este afișat în lista de înscrieri. Va ajuta la ușurarea identificării subiectelor cu nume complexe.", + "display_name_dialog_placeholder": "Nume afișat", + "reserve_dialog_checkbox_label": "Rezervă subiectul și configurează accesul", + "publish_dialog_progress_uploading": "Încărcare…", + "publish_dialog_progress_uploading_detail": "Încărcare {{loaded}}/{{total}} ({{percent}}%) …", + "publish_dialog_message_published": "Notificare publicată", + "publish_dialog_attachment_limits_file_and_quota_reached": "depășește {{fileSizeLimit}} limita fișierului și cota, {{remainingBytes}} mai rămân", + "publish_dialog_attachment_limits_file_reached": "depășește {{fileSizeLimit}} limita fișierului", + "publish_dialog_attachment_limits_quota_reached": "depășește cota, {{remainingBytes}} mai rămân", + "publish_dialog_priority_min": "Min. prioritate", + "publish_dialog_base_url_label": "URL serviciu", + "publish_dialog_base_url_placeholder": "URL serviciu, ex: https://example.com", + "publish_dialog_topic_label": "Nume subiect", + "publish_dialog_topic_placeholder": "Nume subiect, ex: alerte_phil", + "publish_dialog_topic_reset": "Resetare subiect", + "publish_dialog_title_label": "Titlu", + "publish_dialog_title_placeholder": "Titlu notificare, ex: Alerta spațiu disc", + "publish_dialog_message_label": "Mesaj", + "publish_dialog_tags_label": "Tag-uri", + "publish_dialog_tags_placeholder": "Lista de tag-uri separate prin virgula, ex: avertizare,srv1-backup", + "publish_dialog_priority_label": "Prioritate", + "publish_dialog_click_label": "Click URL", + "publish_dialog_click_placeholder": "URL deschis când notificarea este selectată", + "publish_dialog_click_reset": "Șterge URL selecție", + "publish_dialog_email_label": "E-mail", + "signup_form_confirm_password": "Confirmă parola", + "action_bar_account": "Cont", + "action_bar_change_display_name": "Schimbă numele afișat", + "action_bar_reservation_limit_reached": "Limita atinsă", + "common_cancel": "Anulează", + "common_save": "Salvează", + "common_add": "Adaugă", + "signup_form_password": "Parolă", + "publish_dialog_title_topic": "Publică în {{topic}}", + "publish_dialog_title_no_topic": "Publică notificare", + "nav_button_all_notifications": "Toate notificările", + "notifications_priority_x": "Prioritate {{priority}}", + "notifications_attachment_file_image": "fișier imagine", + "notifications_attachment_open_button": "Deschide atașament", + "notifications_attachment_file_audio": "fișier audio", + "notifications_actions_open_url_title": "Mergi la {{url}}", + "notifications_attachment_file_document": "alt document", + "notifications_attachment_link_expired": "link-ul de descărcare expirat", + "notifications_attachment_file_app": "fișier aplicație Android", + "notifications_click_copy_url_title": "Copiază URL-ul în clipboard", + "notifications_click_copy_url_button": "Copiază link", + "notifications_click_open_button": "Deschide link", + "publish_dialog_emoji_picker_show": "Alege un emoji", + "notifications_loading": "Încărcare notificări…", + "publish_dialog_priority_low": "Prioritate joasă" } diff --git a/web/public/static/langs/ru.json b/web/public/static/langs/ru.json index 42025e43..71cef5a4 100644 --- a/web/public/static/langs/ru.json +++ b/web/public/static/langs/ru.json @@ -8,7 +8,7 @@ "notifications_none_for_topic_description": "Чтобы отправить уведомление на данную тему, просто сделаете PUT или POST-запрос на URL-адрес этой темы.", "notifications_none_for_any_description": "Чтобы отправить уведомление на тему, просто сделаете PUT или POST-запрос на её URL-адрес. Вот пример с использованием одной из ваших тем.", "notifications_no_subscriptions_title": "Похоже, что у вас ещё нет подписок.", - "alert_grant_description": "Разрешите браузеру показывать уведомления.", + "alert_notification_permission_required_description": "Разрешите браузеру показывать уведомления.", "notifications_no_subscriptions_description": "Нажмите на ссылку \"{{linktext}}\", чтобы создать или подписаться на тему. После этого Вы сможете отправлять сообщения используя PUT или POST-запросы и получать уведомления здесь.", "notifications_example": "Пример", "notifications_more_details": "Для более подробной информации, посетите наш сайт или документацию.", @@ -51,13 +51,13 @@ "nav_button_documentation": "Документация", "nav_button_publish_message": "Опубликовать уведомление", "nav_button_subscribe": "Подписаться на тему", - "alert_grant_button": "Разрешить", + "alert_notification_permission_required_button": "Разрешить", "notifications_attachment_copy_url_button": "Скопировать URL-адрес", "notifications_attachment_open_title": "Перейти на {{url}}", "notifications_attachment_link_expired": "срок действия ссылки для скачивания истёк", "notifications_click_copy_url_button": "Скопировать ссылку", "notifications_none_for_any_title": "Вы ещё не получали никаких уведомлений.", - "alert_grant_title": "Уведомления отключены", + "alert_notification_permission_required_title": "Уведомления отключены", "notifications_attachment_copy_url_title": "Скопировать URL-адрес вложения", "notifications_actions_open_url_title": "Перейти на {{url}}", "notifications_tags": "Тэги", @@ -98,7 +98,7 @@ "subscribe_dialog_login_description": "Эта тема защищена паролем. Пожалуйста, введите имя пользователя и пароль, чтобы подписаться.", "subscribe_dialog_login_username_label": "Имя пользователя. Например, phil", "subscribe_dialog_login_password_label": "Пароль", - "subscribe_dialog_login_button_back": "Назад", + "common_back": "Назад", "subscribe_dialog_login_button_login": "Войти", "subscribe_dialog_error_user_not_authorized": "Пользователь {{username}} не авторизован", "subscribe_dialog_error_user_anonymous": "анонимный пользователь", @@ -206,7 +206,7 @@ "account_basics_tier_free": "Бесплатный", "account_tokens_dialog_title_create": "Создать токен доступа", "account_tokens_dialog_title_delete": "Удалить токен доступа", - "account_tokens_table_copy_to_clipboard": "Скопировать в буфер обмена", + "common_copy_to_clipboard": "Скопировать в буфер обмена", "account_tokens_dialog_button_cancel": "Отмена", "account_tokens_dialog_expires_unchanged": "Оставить срок истечения без изменений", "account_tokens_dialog_expires_x_days": "Токен истекает через {{days}} дней", @@ -352,5 +352,33 @@ "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} в год. Оплата помесячно.", "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} ежегодно. Сэкономьте {{save}}.", "account_upgrade_dialog_billing_contact_email": "По вопросам оплаты, пожалуйста свяжитесь с нами.", - "account_upgrade_dialog_billing_contact_website": "По вопросам оплаты, пожалуйста обратитесь к нашему сайту." + "account_upgrade_dialog_billing_contact_website": "По вопросам оплаты, пожалуйста обратитесь к нашему сайту.", + "publish_dialog_call_reset": "Удалить вызов", + "account_basics_phone_numbers_dialog_description": "Для того что бы использовать возможность уведомлений о вызовах, нужно добавить и проверить хотя бы один номер телефона. Проверить можно используя SMS или звонок.", + "account_basics_phone_numbers_dialog_title": "Добавить номер телефона", + "account_basics_phone_numbers_dialog_number_placeholder": "например +1222333444", + "account_basics_phone_numbers_dialog_code_placeholder": "например 123456", + "account_basics_phone_numbers_dialog_verify_button_sms": "Отправить SMS", + "account_usage_calls_title": "Совершённые вызовы", + "account_usage_calls_none": "Невозможно совершать вызовы с этим аккаунтом", + "publish_dialog_chip_call_no_verified_numbers_tooltip": "Нет проверенных номеров", + "account_basics_phone_numbers_copied_to_clipboard": "Номер телефона скопирован в буфер обмена", + "account_upgrade_dialog_tier_features_no_calls": "Нет вызовов", + "account_upgrade_dialog_tier_features_calls_one": "{{calls}} ежедневный звонок", + "account_basics_phone_numbers_dialog_number_label": "Номер телефона", + "account_basics_phone_numbers_dialog_check_verification_button": "Подтвердить код", + "account_upgrade_dialog_tier_features_calls_other": "{{calls}} ежедневных звонков", + "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} зарезервированная тема", + "account_basics_phone_numbers_no_phone_numbers_yet": "Телефонных номеров пока нет", + "publish_dialog_chip_call_label": "Звонок", + "account_upgrade_dialog_tier_features_emails_one": "{{emails}} ежедневное письмо", + "account_upgrade_dialog_tier_features_messages_one": "{{messages}} ежедневное сообщения", + "account_basics_phone_numbers_description": "Для уведомлений о телефонных звонках", + "publish_dialog_call_label": "Звонок", + "account_basics_phone_numbers_dialog_channel_call": "Позвонить", + "account_basics_phone_numbers_title": "Номера телефонов", + "account_basics_phone_numbers_dialog_code_label": "Проверочный код", + "account_basics_phone_numbers_dialog_verify_button_call": "Позвонить мне", + "publish_dialog_call_item": "Вызов телефонного номера {{number}}", + "account_basics_phone_numbers_dialog_channel_sms": "SMS" } diff --git a/web/public/static/langs/sk.json b/web/public/static/langs/sk.json new file mode 100644 index 00000000..0e3f57a7 --- /dev/null +++ b/web/public/static/langs/sk.json @@ -0,0 +1,384 @@ +{ + "common_save": "Uložiť", + "common_back": "Späť", + "common_copy_to_clipboard": "Kopírovať do schránky", + "signup_title": "Vytvoriť ntfy účet", + "signup_form_username": "Používateľské meno", + "signup_form_confirm_password": "Potvrdenie hesla", + "signup_form_button_submit": "Zaregistrovať sa", + "signup_form_toggle_password_visibility": "Prepnúť viditeľnosť hesla", + "signup_error_username_taken": "Používateľské meno {{username}} je už obsadené", + "login_form_button_submit": "Prihlásiť sa", + "login_disabled": "Prihlásenie je zakázané", + "action_bar_logo_alt": "ntfy logo", + "action_bar_settings": "Nastavenia", + "action_bar_account": "Účet", + "action_bar_sign_in": "Prihlásiť sa", + "action_bar_profile_settings": "Nastavenia", + "action_bar_reservation_edit": "Zmeniť rezerváciu", + "action_bar_unsubscribe": "Odhlásiť odber", + "action_bar_toggle_mute": "Stlmiť/zrušiť stlmenie upozornení", + "action_bar_toggle_action_menu": "Otvoriť/zavrieť akčné menu", + "action_bar_profile_title": "Profil", + "nav_button_settings": "Nastavenia", + "nav_button_account": "Účet", + "message_bar_show_dialog": "Zobraziť okno pre odosielanie oznámení", + "message_bar_publish": "Zverejniť správu", + "nav_topics_title": "Odoberané témy", + "nav_button_all_notifications": "Všetky oznámenia", + "alert_grant_description": "Udeliť prehliadaču povolenie na zobrazovanie oznámení na ploche.", + "alert_not_supported_context_description": "Oznámenia sú podporované len cez HTTPS. Ide o obmedzenie rozhrania Notifications API.", + "notifications_list": "Zoznam oznámení", + "notifications_list_item": "Oznámenie", + "notifications_mark_read": "Označiť ako prečítané", + "notifications_delete": "Zmazať", + "notifications_copied_to_clipboard": "Skopírované do schránky", + "notifications_tags": "Štítky", + "notifications_priority_x": "Priorita {{priority}}", + "notifications_new_indicator": "Nové oznámenie", + "notifications_attachment_image": "Obrázok prílohy", + "notifications_attachment_link_expired": "odkaz na stiahnutie vypršal", + "notifications_attachment_file_image": "súbor s obrázkom", + "notifications_attachment_file_video": "video súbor", + "notifications_attachment_file_audio": "zvukový súbor", + "notifications_attachment_file_app": "Súbor aplikácie pre Android", + "notifications_attachment_file_document": "iný dokument", + "notifications_click_copy_url_title": "Skopírovať URL adresu odkazu do schránky", + "notifications_click_copy_url_button": "Kopírovať odkaz", + "notifications_click_open_button": "Otvoriť odkaz", + "notifications_actions_not_supported": "Akcia nie je podporovaná vo webovej aplikácii", + "notifications_none_for_topic_title": "K tejto téme ste zatiaľ nedostali žiadne upozornenia.", + "notifications_none_for_any_title": "Nedostali ste žiadne upozornenia.", + "notifications_none_for_any_description": "Ak chcete posielať oznámenia do témy, jednoducho zadajte adresu PUT alebo POST na adresu URL témy. Tu je príklad s použitím jednej z vašich tém.", + "notifications_no_subscriptions_title": "Zdá sa, že zatiaľ nemáte žiadne prihlásenia na odber.", + "display_name_dialog_title": "Zmeniť zobrazovaný názov", + "notifications_no_subscriptions_description": "Kliknutím na odkaz \"{{text odkazu}}\" vytvoríte tému alebo sa na ňu prihlásite. Potom môžete posielať správy prostredníctvom PUT alebo POST a budete tu dostávať oznámenia.", + "notifications_example": "Príklad", + "notifications_more_details": "Ďalšie informácie nájdete na webovej stránke alebo v dokumentácií.", + "display_name_dialog_placeholder": "Zobrazený názov", + "reserve_dialog_checkbox_label": "Rezervovať tému a nakonfigurovať prístup", + "notifications_loading": "Načítavanie oznámení …", + "publish_dialog_title_no_topic": "Zverejniť oznámenie", + "publish_dialog_title_topic": "Zverejniť v {{topic}}", + "publish_dialog_progress_uploading": "Nahrávanie…", + "publish_dialog_progress_uploading_detail": "Nahrávanie {{loaded}}/{{total}} ({{percent}}%) …", + "publish_dialog_message_published": "Oznámenie zverejnené", + "publish_dialog_attachment_limits_file_and_quota_reached": "prekročí {{fileSizeLimit}} limit súboru a kvótu, {{remainingBytes}} zostáva", + "publish_dialog_attachment_limits_file_reached": "prekračuje {{fileSizeLimit}} limit súboru", + "publish_dialog_attachment_limits_quota_reached": "prekračuje kvótu, {{remainingBytes}} zostáva", + "publish_dialog_emoji_picker_show": "Vyberte emoji", + "publish_dialog_priority_min": "Min. priorita", + "publish_dialog_priority_low": "Nízka priorita", + "publish_dialog_priority_default": "Predvolená priorita", + "publish_dialog_priority_high": "Vysoká priorita", + "publish_dialog_priority_max": "Max. priorita", + "publish_dialog_base_url_label": "URL Adresa služby", + "publish_dialog_base_url_placeholder": "URL adresa služby, napr. https://example.com", + "publish_dialog_topic_label": "Názov témy", + "publish_dialog_topic_placeholder": "Názov témy, napr. phil_alerts", + "publish_dialog_topic_reset": "Resetovať tému", + "publish_dialog_title_label": "Názov", + "publish_dialog_title_placeholder": "Názov oznámenia, napr. Upozornenie na miesto na disku", + "publish_dialog_tags_label": "Štítky", + "publish_dialog_message_label": "Správa", + "publish_dialog_priority_label": "Priorita", + "publish_dialog_click_label": "Kliknite na URL", + "publish_dialog_click_placeholder": "URL adresa sa otvorí po kliknutí na oznámenie", + "publish_dialog_email_label": "Email", + "publish_dialog_email_placeholder": "Emailová adresa, na ktorú sa má oznámenie zaslať, napr. phil@example.com", + "publish_dialog_call_label": "Telefonovať", + "publish_dialog_call_item": "Zavolať na telefónne číslo {{number}}", + "publish_dialog_call_reset": "Odstrániť telefón", + "publish_dialog_attach_label": "URL prílohy", + "publish_dialog_attach_reset": "Odstrániť URL prílohy", + "publish_dialog_filename_label": "Názov súboru", + "publish_dialog_filename_placeholder": "Názov súboru prílohy", + "publish_dialog_delay_label": "Oneskorenie", + "publish_dialog_delay_placeholder": "Oneskorenie doručenia, napr. {{unixTimestamp}}, {{relativeTime}} alebo \"{{naturalLanguage}}\" (len v angličtine)", + "publish_dialog_delay_reset": "Odstrániť oneskorené doručenie", + "publish_dialog_chip_call_label": "Telefonovať", + "publish_dialog_other_features": "Ďalšie funkcie:", + "publish_dialog_chip_call_no_verified_numbers_tooltip": "Žiadne overené telefónne čísla", + "publish_dialog_chip_attach_url_label": "Pripojiť súbor pomocou adresy URL", + "publish_dialog_chip_delay_label": "Oneskoriť doručenie", + "publish_dialog_chip_topic_label": "Zmeniť tému", + "publish_dialog_button_cancel_sending": "Zrušiť odosielanie", + "publish_dialog_button_send": "Odoslať", + "publish_dialog_checkbox_publish_another": "Zverejniť ďalšie", + "publish_dialog_attached_file_title": "Priložený súbor:", + "subscribe_dialog_subscribe_button_cancel": "Zrušiť", + "subscribe_dialog_subscribe_title": "Odoberať tému", + "subscribe_dialog_subscribe_base_url_label": "URL Adresa služby", + "subscribe_dialog_subscribe_topic_placeholder": "Názov témy, napr. phil_alerts", + "publish_dialog_attached_file_filename_placeholder": "Názov súboru prílohy", + "publish_dialog_attached_file_remove": "Odstrániť priložený súbor", + "publish_dialog_drop_file_here": "Vložiť súbor", + "subscribe_dialog_login_password_label": "Heslo", + "account_basics_password_dialog_confirm_password_label": "Potvrdenie hesla", + "account_basics_title": "Účet", + "account_delete_dialog_button_cancel": "Zrušiť", + "account_delete_dialog_label": "Heslo", + "prefs_reservations_dialog_title_add": "Rezervovať tému", + "publish_dialog_button_cancel": "Zrušiť", + "account_upgrade_dialog_button_cancel": "Zrušiť", + "account_tokens_dialog_button_cancel": "Zrušiť", + "common_cancel": "Zrušiť", + "common_add": "Pridať", + "account_basics_username_title": "Používateľské meno", + "signup_form_password": "Heslo", + "signup_error_creation_limit_reached": "Dosiahnutý limit na vytvorenie konta", + "account_basics_password_title": "Heslo", + "action_bar_change_display_name": "Zmeniť zobrazovaný názov", + "prefs_users_dialog_password_label": "Heslo", + "action_bar_sign_up": "Zaregistrovať sa", + "login_link_signup": "Zaregistrovať sa", + "signup_already_have_account": "Už máte účet? Prihláste sa!", + "signup_disabled": "Registrácia je vypnutá", + "login_title": "Prihláste sa do svojho konta ntfy", + "action_bar_show_menu": "Zobraziť menu", + "action_bar_reservation_add": "Rezervovať tému", + "action_bar_reservation_delete": "Odstrániť rezerváciu", + "action_bar_reservation_limit_reached": "Dosiahnutý limit", + "action_bar_send_test_notification": "Odoslať testovacie oznámenie", + "action_bar_clear_notifications": "Vymazať všetky oznámenia", + "publish_dialog_message_placeholder": "Sem napíšte správu", + "action_bar_profile_logout": "Odhlásiť sa", + "message_bar_type_message": "Sem napíšte správu", + "message_bar_error_publishing": "Chyba pri zverejňovaní oznámenia", + "nav_button_documentation": "Dokumentácia", + "nav_button_publish_message": "Zverejniť oznámenie", + "nav_button_subscribe": "Odoberať tému", + "nav_button_muted": "Oznámenia stlmené", + "nav_button_connecting": "pripájanie", + "nav_upgrade_banner_description": "Rezervovať témy, viac správ a e-mailov a väčšie prílohy", + "nav_upgrade_banner_label": "Vylepšiť na ntfy Pro", + "alert_grant_title": "Oznámenia sú vypnuté", + "alert_grant_button": "Prideliť teraz", + "alert_not_supported_title": "Oznámenia nie sú podporované", + "alert_not_supported_description": "Oznámenia nie sú vo vašom prehliadači podporované.", + "notifications_attachment_copy_url_title": "Kopírovať URL adresu prílohy do schránky", + "notifications_attachment_copy_url_button": "Kopírovať adresu URL", + "notifications_attachment_open_title": "Prejsť na {{url}}", + "notifications_actions_open_url_title": "Prejsť na {{url}}", + "notifications_attachment_open_button": "Otvoriť prílohu", + "notifications_attachment_link_expires": "platnosť odkazu vyprší {{date}}", + "notifications_none_for_topic_description": "Ak chcete posielať oznámenia do tejto témy, jednoducho zadajte adresu PUT alebo POST na URL adresu témy.", + "notifications_actions_http_request_title": "Odoslať HTTP {{method}} na {{url}}", + "display_name_dialog_description": "Nastavenie alternatívneho názvu témy, ktorá sa zobrazuje v zozname odberov. Pomáha to ľahšie identifikovať témy so zložitými názvami.", + "prefs_users_table_base_url_header": "URL Adresa služby", + "publish_dialog_tags_placeholder": "Zoznam štítkov oddelených čiarkou, napr. varovanie, srv1-backup", + "publish_dialog_chip_click_label": "Kliknite na URL", + "publish_dialog_email_reset": "Odstrániť email na preposielanie", + "publish_dialog_click_reset": "Odobrať URL kliknutím", + "publish_dialog_attach_placeholder": "Pripojiť súbor pomocou URL adresy, napr. https://f-droid.org/F-Droid.apk", + "publish_dialog_chip_email_label": "Preposlanie na email", + "publish_dialog_chip_attach_file_label": "Pripojiť miestny súbor", + "publish_dialog_details_examples_description": "Príklady a podrobný opis všetkých funkcií odosielania nájdete v dokumentácii.", + "account_upgrade_dialog_tier_features_no_calls": "Žiadne telefonáty", + "account_upgrade_dialog_billing_contact_email": "V prípade otázok týkajúcich sa fakturácie nás prosím kontaktujte tu.", + "account_tokens_dialog_title_create": "Vytvoriť prístupový token", + "prefs_reservations_dialog_title_edit": "Upraviť rezervovanú tému", + "account_basics_tier_interval_monthly": "mesačne", + "account_basics_tier_canceled_subscription": "Vaše predplatné bolo zrušené a bude preradené na bezplatné konto k dátumu {{date}}.", + "priority_default": "predvolená", + "prefs_notifications_min_priority_title": "Najnižšia priorita", + "account_upgrade_dialog_tier_features_calls_one": "{{calls}} denný telefonát", + "account_upgrade_dialog_tier_current_label": "Aktuálne", + "account_basics_password_dialog_current_password_incorrect": "Nesprávne heslo", + "account_tokens_table_token_header": "Token", + "prefs_notifications_delete_after_never": "Nikdy", + "prefs_users_description": "Tu môžete pridávať/odstraňovať používateľov pre svoje chránené témy. Upozorňujeme, že používateľské meno a heslo sú uložené v lokálnom úložisku prehliadača.", + "account_basics_phone_numbers_dialog_number_label": "Telefónne číslo", + "subscribe_dialog_subscribe_description": "Témy nemusia byť chránené heslom, preto vyberte názov, ktorý nie je ľahké uhádnuť. Po prihlásení sa na odber môžete PUT/POST oznámenia.", + "account_basics_password_dialog_button_submit": "Zmeniť heslo", + "account_basics_phone_numbers_dialog_check_verification_button": "Potvrdiť kód", + "account_upgrade_dialog_interval_yearly_discount_save_up_to": "ušetrite až {{discount}}%", + "account_tokens_dialog_label": "Označenie, napr. Radarr notifications", + "account_tokens_table_expires_header": "Vyprší", + "account_upgrade_dialog_proration_info": "Vyhlásenie: Pri prechode medzi platenými plánmi sa rozdiel v cene účtuje okamžite. Pri prechode na nižšiu úroveň sa zostatok použije na platbu za budúce fakturačné obdobia.", + "prefs_reservations_dialog_access_label": "Prístup", + "account_usage_attachment_storage_title": "Ukladanie príloh", + "prefs_users_dialog_username_label": "Používateľské meno, napr. phil", + "account_usage_messages_title": "Zverejnené správy", + "emoji_picker_search_clear": "Vymazať vyhľadávanie", + "prefs_reservations_table_not_subscribed": "Odber nie je prihlásený", + "account_upgrade_dialog_tier_features_emails_other": "{{emails}} denné emaily", + "prefs_notifications_min_priority_max_only": "Iba najvyššia priorita", + "account_upgrade_dialog_tier_features_calls_other": "{{calls}} denné telefonáty", + "prefs_notifications_sound_description_some": "Oznámenia pri príchode prehrávajú zvuk {{sound}}", + "prefs_reservations_edit_button": "Upraviť prístup k téme", + "account_basics_phone_numbers_dialog_verify_button_sms": "Poslať SMS", + "account_basics_tier_change_button": "Zmeniť", + "account_tokens_dialog_expires_never": "Platnosť tokenu nikdy nevyprší", + "subscribe_dialog_login_title": "Vyžaduje sa prihlásenie", + "account_tokens_dialog_expires_x_days": "Token vyprší za {{days}} dní", + "prefs_reservations_table_everyone_read_only": "Môžem publikovať a odoberať, každý môže odoberať", + "prefs_reservations_table_everyone_deny_all": "Iba ja môžem publikovať a odoberať", + "account_basics_phone_numbers_dialog_description": "Ak chcete používať funkciu oznamovanie hovorom, musíte pridať a overiť aspoň jedno telefónne číslo. Overenie je možné vykonať prostredníctvom SMS alebo telefonického hovoru.", + "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} rezervovaná téma", + "account_delete_title": "Odstrániť účet", + "subscribe_dialog_login_button_login": "Prihlásenie", + "account_upgrade_dialog_tier_features_no_reservations": "Žiadne rezervované témy", + "prefs_users_table_cannot_delete_or_edit": "Nie je možné odstrániť alebo upraviť prihláseného používateľa", + "account_basics_tier_admin_suffix_with_tier": "(s úrovňou {{tier}})", + "prefs_notifications_delete_after_three_hours_description": "Oznámenia sa automaticky odstránia po troch hodinách", + "prefs_notifications_delete_after_three_hours": "Po troch hodinách", + "prefs_notifications_min_priority_description_x_or_higher": "Zobraziť oznámenia, ak je priorita {{number}} ({{name}}) alebo vyššia", + "reservation_delete_dialog_description": "Odstránením rezervácie sa vzdáte vlastníctva témy a umožníte ostatným, aby si ju rezervovali. Existujúce správy a prílohy si môžete ponechať alebo odstrániť.", + "subscribe_dialog_login_username_label": "Používateľské meno, napr. phil", + "subscribe_dialog_error_user_not_authorized": "Používateľ {{username}} nie je autorizovaný", + "prefs_reservations_table_everyone_read_write": "Každý môže publikovať a odoberať", + "prefs_reservations_dialog_title_delete": "Odstrániť rezervovanú tému", + "prefs_users_table": "Tabuľka používateľov", + "prefs_reservations_table_topic_header": "Téma", + "reservation_delete_dialog_submit_button": "Vymazať rezerváciu", + "prefs_reservations_limit_reached": "Dosiahli ste limit rezervovaných tém.", + "account_upgrade_dialog_interval_monthly": "Mesačne", + "prefs_users_add_button": "Pridať používateľa", + "account_upgrade_dialog_tier_features_messages_other": "{{messages}} denné správy", + "account_basics_phone_numbers_no_phone_numbers_yet": "Zatiaľ žiadne telefónne čísla", + "subscribe_dialog_subscribe_button_generate_topic_name": "Vygenerovať názov", + "prefs_appearance_language_title": "Jazyk", + "prefs_notifications_delete_after_one_day_description": "Oznámenia sa automaticky odstránia po jednom dni", + "subscribe_dialog_subscribe_button_subscribe": "Odoberať", + "account_tokens_table_never_expires": "Nikdy nevyprší", + "account_tokens_delete_dialog_title": "Odstrániť prístupový token", + "prefs_notifications_delete_after_one_month": "Po jednom mesiaci", + "account_basics_phone_numbers_dialog_title": "Pridať telefónne číslo", + "account_tokens_delete_dialog_description": "Pred odstránením prístupového tokenu sa uistite, že ho aktívne nepoužívajú žiadne aplikácie ani skripty. Túto akciu nie je možné vrátiť späť.", + "account_tokens_table_label_header": "Označenie", + "account_upgrade_dialog_billing_contact_website": "Otázky týkajúce sa fakturácie nájdete na našej webovej stránke.", + "account_basics_username_admin_tooltip": "Ste Admin", + "prefs_notifications_delete_after_never_description": "Oznámenia sa nikdy automaticky neodstránia", + "account_delete_dialog_description": "Tým sa vaše konto natrvalo odstráni vrátane všetkých údajov uložených na serveri. Po vymazaní bude vaše používateľské meno 7 dní nedostupné. Ak naozaj chcete pokračovať, potvrďte svoje heslo v poli nižšie.", + "account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} rezervované témy", + "account_usage_reservations_none": "Žiadne rezervované témy pre toto konto", + "prefs_notifications_sound_description_none": "Pri príchode oznámení sa neprehráva žiadny zvuk", + "account_tokens_description": "Pri publikovaní a prihlasovaní prostredníctvom rozhrania ntfy API používajte prístupové tokeny, aby ste nemuseli posielať prihlasovacie údaje k účtu. Viacej informácií nájdete v dokumentácií.", + "prefs_reservations_table": "Tabuľka rezervovaných tém", + "emoji_picker_search_placeholder": "Vyhľadať emoji", + "account_upgrade_dialog_button_cancel_subscription": "Zrušiť predplatné", + "account_upgrade_dialog_tier_features_emails_one": "{{emails}} denný email", + "account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} na jeden súbor", + "prefs_reservations_description": "Tu si môžete rezervovať názvy tém na osobné použitie. Rezervovaním témy získate vlastníctvo nad témou a môžete definovať prístupové práva pre ostatných používateľov k téme.", + "account_usage_title": "Používanie", + "account_basics_tier_upgrade_button": "Vylepšiť na PRO verziu", + "prefs_users_description_no_sync": "Používatelia a heslá nie sú synchronizované s vaším účtom.", + "account_tokens_dialog_title_edit": "Upraviť prístupový token", + "account_upgrade_dialog_tier_features_messages_one": "{{messages}} denná správa", + "account_upgrade_dialog_reservations_warning_one": "Vybraná úroveň umožňuje menej rezervovaných tém ako vaša aktuálna úroveň. Pred zmenou úrovne vymažte aspoň jednu rezerváciu. Rezervácie môžete odstrániť v Nastaveniach.", + "subscribe_dialog_error_topic_already_reserved": "Téma je už rezervovaná", + "prefs_users_table_user_header": "Používateľ", + "error_boundary_stack_trace": "Výpis zásobníka", + "prefs_notifications_delete_after_one_week": "Po jednom týždni", + "prefs_reservations_delete_button": "Resetovať prístup k téme", + "account_basics_tier_admin_suffix_no_tier": "(bez úrovne)", + "prefs_notifications_delete_after_one_week_description": "Oznámenia sa automaticky odstránia po jednom týždni", + "error_boundary_unsupported_indexeddb_description": "Webová aplikácia ntfy potrebuje na fungovanie IndexedDB a váš prehliadač nepodporuje IndexedDB v režime súkromného prehliadania.

Je to síce nešťastné, ale aj tak nemá veľký zmysel používať webovú aplikáciu ntfy v režime súkromného prehliadania, pretože všetko je uložené v úložisku prehliadača. Viac informácií si môžete prečítať v tomto probléme GitHubu alebo sa s nami porozprávať na Discord alebo Matrix.", + "account_basics_tier_payment_overdue": "Vaša platba je po termíne splatnosti. Aktualizujte prosím svoj spôsob platby, inak bude váš účet preradený do nižšej kategórie.", + "account_basics_tier_description": "Úroveň výkonu vášho účtu", + "account_basics_phone_numbers_description": "Pre oznamovanie hovorom", + "account_basics_tier_free": "Zadarmo", + "account_upgrade_dialog_cancel_warning": "Týmto zrušíte svoje predplatné a {{date}} prejdete na nižšiu úroveň svojho účtu. V tento deň budú odstránené rezervácie tém, ako aj správy uložené vo vyrovnávacej pamäti servera.", + "account_basics_tier_admin": "Admin", + "prefs_notifications_sound_title": "Zvuk oznámenia", + "prefs_notifications_min_priority_default_and_higher": "Predvolená priorita a vyššia", + "prefs_reservations_table_access_header": "Prístup", + "account_tokens_table_copied_to_clipboard": "Prístupový token skopírovaný", + "account_tokens_dialog_expires_x_hours": "Token vyprší za {{hours}} hodín", + "prefs_users_edit_button": "Upraviť používateľa", + "account_upgrade_dialog_title": "Zmeniť úroveň účtu", + "priority_low": "nízka", + "prefs_reservations_table_click_to_subscribe": "Kliknutím sa prihlásite na odber", + "account_basics_password_description": "Zmeniť heslo účtu", + "account_usage_calls_title": "Uskutočnené telefonické hovory", + "error_boundary_description": "Toto samozrejme nemalo nastať. Je mi to veľmi ľúto.
Ak máte chvíľu, nahláste to na GitHub alebo nám dajte vedieť cez Discord alebo Matrix.", + "priority_min": "najnižšia", + "account_basics_tier_basic": "Základný", + "prefs_notifications_min_priority_description_any": "Zobraziť všetky oznámenia bez ohľadu na prioritu", + "error_boundary_gathering_info": "Získajte viac informácií…", + "error_boundary_unsupported_indexeddb_title": "Súkromné prehliadanie nie je podporované", + "prefs_notifications_delete_after_one_day": "Po jednom dni", + "error_boundary_title": "Ale nie, ntfy prestalo fungovať", + "reservation_delete_dialog_action_keep_description": "Správy a prílohy, ktoré sú uložené v medzipamäti na serveri, budú verejne viditeľné pre ľudí, ktorí poznajú názov témy.", + "prefs_reservations_add_button": "Pridať rezervovanú tému", + "prefs_reservations_title": "Rezervované témy", + "account_basics_phone_numbers_copied_to_clipboard": "Telefónne číslo skopírované do schránky", + "prefs_reservations_dialog_description": "Rezervovaním témy získate vlastníctvo nad témou a môžete definovať prístupové práva pre ostatných používateľov k téme.", + "account_basics_tier_title": "Typ účtu", + "account_usage_cannot_create_portal_session": "Nemožnosť otvoriť fakturačný portál", + "account_tokens_delete_dialog_submit_button": "Trvalo odstrániť token", + "account_delete_description": "Natrvalo odstrániť vaše konto", + "account_basics_phone_numbers_dialog_number_placeholder": "napr. +1222333444", + "account_basics_phone_numbers_dialog_code_placeholder": "napr. 123456", + "prefs_notifications_title": "Oznámenia", + "account_basics_tier_manage_billing_button": "Spravovať fakturáciu", + "account_tokens_title": "Prístupové tokeny", + "account_basics_username_description": "Hej, to si ty ❤", + "prefs_reservations_dialog_topic_label": "Téma", + "prefs_users_title": "Správa používateľov", + "account_basics_tier_interval_yearly": "ročne", + "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} za rok. Účtuje sa mesačne.", + "account_delete_dialog_button_submit": "Natrvalo odstrániť konto", + "account_basics_phone_numbers_dialog_channel_call": "Hovor", + "account_basics_password_dialog_new_password_label": "Nové heslo", + "account_tokens_dialog_expires_unchanged": "Ponechať dátum skončenia platnosti nezmenený", + "error_boundary_button_copy_stack_trace": "Kopírovať výpis zásobníka", + "account_tokens_dialog_title_delete": "Odstrániť prístupový token", + "account_usage_of_limit": "z {{limit}}", + "reservation_delete_dialog_action_keep_title": "Ponechať správy a prílohy uložené v medzipamäti", + "prefs_notifications_sound_no_sound": "Bez zvuku", + "account_upgrade_dialog_interval_yearly": "Ročne", + "account_upgrade_dialog_button_redirect_signup": "Zaregistrujte sa teraz", + "subscribe_dialog_error_user_anonymous": "anonymný", + "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} účtovaná ročne. Uložiť {{save}}.", + "prefs_notifications_min_priority_high_and_higher": "Vysoká priorita a vyššia", + "account_usage_basis_ip_description": "Štatistiky a limity používania tohto účtu sú založené na vašej IP adrese, takže môžu byť zdieľané s ostatnými používateľmi. Vyššie uvedené limity sú približné hodnoty založené na existujúcich rýchlostných limitoch.", + "account_basics_password_dialog_title": "Zmeniť heslo", + "priority_max": "najvyššia", + "account_usage_limits_reset_daily": "Limity používania sa obnovujú denne o polnoci (UTC)", + "account_usage_unlimited": "Nekonečné", + "prefs_users_delete_button": "Odstrániť používateľa", + "prefs_notifications_min_priority_any": "Akákoľvek priorita", + "account_tokens_dialog_expires_label": "Platnosť prístupového tokenu vyprší za", + "account_basics_phone_numbers_title": "Telefónne čísla", + "prefs_notifications_delete_after_title": "Odstrániť oznámenia", + "account_upgrade_dialog_interval_yearly_discount_save": "ušetríte {{discount}}%", + "prefs_users_dialog_title_edit": "Upraviť používateľa", + "account_basics_password_dialog_current_password_label": "Aktuálne heslo", + "prefs_notifications_min_priority_low_and_higher": "Nízka priorita a vyššia", + "account_tokens_dialog_button_update": "Aktualizovať token", + "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} celkový úložný priestor", + "prefs_reservations_table_everyone_write_only": "Môžem publikovať a odoberať, každý môže publikovať", + "prefs_appearance_title": "Vzhlad", + "account_tokens_table_cannot_delete_or_edit": "Nie je možné upraviť alebo odstrániť aktuálny token relácie", + "prefs_notifications_sound_play": "Prehrať vybraný zvuk", + "account_tokens_table_last_access_header": "Posledný prístup", + "account_tokens_table_last_origin_tooltip": "Z IP adresy {{ip}}, kliknite na vyhľadávanie", + "account_usage_reservations_title": "Rezervované témy", + "account_upgrade_dialog_tier_price_per_month": "mesiac", + "account_usage_calls_none": "S týmto účtom nie je možné uskutočňovať žiadne telefonické hovory", + "account_tokens_table_current_session": "Aktuálna relácia prehliadača", + "account_upgrade_dialog_button_pay_now": "Zaplatiť a predplatiť si", + "subscribe_dialog_subscribe_use_another_label": "Použiť iný server", + "reservation_delete_dialog_action_delete_title": "Odstrániť správy a prílohy uložené v medzipamäti", + "account_basics_phone_numbers_dialog_code_label": "Overovací kód", + "reservation_delete_dialog_action_delete_description": "Správy a prílohy uložené v medzipamäti sa natrvalo vymažú. Túto akciu nemožno vrátiť späť.", + "account_basics_tier_paid_until": "Predplatné zaplatené do {{date}} s automatickou obnovou", + "account_usage_attachment_storage_description": "{{filesize}} na súbor, vymazaný po {{expiry}}", + "prefs_notifications_delete_after_one_month_description": "Oznámenia sa automaticky odstránia po jednom mesiaci", + "account_basics_phone_numbers_dialog_verify_button_call": "Zavolajte mi", + "prefs_users_dialog_base_url_label": "URL adresa služby, napr. https://ntfy.sh", + "account_usage_emails_title": "Odoslané emaily", + "account_basics_phone_numbers_dialog_channel_sms": "SMS", + "account_upgrade_dialog_tier_selected_label": "Vybrané", + "account_upgrade_dialog_button_update_subscription": "Aktualizovať predplatné", + "priority_high": "vysoká", + "account_delete_dialog_billing_warning": "Odstránením konta sa okamžite zruší aj vaše fakturačné predplatné. Už nebudete mať prístup k fakturačnému panelu.", + "prefs_notifications_min_priority_description_max": "Zobraziť oznámenia, ak je priorita 5 (max)", + "subscribe_dialog_login_description": "Táto téma je chránená heslom. Ak sa chcete prihlásiť na odber témy, zadajte používateľské meno a heslo.", + "account_upgrade_dialog_reservations_warning_other": "Vybraná úroveň umožňuje menej rezervovaných tém ako vaša aktuálna úroveň. Pred zmenou úrovne vymažte aspoň {{count}} rezervácií. Rezervácie môžete odstrániť v Nastaveniach.", + "prefs_users_dialog_title_add": "Pridať používateľa", + "account_tokens_dialog_button_create": "Vytvoriť token", + "account_tokens_table_create_token_button": "Vytvoriť prístupový token" +} diff --git a/web/public/static/langs/sv.json b/web/public/static/langs/sv.json index 4896c8c2..1a44a3dc 100644 --- a/web/public/static/langs/sv.json +++ b/web/public/static/langs/sv.json @@ -11,8 +11,8 @@ "nav_button_documentation": "Dokumentation", "nav_button_publish_message": "Publicera notis", "nav_button_subscribe": "Prenumerera på kategori", - "alert_grant_title": "Notiser är avstängda", - "alert_grant_button": "Bevilja nu", + "alert_notification_permission_required_title": "Notiser är avstängda", + "alert_notification_permission_required_button": "Bevilja nu", "alert_not_supported_title": "Notiser stöds inte", "notifications_list": "Notifieringslista", "notifications_list_item": "Notis", @@ -38,7 +38,7 @@ "notifications_attachment_link_expires": "länken utgår {{date}}", "notifications_attachment_file_image": "bild fil", "notifications_attachment_file_audio": "ljud fil", - "alert_grant_description": "Ge din webbläsare behörighet att visa skrivbordsnotiser.", + "alert_notification_permission_required_description": "Ge din webbläsare behörighet att visa skrivbordsnotiser.", "alert_not_supported_description": "Notiser stöds inte i din webbläsare.", "notifications_mark_read": "Markera som läst", "notifications_attachment_file_video": "video fil", @@ -76,5 +76,309 @@ "signup_form_username": "Användarnamn", "signup_already_have_account": "Har du redan ett konto? Logga in!", "signup_disabled": "Registrering är inaktiverad", - "signup_error_username_taken": "Användarnamn [[username]] används redan" + "signup_error_username_taken": "Användarnamn [[username]] används redan", + "notifications_attachment_file_document": "annat dokument", + "notifications_attachment_file_app": "Android app fil", + "notifications_click_copy_url_title": "Kopiera länk till urklipp", + "notifications_none_for_topic_title": "Du har inte fått några notiser för detta ämnet ännu.", + "notifications_none_for_topic_description": "För att kunna skicka notiser till detta ämnet, använd PUT eller POST till ämnets URL.", + "notifications_actions_http_request_title": "Skicka HTTP {{method}} till {{url}}", + "publish_dialog_progress_uploading": "Laddar upp …", + "nav_upgrade_banner_description": "Reservera ämnen, fler meddelanden och e-postmeddelanden och större bilagor", + "publish_dialog_attachment_limits_file_and_quota_reached": "överskrider {{fileSizeLimit}} filgräns och kvot, {{remainingBytes}} återstående", + "publish_dialog_attachment_limits_file_reached": "överskrider {{fileSizeLimit}} filgräns", + "publish_dialog_attachment_limits_quota_reached": "överskrider kvoten, {{remainingBytes}} återstår", + "publish_dialog_message_placeholder": "Skriv ett meddelande här", + "publish_dialog_checkbox_publish_another": "Publicera en till", + "subscribe_dialog_error_user_anonymous": "anonym", + "account_basics_password_dialog_confirm_password_label": "Bekräfta lösenord", + "publish_dialog_email_placeholder": "Adress att vidarebefordra meddelandet till, t.ex. phil@example.com", + "publish_dialog_details_examples_description": "Exempel och en detaljerad beskrivning av alla sändningsfunktioner finns i dokumentationen .", + "publish_dialog_button_send": "Skicka", + "common_back": "Tillbaka", + "account_basics_tier_free": "Gratis", + "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} reserverat ämne", + "account_delete_title": "Ta bort konto", + "account_upgrade_dialog_tier_features_messages_other": "{{messages}} dagliga meddelanden", + "account_upgrade_dialog_tier_features_emails_one": "{{emails}} dagligt e-postmeddelande", + "account_upgrade_dialog_button_cancel": "Avbryt", + "common_copy_to_clipboard": "Kopiera till urklipp", + "account_tokens_table_copied_to_clipboard": "Åtkomsttoken kopierat", + "account_tokens_description": "Använd åtkomsttoken när du publicerar och prenumererar via ntfy API, så att du inte behöver skicka dina kontouppgifter. Läs mer i dokumentationen.", + "account_tokens_table_create_token_button": "Skapa åtkomsttoken", + "prefs_users_description_no_sync": "Användare och lösenord synkroniseras inte till ditt konto.", + "error_boundary_unsupported_indexeddb_description": "ntfy-webbappen behöver IndexedDB för att fungera och din webbläsare har inte stöd för IndexedDB i privat surfläge.

Detta är beklagligt, men det är inte heller särskilt meningsfullt att använda ntfy-webbappen i privat surfläge, eftersom allt lagras i webbläsarens lagringsutrymme. Du kan läsa mer om det i detta GitHub-ärende, eller prata med oss på Discord eller Matrix.", + "account_basics_tier_interval_monthly": "månadsvis", + "account_basics_tier_interval_yearly": "årligen", + "account_basics_tier_canceled_subscription": "Din prenumeration avbröts och kommer att nedgraderas till ett gratis konto den {{date}}.", + "account_basics_tier_manage_billing_button": "Hantera fakturering", + "account_usage_messages_title": "Publicerade meddelande", + "account_usage_emails_title": "Skickade e-postmeddelanden", + "account_usage_reservations_title": "Reserverade ämnen", + "account_usage_reservations_none": "Inga reserverade ämnen för det här kontot", + "account_usage_attachment_storage_title": "Lagring av bilagor", + "account_usage_attachment_storage_description": "{{filesize}} per fil, raderas efter {{expiry}}", + "account_delete_description": "Ta bort ditt konto permanent", + "account_delete_dialog_description": "Detta kommer att radera ditt konto permanent, inklusive all data som lagras på servern. Efter raderingen kommer ditt användarnamn att vara otillgängligt i 7 dagar. Om du verkligen vill fortsätta, bekräfta med ditt lösenord i rutan nedan.", + "account_delete_dialog_label": "Lösenord", + "account_delete_dialog_button_cancel": "Avbryt", + "account_delete_dialog_button_submit": "Ta bort kontot permanent", + "account_delete_dialog_billing_warning": "Om du raderar ditt konto annulleras också din faktureringsprenumeration omedelbart. Du kommer inte längre att ha tillgång till instrumentpanelen för fakturering.", + "account_upgrade_dialog_title": "Ändra kontonivå", + "account_upgrade_dialog_interval_monthly": "Månadsvis", + "account_upgrade_dialog_interval_yearly": "Årligen", + "account_upgrade_dialog_interval_yearly_discount_save": "spara {{discount}}%", + "account_upgrade_dialog_interval_yearly_discount_save_up_to": "spara upp till {{discount}}%", + "account_upgrade_dialog_cancel_warning": "Detta kommer att säga upp din prenumeration och nedgradera ditt konto på {{date}}. På det datumet kommer ämnesreservationer och meddelanden som ligger i cacheminnet på servern att raderas.", + "account_upgrade_dialog_proration_info": "Deklaration: När du uppgraderar mellan betalda planer kommer prisskillnaden att debiteras omedelbart. Vid nedgradering till en lägre nivå kommer saldot att användas för att betala för framtida faktureringsperioder.", + "account_upgrade_dialog_reservations_warning_one": "Den valda nivån tillåter färre reserverade ämnen än din nuvarande nivå. Innan du ändrar nivå, bör du ta bort minst en reservation. Du kan ta bort reservationer i Inställningar.", + "account_upgrade_dialog_reservations_warning_other": "Den valda nivån tillåter färre reserverade ämnen än din nuvarande nivå. Innan du ändrar nivå, ta bort minst {{count}} reservationer. Du kan ta bort reservationer i Inställningar.", + "account_upgrade_dialog_tier_features_no_reservations": "Inga reserverade ämnen", + "account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} per fil", + "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} total lagring", + "account_upgrade_dialog_tier_price_per_month": "månad", + "account_upgrade_dialog_tier_selected_label": "Vald", + "account_tokens_table_token_header": "Token", + "account_tokens_dialog_title_create": "Skapa åtkomsttoken", + "account_tokens_dialog_title_delete": "Ta bort åtkomsttoken", + "account_tokens_dialog_label": "Etikett, t.ex. Radarr-meddelanden", + "account_tokens_dialog_title_edit": "Redigera åtkomsttoken", + "account_tokens_dialog_button_create": "Skapa token", + "account_tokens_dialog_button_update": "Uppdatera token", + "account_tokens_delete_dialog_submit_button": "Ta bort token permanent", + "prefs_notifications_delete_after_one_day": "Efter en dag", + "reservation_delete_dialog_action_delete_description": "Cachade meddelanden och bilagor raderas permanent. Denna åtgärd kan inte ångras.", + "error_boundary_gathering_info": "Samla mer information …", + "error_boundary_unsupported_indexeddb_title": "Privat surfning stöds inte", + "reservation_delete_dialog_submit_button": "Ta bort reservationen", + "priority_low": "låg", + "error_boundary_title": "Åh nej, ntfy kraschade", + "error_boundary_description": "Detta får naturligtvis inte ske. Vi beklagar verkligen detta.
Om du har tid, vänligen rapportera detta på GitHub, eller meddela oss via Discord eller Matrix.", + "notifications_no_subscriptions_title": "Det ser ut som om du inte har några prenumerationer ännu.", + "notifications_more_details": "Mer information finns på webbplatsen eller i dokumentationen .", + "publish_dialog_title_topic": "Publicera till {{topic}}", + "publish_dialog_message_published": "Meddelande publicerat", + "publish_dialog_emoji_picker_show": "Välj emoji", + "publish_dialog_base_url_placeholder": "Service-URL, t.ex. https://example.com", + "publish_dialog_topic_label": "Ämnesnamn", + "publish_dialog_topic_placeholder": "Ämnesnamn, t.ex. phils_alerts", + "publish_dialog_topic_reset": "Återställ ämne", + "publish_dialog_title_label": "Titel", + "publish_dialog_title_placeholder": "Meddelandets rubrik, t.ex. Varning för diskutrymme", + "publish_dialog_tags_label": "Taggar", + "publish_dialog_message_label": "Meddelande", + "publish_dialog_tags_placeholder": "Kommaseparerad lista med taggar, t.ex. warning, srv1-backup", + "publish_dialog_priority_label": "Prioritet", + "publish_dialog_click_label": "Klicka på URL", + "publish_dialog_click_placeholder": "URL som öppnas när man klickar på anmälan", + "publish_dialog_click_reset": "Ta bort klickbar URL", + "publish_dialog_email_reset": "Ta bort vidarebefordran av e-post", + "publish_dialog_attach_label": "URL för bifogade filer", + "publish_dialog_attach_placeholder": "Bifoga fil via URL, t.ex. https://f-droid.org/F-Droid.apk", + "publish_dialog_filename_label": "Filnamn", + "publish_dialog_delay_label": "Fördröjning", + "publish_dialog_filename_placeholder": "Filnamn för bifogad fil", + "publish_dialog_delay_placeholder": "Fördröj leverans, t.ex. {{unixTimestamp}}, {{relativeTime}} eller \"{{naturalLanguage}}\" (endast engelska)", + "publish_dialog_delay_reset": "Ta bort försenad leverans", + "publish_dialog_other_features": "Andra funktioner:", + "publish_dialog_chip_click_label": "Klicka på URL", + "publish_dialog_attached_file_title": "Bifogad fil:", + "publish_dialog_attached_file_filename_placeholder": "Filnamn för bifogad fil", + "emoji_picker_search_placeholder": "Sök emoji", + "subscribe_dialog_subscribe_button_cancel": "Avbryt", + "prefs_notifications_sound_description_some": "Meddelanden spelar upp ljudet {{sound}} när de anländer", + "prefs_notifications_sound_no_sound": "Inget ljud", + "prefs_notifications_min_priority_any": "Alla prioriteringar", + "prefs_notifications_min_priority_low_and_higher": "Låg prioritet och högre", + "prefs_notifications_delete_after_three_hours": "Efter tre timmar", + "prefs_notifications_delete_after_never": "Aldrig", + "prefs_users_table": "Användartabell", + "prefs_users_add_button": "Lägg till användare", + "prefs_users_edit_button": "Redigera användare", + "prefs_users_dialog_title_add": "Lägg till användare", + "prefs_users_dialog_title_edit": "Redigera användare", + "prefs_users_dialog_base_url_label": "Tjänstens URL, t.ex. https://ntfy.sh", + "prefs_users_dialog_password_label": "Lösenord", + "prefs_appearance_title": "Utseende", + "prefs_appearance_language_title": "Språk", + "priority_min": "min", + "priority_default": "standard", + "priority_high": "hög", + "priority_max": "max", + "error_boundary_button_copy_stack_trace": "Kopiera stackspårning", + "error_boundary_stack_trace": "Stackspårning", + "account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} reserverade ämnen", + "account_upgrade_dialog_tier_features_messages_one": "{{messages}} dagligt meddelande", + "account_upgrade_dialog_tier_features_emails_other": "{{emails}} dagliga e-postmeddelanden", + "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} per år. Faktureras månadsvis.", + "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} faktureras årligen. Spara {{save}}.", + "account_upgrade_dialog_tier_current_label": "Aktuell", + "account_upgrade_dialog_billing_contact_email": "För faktureringsfrågor, vänligen kontakta oss direkt.", + "account_upgrade_dialog_billing_contact_website": "För frågor om fakturering hänvisar vi till vår webbplats.", + "account_upgrade_dialog_button_redirect_signup": "Registrera dig nu", + "account_upgrade_dialog_button_pay_now": "Betala nu och prenumerera", + "account_upgrade_dialog_button_cancel_subscription": "Avbryt prenumeration", + "account_upgrade_dialog_button_update_subscription": "Uppdatera prenumeration", + "account_tokens_table_label_header": "Etikett", + "account_tokens_table_last_access_header": "Sista åtkomst", + "account_tokens_table_expires_header": "Upphör", + "account_tokens_table_never_expires": "Upphör aldrig", + "account_tokens_table_current_session": "Nuvarande webbläsarsession", + "account_tokens_table_cannot_delete_or_edit": "Det går inte att redigera eller ta bort aktuell sessionstoken", + "account_tokens_table_last_origin_tooltip": "Från IP-adress {{ip}}, klicka för att söka upp", + "account_tokens_dialog_button_cancel": "Avbryt", + "account_tokens_dialog_expires_label": "Åtkomsttoken löper ut om", + "account_tokens_dialog_expires_unchanged": "Lämna utgångsdatumet oförändrat", + "account_tokens_dialog_expires_x_hours": "Token går ut om {{hours}} timmar", + "account_tokens_dialog_expires_x_days": "Token löper ut om {{days}} dagar", + "account_tokens_dialog_expires_never": "Token upphör aldrig att gälla", + "account_tokens_delete_dialog_title": "Ta bort åtkomsttoken", + "account_tokens_delete_dialog_description": "Innan du tar bort en åtkomsttoken bör du se till att inga program eller skript använder den aktivt. Den här åtgärden kan inte ångras.", + "prefs_notifications_title": "Notifieringar", + "prefs_notifications_sound_title": "Ljud för meddelanden", + "prefs_notifications_sound_description_none": "Meddelanden spelar inte upp något ljud när de kommer", + "prefs_notifications_sound_play": "Spela upp valt ljud", + "prefs_notifications_min_priority_title": "Lägsta prioritet", + "prefs_notifications_min_priority_description_any": "Visa alla meddelanden, oavsett prioritet", + "prefs_notifications_min_priority_description_x_or_higher": "Visa meddelanden om prioritet är {{number}} ({{name}}) eller högre", + "prefs_notifications_min_priority_description_max": "Visa notifieringar om prioritet är 5 (max)", + "prefs_notifications_min_priority_default_and_higher": "Standardprioritet och högre", + "prefs_notifications_min_priority_high_and_higher": "Hög prioritet och högre", + "prefs_notifications_min_priority_max_only": "Bara högsta prioritet", + "prefs_notifications_delete_after_title": "Radera meddelanden", + "prefs_notifications_delete_after_one_week": "Efter en vecka", + "prefs_notifications_delete_after_one_month": "Efter en månad", + "prefs_notifications_delete_after_never_description": "Meddelanden raderas aldrig automatiskt", + "prefs_notifications_delete_after_three_hours_description": "Meddelanden raderas automatiskt efter tre timmar", + "prefs_users_description": "Lägg till/ta bort användare för dina skyddade ämnen här. Observera att användarnamn och lösenord lagras i webbläsarens lokala lagring.", + "prefs_users_delete_button": "Ta bort användare", + "prefs_users_table_cannot_delete_or_edit": "Kan inte ta bort eller redigera inloggad användare", + "prefs_users_table_user_header": "Användare", + "prefs_users_table_base_url_header": "Service-URL", + "prefs_users_dialog_username_label": "Användarnamn, t.ex. phil", + "prefs_reservations_title": "Reserverade ämnen", + "prefs_reservations_description": "Du kan reservera ämnesnamn för personligt bruk här. Genom att reservera ett ämne får du äganderätt till ämnet och kan definiera åtkomstbehörigheter för andra användare till ämnet.", + "prefs_reservations_limit_reached": "Du har nått gränsen för reserverade ämnen.", + "prefs_reservations_add_button": "Lägg till reserverat ämne", + "prefs_reservations_dialog_title_edit": "Redigera reserverat ämne", + "prefs_reservations_dialog_title_delete": "Ta bort ämnesreservation", + "signup_error_creation_limit_reached": "Gränsen för skapande av konton har uppnåtts", + "alert_not_supported_context_description": "Meddelanden stöds endast via HTTPS. Detta är en begränsning av Notifications API.", + "notifications_actions_not_supported": "Åtgärd stöds inte i webbapplikationen", + "notifications_none_for_any_description": "För att skicka meddelanden till ett ämne är det bara att PUT eller POST till ämnets URL. Här är ett exempel med ett av dina ämnen.", + "notifications_no_subscriptions_description": "Klicka på länken \"{{linktext}}\" för att skapa eller prenumerera på ett ämne. Därefter kan du skicka meddelanden via PUT eller POST och du får meddelanden här.", + "display_name_dialog_title": "Ändra visningsnamn", + "display_name_dialog_description": "Ange ett alternativt namn för ett ämne som visas i prenumerationslistan. På så sätt kan du lättare identifiera ämnen med komplicerade namn.", + "display_name_dialog_placeholder": "Visningsnamn", + "reserve_dialog_checkbox_label": "Reservera ämne och konfigurera åtkomst", + "publish_dialog_title_no_topic": "Publicera meddelande", + "publish_dialog_progress_uploading_detail": "Laddar upp {{loaded}}/{{{total}} ({{procent}}}%) …", + "publish_dialog_priority_min": "Lägsta prioritet", + "publish_dialog_priority_low": "Låg prioritet", + "publish_dialog_priority_default": "Standard prioritet", + "publish_dialog_priority_high": "Hög prioritet", + "publish_dialog_priority_max": "Max. prioritet", + "publish_dialog_base_url_label": "Service-URL", + "publish_dialog_email_label": "E-post", + "publish_dialog_attach_reset": "Ta bort URL för bifogade filer", + "publish_dialog_chip_email_label": "Vidarebefordra till e-post", + "publish_dialog_chip_attach_url_label": "Bifoga fil via URL", + "publish_dialog_chip_attach_file_label": "Bifoga lokal fil", + "publish_dialog_chip_delay_label": "Fördröj leveransen", + "publish_dialog_chip_topic_label": "Ändra ämne", + "publish_dialog_button_cancel_sending": "Avbryt sändning", + "publish_dialog_button_cancel": "Avbryt", + "publish_dialog_attached_file_remove": "Ta bort bifogad fil", + "publish_dialog_drop_file_here": "Släpp filen här", + "emoji_picker_search_clear": "Rensa sökning", + "subscribe_dialog_subscribe_title": "Prenumerera på ämnet", + "subscribe_dialog_subscribe_description": "Ämnen kanske inte är lösenordsskyddade, så välj ett namn som inte är lätt att gissa. När du har prenumererat kan du lägga in/lägga in meddelanden.", + "subscribe_dialog_subscribe_topic_placeholder": "Ämnesnamn, t.ex. phils_alerts", + "subscribe_dialog_subscribe_use_another_label": "Använd en annan server", + "subscribe_dialog_subscribe_base_url_label": "Service-URL", + "subscribe_dialog_subscribe_button_generate_topic_name": "Generera namn", + "subscribe_dialog_subscribe_button_subscribe": "Prenumerera", + "subscribe_dialog_login_title": "Inloggning krävs", + "subscribe_dialog_login_description": "Det här ämnet är lösenordsskyddat. Ange användarnamn och lösenord för att prenumerera.", + "subscribe_dialog_login_username_label": "Användarnamn, t.ex. phil", + "subscribe_dialog_login_password_label": "Lösenord", + "subscribe_dialog_login_button_login": "Logga in", + "subscribe_dialog_error_user_not_authorized": "Användaren {{användarnamn}} inte auktoriserad", + "subscribe_dialog_error_topic_already_reserved": "Ämnet är redan reserverat", + "account_basics_title": "Konto", + "account_basics_tier_paid_until": "Prenumerationen är betald fram till {{datum}}, och kommer att förnyas automatiskt", + "account_basics_username_title": "Användarnamn", + "account_basics_username_description": "Hej, det är du ❤", + "account_basics_username_admin_tooltip": "Du är admin", + "account_basics_password_title": "Lösenord", + "account_basics_password_description": "Ändra lösenordet till ditt konto", + "account_basics_tier_payment_overdue": "Din betalning är försenad. Vänligen uppdatera din betalningsmetod, annars kommer ditt konto att nedgraderas inom kort.", + "account_basics_password_dialog_title": "Byt lösenord", + "account_basics_password_dialog_current_password_label": "Aktuellt lösenord", + "account_basics_password_dialog_new_password_label": "Nytt lösenord", + "account_basics_password_dialog_button_submit": "Byt lösenord", + "account_basics_password_dialog_current_password_incorrect": "Felaktigt lösenord", + "account_usage_title": "Användning", + "account_usage_of_limit": "av {{limit}}", + "account_usage_unlimited": "Obegränsad", + "account_usage_limits_reset_daily": "Användningsgränserna återställs dagligen vid midnatt (UTC)", + "account_basics_tier_title": "Kontotyp", + "account_basics_tier_description": "Ditt kontos nivå", + "account_basics_tier_admin": "Admin", + "account_basics_tier_admin_suffix_with_tier": "(med {{tier}}} nivå)", + "account_basics_tier_admin_suffix_no_tier": "(ingen nivå)", + "account_basics_tier_basic": "Grundläggande", + "account_basics_tier_upgrade_button": "Uppgradera till Pro", + "account_basics_tier_change_button": "Ändra", + "account_usage_cannot_create_portal_session": "Det går inte att öppna faktureringsportalen", + "account_usage_basis_ip_description": "Användningsstatistik och begränsningar för det här kontot baseras på din IP-adress, så de kan delas med andra användare. De gränser som visas ovan är ungefärliga och baseras på befintliga gränser.", + "account_tokens_title": "Åtkomsttoken", + "prefs_notifications_delete_after_one_day_description": "Meddelanden raderas automatiskt efter en dag", + "prefs_notifications_delete_after_one_week_description": "Meddelanden raderas automatiskt efter en vecka", + "prefs_notifications_delete_after_one_month_description": "Meddelanden raderas automatiskt efter en månad", + "prefs_users_title": "Hantera användare", + "prefs_reservations_table_not_subscribed": "Prenumererar inte", + "prefs_reservations_table_click_to_subscribe": "Klicka för att prenumerera", + "prefs_reservations_edit_button": "Redigera ämnesåtkomst", + "prefs_reservations_delete_button": "Återställ ämnesåtkomst", + "prefs_reservations_table": "Tabell över reserverade ämnen", + "prefs_reservations_table_topic_header": "Ämne", + "prefs_reservations_table_access_header": "Tillgång", + "prefs_reservations_table_everyone_deny_all": "Endast jag kan publicera och prenumerera", + "prefs_reservations_table_everyone_read_only": "Jag kan publicera och prenumerera, alla kan prenumerera", + "prefs_reservations_table_everyone_write_only": "Jag kan publicera och prenumerera, alla kan publicera", + "prefs_reservations_table_everyone_read_write": "Alla kan publicera och prenumerera", + "prefs_reservations_dialog_title_add": "Reserverade ämnen", + "prefs_reservations_dialog_description": "Genom att reservera ett ämne får du äganderätt till ämnet och kan definiera åtkomstbehörigheter för andra användare till ämnet.", + "prefs_reservations_dialog_topic_label": "Ämne", + "prefs_reservations_dialog_access_label": "Tillgång", + "reservation_delete_dialog_action_keep_title": "Behåll cachade meddelanden och bilagor", + "reservation_delete_dialog_action_keep_description": "Meddelanden och bilagor som lagras på servern blir offentligt synliga för personer som känner till ämnesnamnet.", + "reservation_delete_dialog_action_delete_title": "Ta bort meddelanden och bilagor som sparats i cacheminnet", + "reservation_delete_dialog_description": "Om du tar bort en reservation ger du upp äganderätten till ämnet och låter andra reservera det. Du kan behålla eller radera befintliga meddelanden och bilagor.", + "publish_dialog_call_label": "Telefonsamtal", + "publish_dialog_call_reset": "Ta bort telefonsamtal", + "publish_dialog_chip_call_label": "Telefonsamtal", + "account_basics_phone_numbers_title": "Telefonnummer", + "account_basics_phone_numbers_description": "För notifieringar via telefonsamtal", + "account_basics_phone_numbers_no_phone_numbers_yet": "Inga telefonnummer ännu", + "account_basics_phone_numbers_copied_to_clipboard": "Telefonnummer kopierat till urklipp", + "account_basics_phone_numbers_dialog_title": "Lägga till telefonnummer", + "account_basics_phone_numbers_dialog_number_label": "Telefonnummer", + "account_basics_phone_numbers_dialog_number_placeholder": "t.ex. +1222333444", + "account_basics_phone_numbers_dialog_verify_button_sms": "Skicka SMS", + "account_basics_phone_numbers_dialog_verify_button_call": "Ring mig", + "account_basics_phone_numbers_dialog_code_label": "Verifieringskod", + "account_basics_phone_numbers_dialog_channel_call": "Ring", + "account_usage_calls_title": "Telefonsamtal som gjorts", + "account_usage_calls_none": "Inga telefonsamtal kan göras med detta konto", + "publish_dialog_call_item": "Ring telefonnummer {{number}}", + "publish_dialog_chip_call_no_verified_numbers_tooltip": "Inga verifierade telefonnummer", + "account_basics_phone_numbers_dialog_description": "För att använda funktionen för samtalsavisering måste du lägga till och verifiera minst ett telefonnummer. Verifieringen kan göras via SMS eller ett telefonsamtal.", + "account_basics_phone_numbers_dialog_code_placeholder": "t.ex. 123456", + "account_basics_phone_numbers_dialog_check_verification_button": "Bekräfta kod", + "account_basics_phone_numbers_dialog_channel_sms": "SMS", + "account_upgrade_dialog_tier_features_calls_other": "{{calls}} dagliga telefonsamtal", + "account_upgrade_dialog_tier_features_no_calls": "Inga telefonsamtal", + "account_upgrade_dialog_tier_features_calls_one": "{{calls}} dagliga telefonsamtal" } diff --git a/web/public/static/langs/tr.json b/web/public/static/langs/tr.json index bb88cc77..28eca9f6 100644 --- a/web/public/static/langs/tr.json +++ b/web/public/static/langs/tr.json @@ -34,7 +34,7 @@ "subscribe_dialog_login_description": "Bu konu parola korumalı. Abone olmak için lütfen kullanıcı adı ve parola girin.", "subscribe_dialog_login_username_label": "Kullanıcı adı, örn. phil", "subscribe_dialog_login_password_label": "Parola", - "subscribe_dialog_login_button_back": "Geri", + "common_back": "Geri", "subscribe_dialog_login_button_login": "Oturum aç", "subscribe_dialog_error_user_not_authorized": "{{username}} kullanıcısı yetkili değil", "subscribe_dialog_error_user_anonymous": "anonim", @@ -44,7 +44,7 @@ "prefs_notifications_min_priority_title": "En düşük öncelik", "prefs_notifications_min_priority_any": "Herhangi bir öncelik", "publish_dialog_topic_placeholder": "Konu adı, örn. benim_uyarilarim", - "alert_grant_button": "Şimdi ver", + "alert_notification_permission_required_button": "Şimdi ver", "alert_not_supported_title": "Bildirimler desteklenmiyor", "notifications_attachment_link_expires": "bağlantının süresi {{date}} tarihinde doluyor", "notifications_click_copy_url_title": "Bağlantı URL'sini panoya kopyala", @@ -59,8 +59,8 @@ "notifications_attachment_open_button": "Eki aç", "nav_button_documentation": "Belgelendirme", "nav_button_publish_message": "Bildirim yayınla", - "alert_grant_title": "Bildirimler devre dışı", - "alert_grant_description": "Tarayıcınıza masaüstü bildirimlerini görüntüleme izni verin.", + "alert_notification_permission_required_title": "Bildirimler devre dışı", + "alert_notification_permission_required_description": "Tarayıcınıza masaüstü bildirimlerini görüntüleme izni verin.", "alert_not_supported_description": "Tarayıcınızda bildirimler desteklenmiyor.", "notifications_copied_to_clipboard": "Panoya kopyalandı", "notifications_tags": "Etiketler", @@ -77,7 +77,7 @@ "notifications_example": "Örnek", "notifications_more_details": "Daha fazla bilgi için web sitesine veya belgelendirmeye bakın.", "publish_dialog_chip_attach_url_label": "URL ile dosya ekle", - "prefs_notifications_min_priority_default_and_higher": "Öntanımlı öncelik ve üstü", + "prefs_notifications_min_priority_default_and_higher": "Varsayılan öncelik ve üstü", "prefs_notifications_delete_after_three_hours": "Üç saat sonra", "notifications_none_for_any_description": "Bir konuya bildirim göndermek için konu URL'sine PUT veya POST göndermeniz yeterlidir. İşte konularınızdan birini kullanan bir örnek.", "notifications_no_subscriptions_title": "Henüz aboneliğiniz yok gibi görünüyor.", @@ -268,7 +268,7 @@ "account_tokens_table_token_header": "Belirteç", "account_tokens_table_label_header": "Etiket", "account_tokens_table_current_session": "Geçerli tarayıcı oturumu", - "account_tokens_table_copy_to_clipboard": "Panoya kopyala", + "common_copy_to_clipboard": "Panoya kopyala", "account_tokens_table_copied_to_clipboard": "Erişim belirteci kopyalandı", "account_tokens_table_cannot_delete_or_edit": "Geçerli oturum belirteci düzenlenemez veya silinemez", "account_tokens_table_create_token_button": "Erişim belirteci oluştur", @@ -352,5 +352,33 @@ "account_upgrade_dialog_interval_yearly_discount_save_up_to": "%{{discount}} kadar tasarruf edin", "account_upgrade_dialog_interval_monthly": "Aylık", "account_basics_tier_interval_monthly": "aylık", - "account_upgrade_dialog_billing_contact_website": "Faturalama ile ilgili sorularınız için lütfen web sitemizi ziyaret edin." + "account_upgrade_dialog_billing_contact_website": "Faturalama ile ilgili sorularınız için lütfen web sitemizi ziyaret edin.", + "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} ayırtılan konu", + "account_upgrade_dialog_tier_features_emails_one": "{{emails}} günlük e-posta", + "account_upgrade_dialog_tier_features_messages_one": "{{messages}} günlük mesaj", + "account_upgrade_dialog_tier_features_calls_one": "{{calls}} günlük telefon araması", + "account_upgrade_dialog_tier_features_calls_other": "{{calls}} günlük telefon araması", + "publish_dialog_call_label": "Telefon araması", + "publish_dialog_call_reset": "Telefon aramasını kaldır", + "publish_dialog_chip_call_label": "Telefon araması", + "account_basics_phone_numbers_title": "Telefon numaraları", + "account_basics_phone_numbers_dialog_description": "Arama bildirimi özelliğini kullanmak için en az bir telefon numarası eklemeniz ve doğrulamanız gerekir. Doğrulama SMS veya telefon araması yoluyla yapılabilir.", + "account_basics_phone_numbers_description": "Telefon araması bildirimleri için", + "account_basics_phone_numbers_no_phone_numbers_yet": "Henüz telefon numarası yok", + "account_basics_phone_numbers_copied_to_clipboard": "Telefon numarası panoya kopyalandı", + "account_basics_phone_numbers_dialog_title": "Telefon numarası ekle", + "account_basics_phone_numbers_dialog_number_label": "Telefon numarası", + "account_basics_phone_numbers_dialog_check_verification_button": "Kodu doğrula", + "account_basics_phone_numbers_dialog_channel_sms": "SMS", + "account_basics_phone_numbers_dialog_channel_call": "Ara", + "account_usage_calls_none": "Bu hesapla telefon araması yapılamaz", + "publish_dialog_call_item": "{{number}} telefon numarasını ara", + "publish_dialog_chip_call_no_verified_numbers_tooltip": "Doğrulanan telefon numarası yok", + "account_basics_phone_numbers_dialog_number_placeholder": "örn. +905554443322", + "account_basics_phone_numbers_dialog_verify_button_sms": "SMS gönder", + "account_basics_phone_numbers_dialog_verify_button_call": "Beni ara", + "account_basics_phone_numbers_dialog_code_label": "Doğrulama kodu", + "account_basics_phone_numbers_dialog_code_placeholder": "örn. 123456", + "account_usage_calls_title": "Yapılan telefon aramaları", + "account_upgrade_dialog_tier_features_no_calls": "Telefon araması yok" } diff --git a/web/public/static/langs/uk.json b/web/public/static/langs/uk.json index 686a3d3e..b09822dd 100644 --- a/web/public/static/langs/uk.json +++ b/web/public/static/langs/uk.json @@ -10,9 +10,9 @@ "nav_button_subscribe": "Підписатися на тему", "nav_button_muted": "Сповіщення вимкнено", "nav_button_connecting": "підключення", - "alert_grant_title": "Сповіщення вимкнено", - "alert_grant_description": "Дозвольте браузеру показувати сповіщення.", - "alert_grant_button": "Дозволити", + "alert_notification_permission_required_title": "Сповіщення вимкнено", + "alert_notification_permission_required_description": "Дозвольте браузеру показувати сповіщення.", + "alert_notification_permission_required_button": "Дозволити", "alert_not_supported_title": "Сповіщення не підтримуються", "notifications_list_item": "Сповіщення", "notifications_attachment_image": "Прикріплене зображення", @@ -53,7 +53,7 @@ "subscribe_dialog_subscribe_use_another_label": "Використовувати інший сервер", "subscribe_dialog_subscribe_base_url_label": "URL служби", "subscribe_dialog_login_password_label": "Пароль", - "subscribe_dialog_login_button_back": "Назад", + "common_back": "Назад", "subscribe_dialog_error_user_not_authorized": "{{username}} користувач не авторизований", "prefs_notifications_sound_description_none": "Сповіщення не відтворюють жодного звуку при надходженні", "prefs_notifications_sound_description_some": "Сповіщення відтворюють звук {{sound}}", @@ -237,5 +237,149 @@ "display_name_dialog_description": "Задайте альтернативну назву для теми, яка відображатиметься у списку підписок. Це допоможе легше ідентифікувати теми зі складними назвами.", "display_name_dialog_placeholder": "Відображуване ім'я", "account_basics_password_title": "Пароль", - "account_basics_username_admin_tooltip": "Ви адміністратор" + "account_basics_username_admin_tooltip": "Ви адміністратор", + "account_basics_tier_interval_monthly": "щомісяця", + "common_copy_to_clipboard": "Скопіювати в буфер обміну", + "account_basics_phone_numbers_title": "Номери телефонів", + "account_basics_phone_numbers_description": "Для сповіщень через телефонні дзвінки", + "account_basics_phone_numbers_no_phone_numbers_yet": "Поки що немає номерів телефонів", + "account_basics_phone_numbers_copied_to_clipboard": "Номер телефону скопійовано в буфер обміну", + "account_basics_phone_numbers_dialog_title": "Додати номер телефону", + "account_basics_phone_numbers_dialog_number_label": "Номер телефону", + "account_basics_phone_numbers_dialog_number_placeholder": "наприклад, +1222333444", + "account_basics_phone_numbers_dialog_verify_button_sms": "Надіслати SMS", + "account_basics_phone_numbers_dialog_verify_button_call": "Зателефонуйте мені", + "account_basics_phone_numbers_dialog_code_label": "Код підтвердження", + "account_basics_phone_numbers_dialog_code_placeholder": "наприклад, 123456", + "account_basics_phone_numbers_dialog_check_verification_button": "Підтвердити код", + "account_basics_phone_numbers_dialog_channel_sms": "SMS", + "account_basics_phone_numbers_dialog_channel_call": "Дзвінок", + "account_basics_tier_interval_yearly": "щороку", + "account_usage_calls_title": "Здійснені телефонні дзвінки", + "account_usage_calls_none": "З цього облікового запису не можна здійснювати телефонні дзвінки", + "account_usage_attachment_storage_title": "Зберігання вкладень", + "account_usage_attachment_storage_description": "{{filesize}} на файл, видаляється після {{expiry}}", + "account_usage_basis_ip_description": "Статистика використання та ліміти для цього облікового запису базуються на вашій IP-адресі, тому вони можуть бути доступні іншим користувачам. Ліміти, показані вище, є приблизними і базуються на існуючих лімітах тарифів.", + "account_usage_cannot_create_portal_session": "Не вдається відкрити білінговий портал", + "account_delete_title": "Видалення облікового запису", + "account_delete_description": "Назавжди видалити свій обліковий запис", + "account_delete_dialog_label": "Пароль", + "account_delete_dialog_button_cancel": "Скасувати", + "account_delete_dialog_button_submit": "Видалити обліковий запис назавжди", + "account_delete_dialog_billing_warning": "Видалення облікового запису також негайно скасовує вашу підписку. Ви більше не матимете доступу до білінгової панелі.", + "account_upgrade_dialog_title": "Зміна рівня облікового запису", + "account_upgrade_dialog_interval_monthly": "Щомісяця", + "account_upgrade_dialog_interval_yearly": "Щорічно", + "account_upgrade_dialog_interval_yearly_discount_save": "економія {{discount}}%", + "account_upgrade_dialog_interval_yearly_discount_save_up_to": "економія до {{discount}}%", + "publish_dialog_call_label": "Телефонний дзвінок", + "publish_dialog_call_placeholder": "Номер телефону, на який потрібно зателефонувати з повідомленням, наприклад, +12223334444 або \"yes\"", + "publish_dialog_chip_call_label": "Телефонний дзвінок", + "publish_dialog_call_reset": "Видалити телефонний дзвінок", + "account_basics_phone_numbers_dialog_description": "Щоб користуватися функцією сповіщення про дзвінки, потрібно додати та верифікувати принаймні один телефонний номер. Верифікацію можна здійснити за допомогою SMS або телефонного дзвінка.", + "account_delete_dialog_description": "Це призведе до остаточного видалення вашого облікового запису, включаючи всі дані, які зберігаються на сервері. Після видалення ваше ім'я користувача буде недоступне протягом 7 днів. Якщо ви дійсно хочете продовжити, будь ласка, підтвердьте свій пароль у полі нижче.", + "account_basics_tier_upgrade_button": "Оновлення до Pro", + "account_basics_password_description": "Зміна пароля облікового запису", + "account_usage_of_limit": "з {{limit}}", + "account_usage_unlimited": "Без обмежень", + "account_basics_tier_description": "Рівень потужності вашого облікового запису", + "account_basics_tier_admin_suffix_with_tier": "(з рівнем {{tier}})", + "account_basics_tier_admin_suffix_no_tier": "(без рівня)", + "account_basics_tier_basic": "Базовий", + "account_basics_tier_free": "Безкоштовний", + "account_basics_tier_change_button": "Змінити", + "account_basics_tier_paid_until": "Підписка оплачена до {{date}} і буде автоматично поновлюватися", + "account_basics_tier_payment_overdue": "Ваш платіж прострочено. Будь ласка, оновіть спосіб оплати, інакше ваш обліковий запис буде знижено до нижчого рівня.", + "account_basics_tier_canceled_subscription": "Вашу підписку було скасовано, і з {{date}} вона буде знижена до безкоштовного акаунта.", + "account_basics_tier_manage_billing_button": "Керувати рахунками", + "account_usage_messages_title": "Опубліковані повідомлення", + "account_usage_emails_title": "Надіслані електронні листи", + "account_usage_reservations_title": "Зарезервовані теми", + "account_usage_reservations_none": "Для цього облікового запису немає зарезервованих тем", + "account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} на файл", + "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} загальне сховище", + "account_upgrade_dialog_tier_current_label": "Поточний", + "account_upgrade_dialog_tier_selected_label": "Вибране", + "account_upgrade_dialog_cancel_warning": "Це скасує вашу підписку і знизить версію вашого облікового запису {{date}}. У цю дату резервування тем, а також повідомлення, кешовані на сервері , буде видалено.", + "account_upgrade_dialog_tier_features_reservations_other": "{{reservations}} зарезервовані теми", + "account_upgrade_dialog_tier_features_no_reservations": "Немає зарезервованих тем", + "account_upgrade_dialog_tier_features_messages_other": "{{messages}} повідомлень в день", + "account_upgrade_dialog_tier_features_emails_one": "{{emails}} електронний лист в день", + "account_upgrade_dialog_tier_features_emails_other": "{{emails}} електронних листів в день", + "account_upgrade_dialog_tier_features_calls_one": "{{calls}} телефонний дзвінок в день", + "account_upgrade_dialog_tier_features_calls_other": "{{дзвінки}} телефонних дзвінків в день", + "account_upgrade_dialog_tier_features_no_calls": "Без телефонних дзвінків", + "account_upgrade_dialog_tier_price_per_month": "місяць", + "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} на рік. Рахунок виставляється щомісяця.", + "account_upgrade_dialog_tier_price_billed_yearly": "{{price}} виставляється щорічно. Збережіть {{save}}.", + "account_upgrade_dialog_billing_contact_email": "Якщо у вас виникли запитання щодо оплати, зв’яжіться з нами безпосередньо.", + "account_upgrade_dialog_billing_contact_website": "Якщо у вас виникли запитання щодо оплати, відвідайте наш веб-сайт.", + "account_upgrade_dialog_button_cancel_subscription": "Скасувати підписку", + "account_upgrade_dialog_button_update_subscription": "Оновити підписку", + "account_tokens_title": "Токени доступу", + "account_tokens_table_expires_header": "Термін дії закінчується", + "account_tokens_description": "Використовуйте токени доступу при публікації та підписці через ntfy API, щоб не надсилати свої облікові дані. Ознайомтеся з документацією, щоб дізнатися більше.", + "account_tokens_table_token_header": "Токен", + "account_tokens_table_never_expires": "Ніколи не закінчується", + "account_tokens_table_label_header": "Мітка", + "account_tokens_table_current_session": "Поточний сеанс браузера", + "account_tokens_table_last_access_header": "Останній доступ", + "account_tokens_table_copied_to_clipboard": "Токен доступу скопійовано", + "account_tokens_table_cannot_delete_or_edit": "Неможливо редагувати або видалити токен поточного сеансу", + "account_tokens_table_create_token_button": "Створити токен доступу", + "account_tokens_table_last_origin_tooltip": "З IP-адреси {{ip}} натисніть для пошуку", + "account_tokens_dialog_title_create": "Створити токен доступу", + "account_tokens_dialog_button_cancel": "Скасувати", + "account_tokens_dialog_title_edit": "Редагувати токен доступу", + "account_tokens_dialog_title_delete": "Видалити токен доступу", + "account_tokens_dialog_label": "Мітка, наприклад, сповіщення Radarr", + "account_tokens_dialog_button_create": "Створити токен", + "account_tokens_dialog_button_update": "Оновити токен", + "account_tokens_dialog_expires_label": "Термін дії токену доступу закінчується через", + "account_tokens_dialog_expires_x_hours": "Термін дії токена закінчується через {{hours}} годин", + "account_tokens_dialog_expires_x_days": "Термін дії токена закінчується через {{days}} днів", + "account_tokens_delete_dialog_description": "Перш ніж видалити токен доступу, переконайтеся, що жодна програма або скрипт не використовує його. Ця дія не може бути скасована.", + "prefs_users_description_no_sync": "Користувачі та паролі не синхронізуються з вашим акаунтом.", + "prefs_users_table_cannot_delete_or_edit": "Неможливо видалити або відредагувати користувача, який увійшов у систему", + "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} зарезервована тема", + "account_upgrade_dialog_tier_features_messages_one": "{{messages}} повідомлення в день", + "account_tokens_dialog_expires_unchanged": "Залишити термін придатності без змін", + "account_tokens_dialog_expires_never": "Термін дії токена ніколи не закінчується", + "account_tokens_delete_dialog_title": "Видалити токен доступу", + "account_tokens_delete_dialog_submit_button": "Видалити токен назавжди", + "account_upgrade_dialog_proration_info": "Пропорція: При переході з одного тарифного плану на інший різниця в ціні буде списана негайно. При переході на нижчий рівень залишок коштів буде використано для оплати майбутніх розрахункових періодів.", + "account_upgrade_dialog_reservations_warning_one": "Обраний рівень дозволяє менше зарезервованих тем, ніж ваш поточний рівень. Перш ніж змінити свій рівень, будь ласка, видаліть принаймні одне резервування. Ви можете видалити резервування в Налаштуваннях.", + "account_upgrade_dialog_reservations_warning_other": "Обраний рівень дозволяє менше зарезервованих тем, ніж ваш поточний рівень. Перш ніж змінити свій рівень, будь ласка, видаліть принаймні {{count}} резервувань. Ви можете видалити резервування в Налаштуваннях.", + "account_upgrade_dialog_button_cancel": "Скасувати", + "account_upgrade_dialog_button_redirect_signup": "Зареєструватися зараз", + "account_upgrade_dialog_button_pay_now": "Оплатити зараз і підписатися", + "prefs_reservations_add_button": "Додати зарезервовану тему", + "prefs_reservations_edit_button": "Редагувати доступ до теми", + "prefs_reservations_limit_reached": "Ви досягли ліміту зарезервованих тем.", + "prefs_reservations_table_click_to_subscribe": "Натисніть, щоб підписатися", + "prefs_reservations_table_topic_header": "Тема", + "prefs_reservations_description": "Тут ви можете зарезервувати назви тем для особистого користування. Резервування теми дає вам право власності на тему і дозволяє визначати права доступу до неї інших користувачів.", + "prefs_reservations_table": "Таблиця зарезервованих тем", + "prefs_reservations_table_access_header": "Доступ", + "prefs_reservations_table_everyone_deny_all": "Тільки я можу публікувати та підписуватись", + "prefs_reservations_table_everyone_read_only": "Я можу публікувати та підписуватись, кожен може підписатися", + "prefs_reservations_table_everyone_write_only": "Я можу публікувати і підписуватися, кожен може публікувати", + "prefs_reservations_table_everyone_read_write": "Кожен може публікувати та підписуватися", + "prefs_reservations_table_not_subscribed": "Не підписаний", + "prefs_reservations_dialog_title_add": "Зарезервувати тему", + "prefs_reservations_dialog_title_edit": "Редагувати зарезервовану тему", + "prefs_reservations_title": "Зарезервовані теми", + "prefs_reservations_delete_button": "Скинути доступ до теми", + "prefs_reservations_dialog_description": "Резервування теми дає вам право власності на цю тему і дозволяє визначати права доступу до неї інших користувачів.", + "prefs_reservations_dialog_topic_label": "Тема", + "prefs_reservations_dialog_access_label": "Доступ", + "reservation_delete_dialog_description": "Видалення резервування позбавляє вас права власності на тему і дозволяє іншим зарезервувати її. Ви можете зберегти або видалити існуючі повідомлення і вкладення.", + "reservation_delete_dialog_submit_button": "Видалити резервування", + "publish_dialog_call_item": "Телефонувати за номером {{номер}}", + "publish_dialog_chip_call_no_verified_numbers_tooltip": "Немає підтверджених номерів телефонів", + "prefs_reservations_dialog_title_delete": "Видалити резервування теми", + "reservation_delete_dialog_action_delete_title": "Видалення кешованих повідомлень і вкладень", + "reservation_delete_dialog_action_keep_title": "Збереження кешованих повідомлень і вкладень", + "reservation_delete_dialog_action_keep_description": "Повідомлення і вкладення, які кешуються на сервері, стають загальнодоступними для людей, які знають назву теми.", + "reservation_delete_dialog_action_delete_description": "Кешовані повідомлення та вкладення будуть видалені назавжди. Ця дія не може бути скасована." } diff --git a/web/public/static/langs/vi.json b/web/public/static/langs/vi.json new file mode 100644 index 00000000..b2f94441 --- /dev/null +++ b/web/public/static/langs/vi.json @@ -0,0 +1,21 @@ +{ + "common_add": "Thêm", + "common_back": "Quay lại", + "signup_title": "Tạo tài khoản ntfy", + "signup_form_toggle_password_visibility": "Hiện mật khẩu", + "login_form_button_submit": "Đăng nhập", + "common_copy_to_clipboard": "Lưu vào clipboard", + "signup_form_username": "Tên user", + "signup_already_have_account": "Đã có tài khoản? Đăng nhập!", + "signup_disabled": "Đăng kí bị đóng", + "signup_error_username_taken": "Tên {{username}} đã được sử dụng", + "signup_error_creation_limit_reached": "Đã bị giới hạn tạo tài khoản", + "login_title": "Đăng nhập vào tài khoản ntfy", + "login_link_signup": "Đăng kí", + "login_disabled": "Đăng nhập bị đóng", + "action_bar_show_menu": "Hiện menu", + "signup_form_password": "Mật khẩu", + "action_bar_settings": "Cài đặt", + "signup_form_confirm_password": "Xác nhận mật khẩu", + "signup_form_button_submit": "Đăng kí" +} diff --git a/web/public/static/langs/zh_Hans.json b/web/public/static/langs/zh_Hans.json index 4da4328c..e26e7f14 100644 --- a/web/public/static/langs/zh_Hans.json +++ b/web/public/static/langs/zh_Hans.json @@ -1,11 +1,13 @@ { "action_bar_show_menu": "显示菜单", "action_bar_logo_alt": "ntfy图标", + "action_bar_mute_notifications": "静音", "action_bar_settings": "设置", "action_bar_send_test_notification": "发送测试通知", "action_bar_clear_notifications": "清除所有通知", "action_bar_unsubscribe": "取消订阅", "action_bar_toggle_action_menu": "开启或关闭操作菜单", + "action_bar_unmute_notifications": "取消静音", "message_bar_type_message": "在此处输入消息", "message_bar_show_dialog": "显示发布对话框", "message_bar_publish": "发布消息", @@ -15,11 +17,15 @@ "nav_button_publish_message": "发布通知", "nav_button_subscribe": "订阅主题", "nav_button_connecting": "正在连接", - "alert_grant_title": "已禁用通知", - "alert_grant_description": "授予浏览器显示桌面通知的权限。", - "alert_grant_button": "现在授予", + "alert_notification_permission_required_title": "已禁用通知", + "alert_notification_permission_required_description": "授予浏览器显示桌面通知的权限。", + "alert_notification_permission_required_button": "现在授予", "alert_not_supported_title": "不支持通知", "alert_not_supported_description": "您的浏览器不支持通知。", + "alert_notification_ios_install_required_description": "要接收通知,请在iOS上点击分享图标,然后添加到主屏幕。", + "alert_notification_ios_install_required_title": "需要安装iOS应用程序", + "alert_notification_permission_denied_description": "你已禁用通知。要重新启用通知,请在浏览器设置中启用通知。", + "alert_notification_permission_denied_title": "已禁用通知", "notifications_list": "通知列表", "notifications_list_item": "通知", "notifications_mark_read": "标记为已读", @@ -103,7 +109,7 @@ "subscribe_dialog_login_description": "本主题受密码保护,请输入用户名和密码进行订阅。", "subscribe_dialog_login_username_label": "用户名,例如 phil", "subscribe_dialog_login_password_label": "密码", - "subscribe_dialog_login_button_back": "返回", + "common_back": "返回", "subscribe_dialog_login_button_login": "登录", "subscribe_dialog_error_user_not_authorized": "未授权 {{username}} 用户", "subscribe_dialog_error_user_anonymous": "匿名", @@ -117,9 +123,9 @@ "prefs_notifications_min_priority_description_x_or_higher": "仅显示优先级为{{number}}({{name}})或以上的通知", "prefs_notifications_min_priority_description_max": "仅显示最高优先级的通知", "prefs_notifications_min_priority_any": "任意优先级", - "prefs_notifications_min_priority_low_and_higher": "低优先级和更高优先级", - "prefs_notifications_min_priority_default_and_higher": "默认优先级和更高优先级", - "prefs_notifications_min_priority_high_and_higher": "高优先级和更高优先级", + "prefs_notifications_min_priority_low_and_higher": "低优先级或更高", + "prefs_notifications_min_priority_default_and_higher": "默认优先级或更高", + "prefs_notifications_min_priority_high_and_higher": "高优先级或更高", "prefs_notifications_min_priority_max_only": "仅最高优先级", "prefs_notifications_delete_after_never": "从不", "prefs_notifications_delete_after_one_month": "一月后", @@ -129,6 +135,11 @@ "prefs_notifications_delete_after_one_day_description": "一天后自动删除通知", "prefs_notifications_delete_after_one_week_description": "一周后自动删除通知", "prefs_notifications_delete_after_one_month_description": "一月后后自动删除通知", + "prefs_notifications_web_push_disabled": "已暂用", + "prefs_notifications_web_push_disabled_description": "当网页程序在运行时将会收到通知 (透过 WebSocket)", + "prefs_notifications_web_push_enabled": "已为 {{server}} 启用", + "prefs_notifications_web_push_enabled_description": "即使网页程序未有运行亦会收到通知 (via Web Push)", + "prefs_notifications_web_push_title": "背景通知", "prefs_users_title": "管理用户", "prefs_users_description": "在此处添加/删除受保护主题的用户。请注意,用户名和密码存储在浏览器的本地存储中。", "prefs_users_add_button": "添加用户", @@ -140,6 +151,10 @@ "common_save": "保存", "prefs_appearance_title": "外观", "prefs_appearance_language_title": "语言", + "prefs_appearance_theme_title": "主題", + "prefs_appearance_theme_system": "系統 (預設)", + "prefs_appearance_theme_dark": "黑暗模式", + "prefs_appearance_theme_light": "光亮模式", "priority_min": "最低", "priority_low": "低", "priority_default": "默认", @@ -149,6 +164,7 @@ "prefs_users_table_base_url_header": "服务链接地址", "prefs_users_dialog_base_url_label": "服务链接地址,例如 https://ntfy.sh", "error_boundary_button_copy_stack_trace": "复制堆栈跟踪", + "error_boundary_button_reload_ntfy": "重新加载 ntfy", "error_boundary_stack_trace": "堆栈跟踪", "error_boundary_gathering_info": "收集更多信息……", "error_boundary_unsupported_indexeddb_title": "不支持隐私浏览", @@ -160,6 +176,7 @@ "notifications_attachment_copy_url_button": "复制链接地址", "notifications_attachment_open_title": "转到 {{url}}", "notifications_actions_http_request_title": "发送 HTTP {{method}} 到 {{url}}", + "notifications_actions_failed_notification": "通知失败", "notifications_actions_open_url_title": "转到 {{url}}", "notifications_none_for_topic_description": "要向此主题发送通知,只需使用 PUT 或 POST 到主题链接即可。", "subscribe_dialog_subscribe_topic_placeholder": "主题名,例如 phil_alerts", @@ -168,12 +185,14 @@ "publish_dialog_title_placeholder": "主题标题,例如 磁盘空间告警", "publish_dialog_email_label": "电子邮件", "publish_dialog_button_send": "发送", + "publish_dialog_checkbox_markdown": "格式化为 Markdown", "publish_dialog_attachment_limits_quota_reached": "超过配额,剩余 {{remainingBytes}}", "publish_dialog_attach_label": "附件链接地址", "publish_dialog_click_reset": "移除点击连接地址", "publish_dialog_button_cancel": "取消", "subscribe_dialog_subscribe_button_cancel": "取消", "subscribe_dialog_subscribe_base_url_label": "服务地址地址", + "subscribe_dialog_subscribe_use_another_background_info": "当网页程序未开启, 将不会收到来自其他服务器的通知", "prefs_notifications_min_priority_description_any": "显示所有通知,无论优先级如何", "prefs_notifications_delete_after_title": "删除通知", "prefs_notifications_delete_after_three_hours": "三小时后", @@ -333,7 +352,7 @@ "account_tokens_table_expires_header": "过期", "account_tokens_table_never_expires": "永不过期", "account_tokens_table_current_session": "当前浏览器会话", - "account_tokens_table_copy_to_clipboard": "复制到剪贴板", + "common_copy_to_clipboard": "复制到剪贴板", "account_tokens_table_copied_to_clipboard": "已复制访问令牌", "account_tokens_table_cannot_delete_or_edit": "无法编辑或删除当前会话令牌", "account_tokens_table_create_token_button": "创建访问令牌", @@ -352,5 +371,37 @@ "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} 每年。按月计费。", "account_upgrade_dialog_tier_price_billed_yearly": "{{价格}} 按年计费。节省 {{save}}。", "account_upgrade_dialog_billing_contact_email": "有关账单问题,请直接联系我们 。", - "account_upgrade_dialog_billing_contact_website": "有关账单问题,请参考我们的网站 。" + "account_upgrade_dialog_billing_contact_website": "有关账单问题,请参考我们的网站 。", + "publish_dialog_call_item": "拨打电话 {{number}}", + "publish_dialog_call_label": "拨号", + "publish_dialog_chip_call_label": "拨号", + "publish_dialog_chip_call_no_verified_numbers_tooltip": "未验证的手机号", + "account_basics_phone_numbers_title": "电话号码", + "account_basics_phone_numbers_description": "电话通知", + "account_basics_phone_numbers_dialog_description": "要使用来电通知功能,您需要添加并验证至少一个电话号码。可以通过短信或电话进行验证。", + "account_basics_phone_numbers_dialog_code_label": "验证码", + "account_basics_phone_numbers_dialog_code_placeholder": "例如:123456", + "account_basics_phone_numbers_dialog_check_verification_button": "确认码", + "account_basics_phone_numbers_dialog_channel_sms": "短信", + "account_basics_phone_numbers_dialog_channel_call": "拨打", + "publish_dialog_call_reset": "清空拨号", + "account_basics_phone_numbers_no_phone_numbers_yet": "无可执行的电话号码", + "account_basics_phone_numbers_dialog_title": "添加电话号码", + "account_basics_phone_numbers_copied_to_clipboard": "电话号码已复制到剪贴板", + "account_basics_phone_numbers_dialog_number_label": "电话号码", + "account_basics_phone_numbers_dialog_number_placeholder": "例如:+1222333444", + "account_usage_calls_title": "已拨打电话", + "account_usage_calls_none": "此帐号无法拨打电话", + "account_upgrade_dialog_tier_features_reservations_one": "一条保留主题", + "account_upgrade_dialog_tier_features_emails_one": "一封每日邮件", + "account_upgrade_dialog_tier_features_calls_one": "一通每日电话", + "account_basics_phone_numbers_dialog_verify_button_sms": "发送信息", + "account_basics_phone_numbers_dialog_verify_button_call": "拨打电话", + "account_upgrade_dialog_tier_features_messages_one": "一条每日消息", + "account_upgrade_dialog_tier_features_calls_other": "{{calls}} 通每日电话", + "account_upgrade_dialog_tier_features_no_calls": "无电话呼叫", + "web_push_subscription_expiring_title": "通知将被暂停", + "web_push_subscription_expiring_body": "打开ntfy以继续接收通知", + "web_push_unknown_notification_title": "接收到未知通知", + "web_push_unknown_notification_body": "你可能需要打开网页来更新ntfy" } diff --git a/web/public/static/langs/zh_Hant.json b/web/public/static/langs/zh_Hant.json index c1b4de81..683f5a9f 100644 --- a/web/public/static/langs/zh_Hant.json +++ b/web/public/static/langs/zh_Hant.json @@ -1,201 +1,407 @@ { - "action_bar_logo_alt": "ntfy 標識", - "action_bar_unsubscribe": "取消訂閱", - "action_bar_toggle_mute": "通知靜音/解除通知靜音", - "action_bar_toggle_action_menu": "開啟/關閉操作選單", - "message_bar_type_message": "在這輸入訊息", - "alert_grant_description": "允許瀏覽器權限以顯示桌面通知。", - "alert_grant_button": "允許", - "notifications_list": "通知清單", - "notifications_list_item": "通知", - "notifications_mark_read": "標示已讀", - "notifications_attachment_image": "附加圖片", - "notifications_attachment_copy_url_title": "複製附件 URL 到剪貼簿", - "notifications_attachment_copy_url_button": "複製 URL", - "notifications_attachment_open_title": "前往 {{url}}", - "notifications_attachment_open_button": "開啟附件", - "notifications_attachment_link_expired": "下載連結已過期", - "notifications_attachment_file_video": "影片檔案", - "notifications_attachment_file_app": "Android 應用程式檔案", - "notifications_attachment_file_document": "其他文件", - "notifications_click_copy_url_title": "複製連結 URL 到剪貼板", - "notifications_click_copy_url_button": "複製連結", - "notifications_click_open_button": "開啟連結", - "notifications_actions_not_supported": "網頁程式無法支援該動作", - "notifications_actions_http_request_title": "傳送 HTTP {{method}} 到 {{url}}", - "notifications_none_for_topic_title": "尚未收到任何此主題的通知。", - "notifications_none_for_topic_description": "如要寄送通知到此主題,請使用 PUT 或 POST 到此主題URL。", - "notifications_none_for_any_title": "尚未收到任何通知。", - "action_bar_settings": "設定", - "action_bar_send_test_notification": "發送測試通知", + "account_basics_password_description": "更改你的帳戶密碼", + "account_basics_password_dialog_button_submit": "更改密碼", + "account_basics_password_dialog_confirm_password_label": "確認密碼", + "account_basics_password_dialog_current_password_incorrect": "密碼錯誤", + "account_basics_password_dialog_current_password_label": "當前密碼", + "account_basics_password_dialog_new_password_label": "新密碼", + "account_basics_password_dialog_title": "更改密碼", + "account_basics_password_title": "密碼", + "account_basics_phone_numbers_copied_to_clipboard": "電話號碼已複製到剪貼板", + "account_basics_phone_numbers_description": "電話通知", + "account_basics_phone_numbers_dialog_channel_call": "撥打", + "account_basics_phone_numbers_dialog_channel_sms": "短信", + "account_basics_phone_numbers_dialog_check_verification_button": "確認碼", + "account_basics_phone_numbers_dialog_code_label": "驗證碼", + "account_basics_phone_numbers_dialog_code_placeholder": "例如:123456", + "account_basics_phone_numbers_dialog_description": "要使用來電通知功能,你需要新增並驗證至少一個電話號碼。可以通過短信或電話驗證。", + "account_basics_phone_numbers_dialog_number_label": "電話號碼", + "account_basics_phone_numbers_dialog_number_placeholder": "例如:+1222333444", + "account_basics_phone_numbers_dialog_title": "新增電話號碼", + "account_basics_phone_numbers_dialog_verify_button_call": "撥打電話", + "account_basics_phone_numbers_dialog_verify_button_sms": "發送資訊", + "account_basics_phone_numbers_no_phone_numbers_yet": "無可執行的電話號碼", + "account_basics_phone_numbers_title": "電話號碼", + "account_basics_tier_admin_suffix_no_tier": "(無等級)", + "account_basics_tier_admin_suffix_with_tier": "(有 {{tier}} 等級)", + "account_basics_tier_admin": "管理員", + "account_basics_tier_basic": "基礎版", + "account_basics_tier_canceled_subscription": "你的訂閱已取消,並將在 {{date}} 降級為免費帳戶。", + "account_basics_tier_change_button": "改變", + "account_basics_tier_description": "你帳戶的權限級別", + "account_basics_tier_free": "免費", + "account_basics_tier_interval_monthly": "每月", + "account_basics_tier_interval_yearly": "每年", + "account_basics_tier_manage_billing_button": "管理計費", + "account_basics_tier_paid_until": "訂閱已支付至 {{date}},並將自動續訂", + "account_basics_tier_payment_overdue": "你的付款已逾期。請更新你的付款方式,否則你的帳戶將很快被降級。", + "account_basics_tier_title": "帳戶類型", + "account_basics_tier_upgrade_button": "升級到專業版", + "account_basics_title": "帳戶", + "account_basics_username_admin_tooltip": "你是管理員", + "account_basics_username_description": "嘿,那是你 ❤", + "account_basics_username_title": "用戶名", + "account_delete_description": "永久刪除你的帳戶", + "account_delete_dialog_billing_warning": "刪除你的帳戶也會立即取消你的計費訂閱。你將無法再訪問計費儀錶板。", + "account_delete_dialog_button_cancel": "取消", + "account_delete_dialog_button_submit": "永久刪除帳戶", + "account_delete_dialog_description": "這將永久刪除你的帳戶,包括存儲在伺服器上的所有數據。刪除後,你的用戶名將在 7 天內不可用。如果你真的想繼續,請在下面的框中使用你的密碼作確認。", + "account_delete_dialog_label": "密碼", + "account_delete_title": "刪除帳戶", + "account_tokens_delete_dialog_description": "在刪除訪問令牌之前,請確保沒有應用程序或腳本正在活躍使用它。 此操作無法撤銷。", + "account_tokens_delete_dialog_submit_button": "永久删除令牌", + "account_tokens_delete_dialog_title": "刪除訪問令牌", + "account_tokens_description": "通過 ntfy API 發布和訂閱時使用訪問令牌,因此你不必發送你的帳戶憑證。查看文檔以了解更多資訊。", + "account_tokens_dialog_button_cancel": "取消", + "account_tokens_dialog_button_create": "創建令牌", + "account_tokens_dialog_button_update": "更新令牌", + "account_tokens_dialog_expires_label": "訪問令牌過期於", + "account_tokens_dialog_expires_never": "令牌永不過期", + "account_tokens_dialog_expires_unchanged": "保持過期日期不變", + "account_tokens_dialog_expires_x_days": "令牌在 {{days}} 天後過期", + "account_tokens_dialog_expires_x_hours": "令牌在 {{hours}} 小時後過期", + "account_tokens_dialog_label": "標籤,例如:Radarr 通知", + "account_tokens_dialog_title_create": "創建訪問令牌", + "account_tokens_dialog_title_delete": "刪除訪問令牌", + "account_tokens_dialog_title_edit": "編輯訪問令牌", + "account_tokens_table_cannot_delete_or_edit": "無法編輯或刪除當前會話令牌", + "account_tokens_table_copied_to_clipboard": "已複製訪問令牌", + "account_tokens_table_create_token_button": "創建訪問令牌", + "account_tokens_table_current_session": "當前瀏覽器會話", + "account_tokens_table_expires_header": "過期", + "account_tokens_table_label_header": "標籤", + "account_tokens_table_last_access_header": "最後訪問", + "account_tokens_table_last_origin_tooltip": "於IP地址 {{ip}},點擊查找", + "account_tokens_table_never_expires": "永不過期", + "account_tokens_table_token_header": "令牌", + "account_tokens_title": "訪問令牌", + "account_upgrade_dialog_billing_contact_email": "有關賬單問題,請直接聯繫我們 。", + "account_upgrade_dialog_billing_contact_website": "有關賬單問題,請參考我們的網站 。", + "account_upgrade_dialog_button_cancel_subscription": "取消訂閱", + "account_upgrade_dialog_button_cancel": "取消", + "account_upgrade_dialog_button_pay_now": "立即付款並訂閱", + "account_upgrade_dialog_button_redirect_signup": "立即註冊", + "account_upgrade_dialog_button_update_subscription": "更新訂閱", + "account_upgrade_dialog_cancel_warning": "這將取消你的訂閱,並在 {{date}} 降級你的帳戶。在那一天,主題保留以及緩存在伺服器上的訊息將被刪除。", + "account_upgrade_dialog_interval_monthly": "每月", + "account_upgrade_dialog_interval_yearly_discount_save_up_to": "節省高達 {{discount}}%", + "account_upgrade_dialog_interval_yearly_discount_save": "節省 {{discount}}%", + "account_upgrade_dialog_interval_yearly": "每年", + "account_upgrade_dialog_proration_info": "按比例分配:在付費計劃之間升級時,差價將被立刻收取。在降級到較低級別時,餘額將被用於支付未來的賬單周期。", + "account_upgrade_dialog_reservations_warning_one": "所選等級允許的保留主題少於當前等級。在更改你的等級之前,請至少刪除 1 項保留。你可以在設置中刪除保留。", + "account_upgrade_dialog_reservations_warning_other": "所選等級允許的保留主題少於當前等級。在更改你的等級之前,請至少刪除 {{count}} 項保留。你可以在設置中刪除保留。", + "account_upgrade_dialog_tier_current_label": "當前", + "account_upgrade_dialog_tier_features_attachment_file_size": "每個文件 {{filesize}} ", + "account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} 總存儲空間", + "account_upgrade_dialog_tier_features_calls_one": "每日一通電話", + "account_upgrade_dialog_tier_features_calls_other": "每日{{calls}} 通電話", + "account_upgrade_dialog_tier_features_emails_one": "每日一封郵件", + "account_upgrade_dialog_tier_features_emails_other": "每日 {{emails}} 條郵件", + "account_upgrade_dialog_tier_features_messages_one": "每日一條訊息", + "account_upgrade_dialog_tier_features_messages_other": "每日 {{messages}} 條訊息", + "account_upgrade_dialog_tier_features_no_calls": "沒有電話", + "account_upgrade_dialog_tier_features_no_reservations": "無保留主題", + "account_upgrade_dialog_tier_features_reservations_one": "保留一條主題", + "account_upgrade_dialog_tier_features_reservations_other": "保留 {{reservations}} 條主題", + "account_upgrade_dialog_tier_price_billed_monthly": "{{price}} 每年。按月計費。", + "account_upgrade_dialog_tier_price_billed_yearly": "{{價格}} 按年計費。節省 {{save}}。", + "account_upgrade_dialog_tier_price_per_month": "月", + "account_upgrade_dialog_tier_selected_label": "已選", + "account_upgrade_dialog_title": "更改帳戶等級", + "account_usage_attachment_storage_description": "每個文件 {{filesize}},在 {{expiry}} 後刪除", + "account_usage_attachment_storage_title": "附件存儲", + "account_usage_basis_ip_description": "此帳戶的使用統計資訊和限制基於你的 IP 地址,因此可能會與其他用戶共享。上面顯示的限制是基於現有速率限制的近似值。", + "account_usage_calls_none": "此帳號無法撥打電話", + "account_usage_calls_title": "已撥打電話", + "account_usage_cannot_create_portal_session": "無法打開計費門戶", + "account_usage_emails_title": "已發送電子郵件", + "account_usage_limits_reset_daily": "使用限制每天午夜 (UTC) 重置", + "account_usage_messages_title": "已發布訊息", + "account_usage_of_limit": "{{limit}} 的", + "account_usage_reservations_none": "此帳戶沒有保留主題", + "account_usage_reservations_title": "保留主題", + "account_usage_title": "使用量", + "account_usage_unlimited": "無限", + "action_bar_account": "帳戶", + "action_bar_change_display_name": "更改顯示名稱", "action_bar_clear_notifications": "清除所有通知", + "action_bar_logo_alt": "ntfy 標識", + "action_bar_mute_notifications": "靜音", + "action_bar_profile_logout": "登出", + "action_bar_profile_settings": "設定", + "action_bar_profile_title": "個人資料", + "action_bar_reservation_add": "保留主題", + "action_bar_reservation_delete": "移除保留", + "action_bar_reservation_edit": "更改保留", + "action_bar_reservation_limit_reached": "達到限制", + "action_bar_send_test_notification": "發送測試通知", + "action_bar_settings": "設定", "action_bar_show_menu": "顯示選單", - "nav_button_documentation": "文件", - "nav_button_publish_message": "發佈通知", - "nav_button_muted": "通知已靜音", - "notifications_copied_to_clipboard": "已複製到剪貼簿", - "message_bar_publish": "發佈訊息", - "message_bar_show_dialog": "顯示發佈對話框", - "message_bar_error_publishing": "發佈通知時發生錯誤", - "nav_topics_title": "訂閱主題", - "nav_button_all_notifications": "所有通知", + "action_bar_sign_in": "登錄", + "action_bar_sign_up": "註冊", + "action_bar_toggle_action_menu": "開啟或關閉操作選單", + "action_bar_toggle_mute": "通知靜音/解除通知靜音", + "action_bar_unmute_notifications": "取消靜音", + "action_bar_unsubscribe": "取消訂閱", + "alert_notification_ios_install_required_description": "要接收通知,請在 iOS 上點擊共享,然後添加到主屏幕", + "alert_notification_ios_install_required_title": "需要安裝 iOS 應用程式", + "alert_notification_permission_denied_description": "你已禁用通知。要重新啟用通知,請在瀏覽器設置中啟用通知。", + "alert_notification_permission_denied_title": "已禁用通知", + "alert_notification_permission_required_button": "現在授予", + "alert_notification_permission_required_description": "授予瀏覽器顯示桌面通知的權限。", + "alert_notification_permission_required_title": "已禁用通知", + "alert_not_supported_context_description": "通知僅支援 HTTPS。這是 Notifications API 的限制。", + "alert_not_supported_description": "你的瀏覽器不支援通知。", + "alert_not_supported_title": "不支援通知", + "common_add": "新增", + "common_back": "返回", + "common_cancel": "取消", + "common_copy_to_clipboard": "複製到剪貼板", + "common_save": "保存", + "display_name_dialog_description": "為訂閱列表中顯示的主題設置一個替代名稱。這有助於更輕鬆地識別名稱複雜的主題。", + "display_name_dialog_placeholder": "顯示名稱", + "display_name_dialog_title": "更改顯示名稱", + "emoji_picker_search_clear": "清除搜索", + "emoji_picker_search_placeholder": "查找表情符號", + "error_boundary_button_copy_stack_trace": "複製堆疊追踪", + "error_boundary_button_reload_ntfy": "重新加載 ntfy", + "error_boundary_description": "這顯然不應該發生。對此非常抱歉。
如果你有時間,請在GitHub上報告,或通過DiscordMatrix告訴我們。", + "error_boundary_gathering_info": "收集更多資訊……", + "error_boundary_stack_trace": "堆疊追踪", + "error_boundary_title": "天啊,ntfy 崩潰了", + "error_boundary_unsupported_indexeddb_description": "Ntfy Web應用程式需要IndexedDB才能運行,且你的瀏覽器在隱私瀏覽模式下不支援IndexedDB。

儘管這很不幸,但在隱私瀏覽模式下使用ntfy Web應用程式也沒有多大意義,因為所有東西都存儲在瀏覽器存儲中。你可以在本GitHub問題中閱讀有關它的更多資訊,或者在DiscordMatrix上與我們交談。", + "error_boundary_unsupported_indexeddb_title": "不支援隱私瀏覽", + "login_disabled": "登錄已禁用", + "login_form_button_submit": "登錄", + "login_link_signup": "註冊", + "login_title": "請登錄你的 ntfy 帳戶", + "message_bar_error_publishing": "發佈通知時出錯", + "message_bar_publish": "發布訊息", + "message_bar_show_dialog": "顯示發布對話框", + "message_bar_type_message": "在此處輸入訊息", + "nav_button_account": "帳戶", + "nav_button_all_notifications": "全部通知", + "nav_button_connecting": "正在連接", + "nav_button_documentation": "文檔", + "nav_button_muted": "已暫停通知", + "nav_button_publish_message": "發布通知", "nav_button_settings": "設定", "nav_button_subscribe": "訂閱主題", - "nav_button_connecting": "連線中", - "alert_grant_title": "通知已關閉", - "alert_not_supported_title": "不支援通知", - "alert_not_supported_description": "瀏覽器不支援通知。", - "notifications_tags": "標籤", - "notifications_priority_x": "優先度 {{priority}}", - "notifications_new_indicator": "新通知", - "notifications_attachment_file_audio": "聲音檔案", - "notifications_delete": "刪除", + "nav_topics_title": "訂閱主題", + "nav_upgrade_banner_description": "保留主題,更多訊息和郵件,以及更大的附件", + "nav_upgrade_banner_label": "升級到 ntfy Pro", + "notifications_actions_failed_notification": "通知失敗", + "notifications_actions_http_request_title": "發送 HTTP {{method}} 到 {{url}}", + "notifications_actions_not_supported": "網頁應用程序不支援此操作", + "notifications_actions_open_url_title": "轉到 {{url}}", + "notifications_attachment_copy_url_button": "複製連結地址", + "notifications_attachment_copy_url_title": "將附件中連結地址複製到剪貼板", + "notifications_attachment_file_app": "安卓應用程式", + "notifications_attachment_file_audio": "聲音文件", + "notifications_attachment_file_document": "其他文件", + "notifications_attachment_file_image": "圖片文件", + "notifications_attachment_file_video": "影片文件", + "notifications_attachment_image": "附件圖片", + "notifications_attachment_link_expired": "下載連結已過期", "notifications_attachment_link_expires": "連結在 {{date}} 過期", - "notifications_attachment_file_image": "圖片檔案", - "notifications_actions_open_url_title": "前往 {{url}}", - "notifications_no_subscriptions_title": "你尚未有任何訂閱。", - "notifications_example": "範例", - "notifications_more_details": "你可以在 ntfy 網站或者技術文件中查看更多資訊。", - "notifications_loading": "載入中…", - "publish_dialog_title_topic": "發佈到 {{topic}}", - "publish_dialog_title_no_topic": "發佈通知", - "publish_dialog_progress_uploading": "上傳中…", - "publish_dialog_priority_label": "優先度", - "publish_dialog_email_label": "電郵地址", - "publish_dialog_filename_label": "檔案名稱", - "publish_dialog_button_cancel": "取消", - "publish_dialog_button_send": "傳送", - "publish_dialog_button_cancel_sending": "取消傳送", - "subscribe_dialog_subscribe_button_cancel": "取消", - "subscribe_dialog_subscribe_button_subscribe": "訂閱", - "emoji_picker_search_clear": "清除", - "subscribe_dialog_login_password_label": "密碼", - "subscribe_dialog_login_button_back": "返回", - "subscribe_dialog_login_button_login": "登入", + "notifications_attachment_open_button": "打開附件", + "notifications_attachment_open_title": "轉到 {{url}}", + "notifications_click_copy_url_button": "複製鏈結", + "notifications_click_copy_url_title": "複製鏈結地址到剪貼板", + "notifications_click_open_button": "打開鏈結", + "notifications_copied_to_clipboard": "複製到剪貼板", + "notifications_delete": "刪除", + "notifications_example": "示例", + "notifications_list_item": "通知", + "notifications_list": "通知列表", + "notifications_loading": "正在加載通知……", + "notifications_mark_read": "標記為已讀", + "notifications_more_details": "有關更多資訊,請查看網站文檔。", + "notifications_new_indicator": "新通知", + "notifications_none_for_any_description": "要向此主題發送通知,只需使用 PUT 或 POST 到主題鏈結即可。以下是使用你的主題的示例。", + "notifications_none_for_any_title": "你尚未收到任何通知。", + "notifications_none_for_topic_description": "要向此主題發送通知,只需使用 PUT 或 POST 到主題連結即可。", + "notifications_none_for_topic_title": "你尚未收到有關此主題的任何通知。", + "notifications_no_subscriptions_description": "點擊 \"{{linktext}}\" 連結以建立或訂閱主題。之後,你可以使用 PUT 或 POST 發送訊息,你將在這裡收到通知。", + "notifications_no_subscriptions_title": "看起來你還未有任何訂閱", + "notifications_priority_x": "優先級 {{priority}}", + "notifications_tags": "標記", + "prefs_appearance_language_title": "語言", + "prefs_appearance_theme_dark": "黑暗模式", + "prefs_appearance_theme_light": "光亮模式", + "prefs_appearance_theme_system": "系統 (預設)", + "prefs_appearance_theme_title": "主題", + "prefs_appearance_title": "外觀", + "prefs_notifications_delete_after_never_description": "永不自動刪除通知", "prefs_notifications_delete_after_never": "從不", + "prefs_notifications_delete_after_one_day_description": "一天後自動刪除通知", + "prefs_notifications_delete_after_one_day": "一天後", + "prefs_notifications_delete_after_one_month_description": "一個月後自動刪除通知", + "prefs_notifications_delete_after_one_month": "一個月後", + "prefs_notifications_delete_after_one_week_description": "一周後自動刪除通知", + "prefs_notifications_delete_after_one_week": "一周後", + "prefs_notifications_delete_after_three_hours_description": "三小時後自動刪除通知", + "prefs_notifications_delete_after_three_hours": "三小時後", + "prefs_notifications_delete_after_title": "刪除通知", + "prefs_notifications_min_priority_any": "任意優先級", + "prefs_notifications_min_priority_default_and_higher": "默認優先級或更高", + "prefs_notifications_min_priority_description_any": "顯示所有通知,無論優先級如何", + "prefs_notifications_min_priority_description_max": "僅顯示最高優先級的通知", + "prefs_notifications_min_priority_description_x_or_higher": "僅顯示優先級為{{number}}({{name}})或以上的通知", + "prefs_notifications_min_priority_high_and_higher": "高優先級或更高", + "prefs_notifications_min_priority_low_and_higher": "低優先級或更高", + "prefs_notifications_min_priority_max_only": "僅最高優先級", + "prefs_notifications_min_priority_title": "最低優先級", + "prefs_notifications_sound_description_none": "收到通知時不播放任何聲音", + "prefs_notifications_sound_description_some": "收到通知時播放 {{sound}} 聲音", + "prefs_notifications_sound_no_sound": "靜音", + "prefs_notifications_sound_play": "播放選中聲音", + "prefs_notifications_sound_title": "通知提示音", + "prefs_notifications_title": "通知", + "prefs_notifications_web_push_disabled_description": "當網頁程式在運行時將會收到通知 (透過 WebSocket)", + "prefs_notifications_web_push_disabled": "己暫用", + "prefs_notifications_web_push_enabled_description": "即使網頁程式未有運街亦會收到通知 (via Web Push)", + "prefs_notifications_web_push_enabled": "己為 {{server}} 啟用", + "prefs_notifications_web_push_title": "背景通知", + "prefs_reservations_add_button": "新增保留主題", + "prefs_reservations_delete_button": "重置主題訪問", + "prefs_reservations_description": "你可以在此處保留主題名稱供個人使用。保留主題使你擁有該主題的所有權,並允許你為其他用戶定義對該主題的訪問權限。", + "prefs_reservations_dialog_access_label": "訪問", + "prefs_reservations_dialog_description": "保留主題使你擁有該主題的所有權,並允許你為其他用戶定義對該主題的訪問權限。", + "prefs_reservations_dialog_title_add": "保留主題", + "prefs_reservations_dialog_title_delete": "刪除主題保留", + "prefs_reservations_dialog_title_edit": "編輯保留主題", + "prefs_reservations_dialog_topic_label": "主題", + "prefs_reservations_edit_button": "編輯主題訪問", + "prefs_reservations_limit_reached": "你已達到保留主題限制。", + "prefs_reservations_table_access_header": "訪問", + "prefs_reservations_table_click_to_subscribe": "點擊以訂閱", + "prefs_reservations_table_everyone_deny_all": "只有我可以發佈和訂閱", + "prefs_reservations_table_everyone_read_only": "我可以發佈和訂閱,每個人都可以訂閱", + "prefs_reservations_table_everyone_read_write": "每個人都可以發佈和訂閱", + "prefs_reservations_table_everyone_write_only": "我可以發佈和訂閱,每個人都可以發佈", + "prefs_reservations_table_not_subscribed": "未訂閱", + "prefs_reservations_table_topic_header": "主題", + "prefs_reservations_table": "保留主題表格", + "prefs_reservations_title": "保留主題", "prefs_users_add_button": "新增使用者", + "prefs_users_delete_button": "刪除用戶", + "prefs_users_description_no_sync": "用戶和密碼不會同步到你的賬戶。", + "prefs_users_description": "在此處新增/刪除受保護主題的使用者。請注意,使用者名和密碼將存儲在瀏覽器的本地存儲中。", + "prefs_users_dialog_base_url_label": "服務連結地址,例如 https://ntfy.sh", "prefs_users_dialog_password_label": "密碼", "prefs_users_dialog_title_add": "新增使用者", - "common_save": "儲存", - "common_cancel": "取消", - "error_boundary_title": "歐買尬,ntfy 壞掉了", - "notifications_none_for_any_description": "要開始發送通知到一個主題,只需要對主題 URL 發送 HTTP PUT 或者 POST,例如:", - "notifications_no_subscriptions_description": "點選 「{{linktext}}」 連結以建立或訂閱主題。完成後,你就可以使用 HTTP PUT 或者 POST 發送通知到這裡了!", - "error_boundary_description": "很抱歉 ntfy 發生錯誤了。
如果你有時間,煩請到 Github 回報錯誤,或者到 Discord 或者 Matrix 聊天室裡面告訴我們。", - "publish_dialog_tags_placeholder": "逗號分隔的標籤,例如 e.g. warning, srv1-backup", - "publish_dialog_click_label": "點擊網址", - "publish_dialog_attach_placeholder": "從網址新增附件,例如 https://f-droid.org/F-Droid.apk", - "publish_dialog_attach_reset": "移除附件網址", - "publish_dialog_attach_label": "附件網址", - "publish_dialog_delay_reset": "移除延遲傳送", - "publish_dialog_delay_label": "延遲", - "publish_dialog_other_features": "其他功能:", - "publish_dialog_filename_placeholder": "附件檔案名稱", - "publish_dialog_delay_placeholder": "延遲傳送,例如 {{unixTimestamp}}, {{relativeTime}} 或 \"{{naturalLanguage}}\" (僅限英文)", - "publish_dialog_chip_click_label": "點擊網址", - "publish_dialog_chip_email_label": "轉發到電郵", - "publish_dialog_chip_attach_url_label": "從網址新增附件", - "emoji_picker_search_placeholder": "搜尋 emoji", - "subscribe_dialog_subscribe_title": "訂閱主題", - "subscribe_dialog_error_user_not_authorized": "用戶 {{username}} 沒有權限", - "subscribe_dialog_error_user_anonymous": "匿名", - "login_title": "登入 ntfy 帳戶", - "action_bar_reservation_add": "保留主題", - "action_bar_profile_logout": "登出", - "alert_not_supported_context_description": "訊息只支援 HTTPS. 這是受 Notifications API 的限制", - "publish_dialog_base_url_placeholder": "服務網址,例如 https://example.com", - "signup_title": "創建 ntfy 賬戶", - "signup_form_username": "用戶名稱", - "signup_form_password": "密碼", - "signup_form_button_submit": "註冊", - "signup_form_toggle_password_visibility": "顯示/隱藏密碼", - "signup_disabled": "註冊已停止", - "signup_error_username_taken": "用戶名稱 {{username}} 已被取用", - "signup_error_creation_limit_reached": "註冊賬戶限制", - "login_form_button_submit": "登入", - "login_link_signup": "註冊", - "signup_already_have_account": "已有帳戶? 立即登入!", - "login_disabled": "登入已停止", - "action_bar_account": "帳戶", - "action_bar_change_display_name": "改變顯示名稱", - "action_bar_reservation_edit": "改變已保留", - "action_bar_reservation_delete": "移除保留", - "action_bar_reservation_limit_reached": "達到限制", - "action_bar_profile_title": "簡介", - "action_bar_profile_settings": "設置", - "action_bar_sign_in": "登入", - "action_bar_sign_up": "註冊", - "nav_button_account": "帳戶", - "nav_upgrade_banner_label": "升級到 ntfy 專業版", - "nav_upgrade_banner_description": "保留主題,更多信息電郵及附件", - "display_name_dialog_title": "改變顯示名稱", - "display_name_dialog_description": "為主題新增在訂閱清單顯示的第二名稱, 這會令尋找複雜主題時更方便。", - "display_name_dialog_placeholder": "顯示名稱", - "reserve_dialog_checkbox_label": "保留主題及設置權限", - "publish_dialog_progress_uploading_detail": "上載中 {{loaded}}/{{total}} ({{percent}}%) …", - "publish_dialog_message_published": "已公佈通訊", - "publish_dialog_attachment_limits_file_reached": "超出檔案限制 {fileSizeLimit}}", - "publish_dialog_attachment_limits_quota_reached": "超出限制, 尚餘 {{remainingBytes}}", - "publish_dialog_emoji_picker_show": "選擇 emoji", - "publish_dialog_priority_min": "最低優先", - "publish_dialog_priority_low": "較低優先", - "publish_dialog_priority_default": "正常優先", - "publish_dialog_priority_high": "高度優先", - "publish_dialog_priority_max": "最高優先", - "publish_dialog_base_url_label": "服務網址", + "prefs_users_dialog_title_edit": "編輯使用者", + "prefs_users_dialog_username_label": "使用者名,例如 phil", + "prefs_users_edit_button": "編輯用戶", + "prefs_users_table_base_url_header": "服務連結地址", + "prefs_users_table_cannot_delete_or_edit": "無法刪除或編輯已登錄用戶", + "prefs_users_table_user_header": "用戶", + "prefs_users_table": "用戶表", + "prefs_users_title": "管理使用者", + "priority_default": "預設", + "priority_high": "高", + "priority_low": "低", + "priority_max": "最高", + "priority_min": "最低", + "publish_dialog_attached_file_filename_placeholder": "附件文件名", + "publish_dialog_attached_file_remove": "刪除附件文件", + "publish_dialog_attached_file_title": "附件文件:", + "publish_dialog_attach_label": "附件連結地址", + "publish_dialog_attachment_limits_file_and_quota_reached": "超過 {{fileSizeLimit}} 文件限制和配額,剩餘 {{remainingBytes}}", + "publish_dialog_attachment_limits_file_reached": "超過 {{fileSizeLimit}} 文件限制", + "publish_dialog_attachment_limits_quota_reached": "超過配額,剩餘 {{remainingBytes}}", + "publish_dialog_attach_placeholder": "使用鏈結地址附加文件,例如 https://f-droid.org/F-Droid.apk", + "publish_dialog_attach_reset": "移除附件鏈結地址", + "publish_dialog_base_url_label": "服務鏈結地址", + "publish_dialog_base_url_placeholder": "服務鏈結地址,例如 https://example.com", + "publish_dialog_button_cancel_sending": "取消發送", + "publish_dialog_button_cancel": "取消", + "publish_dialog_button_send": "發送", + "publish_dialog_call_item": "撥打電話 {{number}}", + "publish_dialog_call_label": "撥號", + "publish_dialog_call_reset": "清空撥號", + "publish_dialog_checkbox_markdown": "格式化為 Markdown", + "publish_dialog_checkbox_publish_another": "發布另一個", + "publish_dialog_chip_attach_file_label": "本地文件附件", + "publish_dialog_chip_attach_url_label": "鏈結附件地址", + "publish_dialog_chip_call_label": "撥號", + "publish_dialog_chip_call_no_verified_numbers_tooltip": "未驗證的電話號碼", + "publish_dialog_chip_click_label": "點擊鏈結地址", + "publish_dialog_chip_delay_label": "延期投遞", + "publish_dialog_chip_email_label": "轉發郵件", + "publish_dialog_chip_topic_label": "變更主題", + "publish_dialog_click_label": "點擊鏈結地址", + "publish_dialog_click_placeholder": "點擊通知時打開鏈結地址", + "publish_dialog_click_reset": "移除點擊連結地址", + "publish_dialog_delay_label": "延期", + "publish_dialog_delay_placeholder": "延期投遞,例如 {{unixTimestamp}}、{{relativeTime}}或「{{naturalLanguage}}」(僅限英語)", + "publish_dialog_delay_reset": "刪除延期投遞", + "publish_dialog_details_examples_description": "有關所有發送功能的範例和詳細說明,請參閱文檔。", + "publish_dialog_drop_file_here": "將文件拖拽至此", + "publish_dialog_email_label": "電子郵件", + "publish_dialog_email_placeholder": "將通知轉發到的地址,例如 phil@example.com", + "publish_dialog_email_reset": "移除電子郵件轉發", + "publish_dialog_emoji_picker_show": "選擇表情符號", + "publish_dialog_filename_label": "文件名", + "publish_dialog_filename_placeholder": "附件文件名", + "publish_dialog_message_label": "訊息", + "publish_dialog_message_placeholder": "在此輸入訊息", + "publish_dialog_message_published": "已發布通知", + "publish_dialog_other_features": "其它功能:", + "publish_dialog_priority_default": "默認優先級", + "publish_dialog_priority_high": "高優先級", + "publish_dialog_priority_label": "優先級", + "publish_dialog_priority_low": "低優先級", + "publish_dialog_priority_max": "最高優先級", + "publish_dialog_priority_min": "最低優先級", + "publish_dialog_progress_uploading_detail": "正在上傳 {{loaded}}/{{total}} ({{percent}}%) ……", + "publish_dialog_progress_uploading": "正在上傳……", + "publish_dialog_tags_label": "標記", + "publish_dialog_tags_placeholder": "英文逗號分隔標記列表,例如 warning, srv1-backup", + "publish_dialog_title_label": "主題", + "publish_dialog_title_no_topic": "發布通知", + "publish_dialog_title_placeholder": "主題標題,例如 磁碟空間警告", + "publish_dialog_title_topic": "發布到 {{topic}}", "publish_dialog_topic_label": "主題名稱", "publish_dialog_topic_placeholder": "主題名稱,例如 phil_alerts", "publish_dialog_topic_reset": "重置主題", - "publish_dialog_title_label": "標題", - "publish_dialog_title_placeholder": "通訊標題,例如 Disk space alert", - "publish_dialog_message_label": "訊息", - "publish_dialog_message_placeholder": "這裏輸入訊息", - "publish_dialog_tags_label": "標籤", - "publish_dialog_click_placeholder": "通訊被點擊時到訪的網址", - "publish_dialog_click_reset": "移除點擊網址", - "publish_dialog_email_reset": "移除電郵轉發", - "publish_dialog_chip_attach_file_label": "上載檔案", - "publish_dialog_chip_delay_label": "延遲傳送", - "publish_dialog_chip_topic_label": "更變主題", - "publish_dialog_details_examples_description": "可以在 documentation 找到詳細的功能說明及例子。", - "publish_dialog_checkbox_publish_another": "公佈更多", - "publish_dialog_attached_file_title": "附件:", - "publish_dialog_attached_file_filename_placeholder": "附件名稱", - "subscribe_dialog_subscribe_use_another_label": "使用另一個伺服器", - "subscribe_dialog_subscribe_base_url_label": "服務網址", + "reservation_delete_dialog_action_delete_description": "緩存的郵件和附件將被永久刪除。此操作無法撤銷。", + "reservation_delete_dialog_action_delete_title": "刪除緩存的郵件和附件", + "reservation_delete_dialog_action_keep_description": "緩存在伺服器上的訊息和附件將對知道主題名稱的人公開可見。", + "reservation_delete_dialog_action_keep_title": "保留緩存的郵件和附件", + "reservation_delete_dialog_description": "刪除保留會放棄對該主題的所有權,並允許其他人保留它。你可以保留或刪除現有郵件和附件。", + "reservation_delete_dialog_submit_button": "刪除保留", + "reserve_dialog_checkbox_label": "保留主題並配置訪問", + "signup_already_have_account": "已有帳戶?登錄!", + "signup_disabled": "註冊已禁用", + "signup_error_creation_limit_reached": "已達到帳戶創建限制", + "signup_error_username_taken": "用戶名 {{username}} 已被取用", + "signup_form_button_submit": "註冊", + "signup_form_confirm_password": "確認密碼", + "signup_form_password": "密碼", + "signup_form_toggle_password_visibility": "切換密碼可見性", + "signup_form_username": "用戶名", + "signup_title": "創建一個 ntfy 帳戶", + "subscribe_dialog_error_topic_already_reserved": "主題已保留", + "subscribe_dialog_error_user_anonymous": "匿名", + "subscribe_dialog_error_user_not_authorized": "未授權 {{username}} 使用者", + "subscribe_dialog_login_button_login": "登入", + "subscribe_dialog_login_description": "本主題受密碼保護,請輸入用戶名和密碼以訂閱。", + "subscribe_dialog_login_password_label": "密碼", + "subscribe_dialog_login_title": "請登錄", + "subscribe_dialog_login_username_label": "用戶名,例如 phil", + "subscribe_dialog_subscribe_base_url_label": "服務地址地址", + "subscribe_dialog_subscribe_button_cancel": "取消", "subscribe_dialog_subscribe_button_generate_topic_name": "生成名稱", - "subscribe_dialog_login_title": "需要登入", - "subscribe_dialog_login_username_label": "用戶名稱,例如 phil", - "subscribe_dialog_error_topic_already_reserved": "主題已被保留", - "account_basics_title": "帳戶", - "account_basics_username_title": "用戶名稱", - "account_basics_username_description": "這就是你了❤", - "account_basics_username_admin_tooltip": "你是管理員", - "account_basics_password_title": "密碼", - "account_basics_password_description": "更變你的密碼", - "account_basics_password_dialog_title": "更變密碼", - "account_basics_password_dialog_new_password_label": "新的密碼", - "account_basics_password_dialog_confirm_password_label": "確認密碼", - "account_basics_password_dialog_button_submit": "更變密碼", - "account_usage_unlimited": "無限制", - "account_usage_title": "已經使用", - "account_usage_limits_reset_daily": "使用限制每天午夜重置", - "account_basics_tier_title": "帳戶類型", - "account_basics_tier_description": "你的能量值", - "account_basics_tier_admin": "管理員", - "account_basics_tier_admin_suffix_with_tier": "(擁有 {{tier}})", - "account_basics_tier_admin_suffix_no_tier": "(無層)", - "account_basics_tier_basic": "基礎", - "account_basics_tier_free": "免費", - "account_basics_tier_upgrade_button": "升級至專業版", - "publish_dialog_email_placeholder": "轉發到電郵,例如 phil@example.com", - "subscribe_dialog_subscribe_topic_placeholder": "主題名稱,例如 phil_alerts", - "publish_dialog_attached_file_remove": "移除附件", - "subscribe_dialog_subscribe_description": "主題可能不受到密碼保護, 所以盡量選擇一個不會容易被猜中的主題名稱。 一旦已訂閱,你能夠 PUT/POST 通訊。", - "subscribe_dialog_login_description": "這個主題受密碼保護,請輸入用戶名稱及密碼以訂閱主題。", - "account_basics_password_dialog_current_password_label": "現在的密碼", - "account_basics_password_dialog_current_password_incorrect": "密碼不正確", - "account_basics_tier_change_button": "更變", - "common_add": "新增", - "signup_form_confirm_password": "確認密碼" + "subscribe_dialog_subscribe_button_subscribe": "訂閱", + "subscribe_dialog_subscribe_description": "主題可能不受密碼保護,因此請選擇一個不容易被猜中的名字。訂閱後,你可以使用 PUT/POST 通知。", + "subscribe_dialog_subscribe_title": "訂閱主題", + "subscribe_dialog_subscribe_topic_placeholder": "主題名,例如 phil_alerts", + "subscribe_dialog_subscribe_use_another_background_info": "當網頁程式未開啟, 將不會收到來自其他伺服器的通知", + "subscribe_dialog_subscribe_use_another_label": "使用其他伺服器", + "web_push_subscription_expiring_body": "開啟ntfy以繼續接收通知", + "web_push_subscription_expiring_title": "通知會被暫停", + "web_push_unknown_notification_body": "你可能需要開啟網頁來更新ntfy", + "web_push_unknown_notification_title": "接收到不明通知" } diff --git a/web/public/sw.js b/web/public/sw.js new file mode 100644 index 00000000..56d66f16 --- /dev/null +++ b/web/public/sw.js @@ -0,0 +1,262 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from "workbox-precaching"; +import { NavigationRoute, registerRoute } from "workbox-routing"; +import { NetworkFirst } from "workbox-strategies"; +import { clientsClaim } from "workbox-core"; + +import { dbAsync } from "../src/app/db"; + +import { toNotificationParams, icon, badge } from "../src/app/notificationUtils"; +import initI18n from "../src/app/i18n"; + +/** + * General docs for service workers and PWAs: + * https://vite-pwa-org.netlify.app/guide/ + * https://developer.chrome.com/docs/workbox/ + * + * This file uses the (event) => event.waitUntil() pattern. + * This is because the event handler itself cannot be async, but + * the service worker needs to stay active while the promise completes. + */ + +const broadcastChannel = new BroadcastChannel("web-push-broadcast"); + +const addNotification = async ({ subscriptionId, message }) => { + const db = await dbAsync(); + + await db.notifications.add({ + ...message, + subscriptionId, + // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation + new: 1, + }); + + await db.subscriptions.update(subscriptionId, { + last: message.id, + }); + + const badgeCount = await db.notifications.where({ new: 1 }).count(); + console.log("[ServiceWorker] Setting new app badge count", { badgeCount }); + self.navigator.setAppBadge?.(badgeCount); +}; + +/** + * Handle a received web push message and show notification. + * + * Since the service worker cannot play a sound, we send a broadcast to the web app, which (if it is running) + * receives the broadcast and plays a sound (see web/src/app/WebPush.js). + */ +const handlePushMessage = async (data) => { + const { subscription_id: subscriptionId, message } = data; + + broadcastChannel.postMessage(message); // To potentially play sound + + await addNotification({ subscriptionId, message }); + await self.registration.showNotification( + ...toNotificationParams({ + subscriptionId, + message, + defaultTitle: message.topic, + topicRoute: new URL(message.topic, self.location.origin).toString(), + }) + ); +}; + +/** + * Handle a received web push subscription expiring. + */ +const handlePushSubscriptionExpiring = async (data) => { + const t = await initI18n(); + + await self.registration.showNotification(t("web_push_subscription_expiring_title"), { + body: t("web_push_subscription_expiring_body"), + icon, + data, + badge, + }); +}; + +/** + * Handle unknown push message. We can't ignore the push, since + * permission can be revoked by the browser. + */ +const handlePushUnknown = async (data) => { + const t = await initI18n(); + + await self.registration.showNotification(t("web_push_unknown_notification_title"), { + body: t("web_push_unknown_notification_body"), + icon, + data, + badge, + }); +}; + +/** + * Handle a received web push notification + * @param {object} data see server/types.go, type webPushPayload + */ +const handlePush = async (data) => { + if (data.event === "message") { + await handlePushMessage(data); + } else if (data.event === "subscription_expiring") { + await handlePushSubscriptionExpiring(data); + } else { + await handlePushUnknown(data); + } +}; + +/** + * Handle a user clicking on the displayed notification from `showNotification`. + * This is also called when the user clicks on an action button. + */ +const handleClick = async (event) => { + const t = await initI18n(); + + const clients = await self.clients.matchAll({ type: "window" }); + + const rootUrl = new URL(self.location.origin); + const rootClient = clients.find((client) => client.url === rootUrl.toString()); + // perhaps open on another topic + const fallbackClient = clients[0]; + + if (!event.notification.data?.message) { + // e.g. something other than a message, e.g. a subscription_expiring event + // simply open the web app on the root route (/) + if (rootClient) { + rootClient.focus(); + } else if (fallbackClient) { + fallbackClient.focus(); + fallbackClient.navigate(rootUrl.toString()); + } else { + self.clients.openWindow(rootUrl); + } + event.notification.close(); + } else { + const { message, topicRoute } = event.notification.data; + + if (event.action) { + const action = event.notification.data.message.actions.find(({ label }) => event.action === label); + + if (action.action === "view") { + self.clients.openWindow(action.url); + } else if (action.action === "http") { + try { + const response = await fetch(action.url, { + method: action.method ?? "POST", + headers: action.headers ?? {}, + body: action.body, + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status} ${response.statusText}`); + } + } catch (e) { + console.error("[ServiceWorker] Error performing http action", e); + self.registration.showNotification(`${t("notifications_actions_failed_notification")}: ${action.label} (${action.action})`, { + body: e.message, + icon, + badge, + }); + } + } + + if (action.clear) { + event.notification.close(); + } + } else if (message.click) { + self.clients.openWindow(message.click); + + event.notification.close(); + } else { + // If no action was clicked, and the message doesn't have a click url: + // - first try focus an open tab on the `/:topic` route + // - if not, use an open tab on the root route (`/`) and navigate to the topic + // - if not, use whichever tab we have open and navigate to the topic + // - finally, open a new tab focused on the topic + + const topicClient = clients.find((client) => client.url === topicRoute); + + if (topicClient) { + topicClient.focus(); + } else if (rootClient) { + rootClient.focus(); + rootClient.navigate(topicRoute); + } else if (fallbackClient) { + fallbackClient.focus(); + fallbackClient.navigate(topicRoute); + } else { + self.clients.openWindow(topicRoute); + } + + event.notification.close(); + } + } +}; + +self.addEventListener("install", () => { + console.log("[ServiceWorker] Installed"); + self.skipWaiting(); +}); + +self.addEventListener("activate", () => { + console.log("[ServiceWorker] Activated"); + self.skipWaiting(); +}); + +// There's no good way to test this, and Chrome doesn't seem to implement this, +// so leaving it for now +self.addEventListener("pushsubscriptionchange", (event) => { + console.log("[ServiceWorker] PushSubscriptionChange"); + console.log(event); +}); + +self.addEventListener("push", (event) => { + const data = event.data.json(); + console.log("[ServiceWorker] Received Web Push Event", { event, data }); + event.waitUntil(handlePush(data)); +}); + +self.addEventListener("notificationclick", (event) => { + console.log("[ServiceWorker] NotificationClick"); + event.waitUntil(handleClick(event)); +}); + +// See https://vite-pwa-org.netlify.app/guide/inject-manifest.html#service-worker-code +// self.__WB_MANIFEST is the workbox injection point that injects the manifest of the +// vite dist files and their revision ids, for example: +// [{"revision":"aaabbbcccdddeeefff12345","url":"/index.html"},...] +precacheAndRoute( + // eslint-disable-next-line no-underscore-dangle + self.__WB_MANIFEST +); + +// Claim all open windows +clientsClaim(); +// Delete any cached old dist files from previous service worker versions +cleanupOutdatedCaches(); + +if (!import.meta.env.DEV) { + // we need the app_root setting, so we import the config.js file from the go server + // this does NOT include the same base_url as the web app running in a window, + // since we don't have access to `window` like in `src/app/config.js` + self.importScripts("/config.js"); + + // this is the fallback single-page-app route, matching vite.config.js PWA config, + // and is served by the go web server. It is needed for the single-page-app to work. + // https://developer.chrome.com/docs/workbox/modules/workbox-routing/#how-to-register-a-navigation-route + registerRoute( + new NavigationRoute(createHandlerBoundToURL("/app.html"), { + allowlist: [ + // the app root itself, could be /, or not + new RegExp(`^${config.app_root}$`), + ], + }) + ); + + // the manifest excludes config.js (see vite.config.js) since the dist-file differs from the + // actual config served by the go server. this adds it back with `NetworkFirst`, so that the + // most recent config from the go server is cached, but the app still works if the network + // is unavailable. this is important since there's no "refresh" button in the installed pwa + // to force a reload. + registerRoute(({ url }) => url.pathname === "/config.js", new NetworkFirst()); +} diff --git a/web/src/app/AccountApi.js b/web/src/app/AccountApi.js index 243286b4..d9380438 100644 --- a/web/src/app/AccountApi.js +++ b/web/src/app/AccountApi.js @@ -1,389 +1,434 @@ +import i18n from "i18next"; import { - accountBillingPortalUrl, - accountBillingSubscriptionUrl, - accountPasswordUrl, - accountReservationSingleUrl, - accountReservationUrl, - accountSettingsUrl, - accountSubscriptionSingleUrl, - accountSubscriptionUrl, - accountTokenUrl, - accountUrl, maybeWithBearerAuth, - tiersUrl, - withBasicAuth, - withBearerAuth + accountBillingPortalUrl, + accountBillingSubscriptionUrl, + accountPasswordUrl, + accountPhoneUrl, + accountPhoneVerifyUrl, + accountReservationSingleUrl, + accountReservationUrl, + accountSettingsUrl, + accountSubscriptionUrl, + accountTokenUrl, + accountUrl, + maybeWithBearerAuth, + tiersUrl, + withBasicAuth, + withBearerAuth, } from "./utils"; import session from "./Session"; import subscriptionManager from "./SubscriptionManager"; -import i18n from "i18next"; import prefs from "./Prefs"; import routes from "../components/routes"; -import {fetchOrThrow, throwAppError, UnauthorizedError} from "./errors"; +import { fetchOrThrow, UnauthorizedError } from "./errors"; const delayMillis = 45000; // 45 seconds const intervalMillis = 900000; // 15 minutes class AccountApi { - constructor() { - this.timer = null; - this.listener = null; // Fired when account is fetched from remote - this.tiers = null; // Cached - } + constructor() { + this.timer = null; + this.listener = null; // Fired when account is fetched from remote + this.tiers = null; // Cached + } - registerListener(listener) { - this.listener = listener; - } + registerListener(listener) { + this.listener = listener; + } - resetListener() { - this.listener = null; - } + resetListener() { + this.listener = null; + } - async login(user) { - const url = accountTokenUrl(config.base_url); - console.log(`[AccountApi] Checking auth for ${url}`); - const response = await fetchOrThrow(url, { - method: "POST", - headers: withBasicAuth({}, user.username, user.password) - }); - const json = await response.json(); // May throw SyntaxError - if (!json.token) { - throw new Error(`Unexpected server response: Cannot find token`); + async login(user) { + const url = accountTokenUrl(config.base_url); + console.log(`[AccountApi] Checking auth for ${url}`); + const response = await fetchOrThrow(url, { + method: "POST", + headers: withBasicAuth({}, user.username, user.password), + }); + const json = await response.json(); // May throw SyntaxError + if (!json.token) { + throw new Error(`Unexpected server response: Cannot find token`); + } + return json.token; + } + + async logout() { + const url = accountTokenUrl(config.base_url); + console.log(`[AccountApi] Logging out from ${url} using token ${session.token()}`); + await fetchOrThrow(url, { + method: "DELETE", + headers: withBearerAuth({}, session.token()), + }); + } + + async create(username, password) { + const url = accountUrl(config.base_url); + const body = JSON.stringify({ + username, + password, + }); + console.log(`[AccountApi] Creating user account ${url}`); + await fetchOrThrow(url, { + method: "POST", + body, + }); + } + + async get() { + const url = accountUrl(config.base_url); + console.log(`[AccountApi] Fetching user account ${url}`); + const response = await fetchOrThrow(url, { + headers: maybeWithBearerAuth({}, session.token()), // GET /v1/account endpoint can be called by anonymous + }); + const account = await response.json(); // May throw SyntaxError + console.log(`[AccountApi] Account`, account); + if (this.listener) { + this.listener(account); + } + return account; + } + + async delete(password) { + const url = accountUrl(config.base_url); + console.log(`[AccountApi] Deleting user account ${url}`); + await fetchOrThrow(url, { + method: "DELETE", + headers: withBearerAuth({}, session.token()), + body: JSON.stringify({ + password, + }), + }); + } + + async changePassword(currentPassword, newPassword) { + const url = accountPasswordUrl(config.base_url); + console.log(`[AccountApi] Changing account password ${url}`); + await fetchOrThrow(url, { + method: "POST", + headers: withBearerAuth({}, session.token()), + body: JSON.stringify({ + password: currentPassword, + new_password: newPassword, + }), + }); + } + + async createToken(label, expires) { + const url = accountTokenUrl(config.base_url); + const body = { + label, + expires: expires > 0 ? Math.floor(Date.now() / 1000) + expires : 0, + }; + console.log(`[AccountApi] Creating user access token ${url}`); + await fetchOrThrow(url, { + method: "POST", + headers: withBearerAuth({}, session.token()), + body: JSON.stringify(body), + }); + } + + async updateToken(token, label, expires) { + const url = accountTokenUrl(config.base_url); + const body = { + token, + label, + }; + if (expires > 0) { + body.expires = Math.floor(Date.now() / 1000) + expires; + } + console.log(`[AccountApi] Creating user access token ${url}`); + await fetchOrThrow(url, { + method: "PATCH", + headers: withBearerAuth({}, session.token()), + body: JSON.stringify(body), + }); + } + + async extendToken() { + const url = accountTokenUrl(config.base_url); + console.log(`[AccountApi] Extending user access token ${url}`); + await fetchOrThrow(url, { + method: "PATCH", + headers: withBearerAuth({}, session.token()), + }); + } + + async deleteToken(token) { + const url = accountTokenUrl(config.base_url); + console.log(`[AccountApi] Deleting user access token ${url}`); + await fetchOrThrow(url, { + method: "DELETE", + headers: withBearerAuth({ "X-Token": token }, session.token()), + }); + } + + async updateSettings(payload) { + const url = accountSettingsUrl(config.base_url); + const body = JSON.stringify(payload); + console.log(`[AccountApi] Updating user account ${url}: ${body}`); + await fetchOrThrow(url, { + method: "PATCH", + headers: withBearerAuth({}, session.token()), + body, + }); + } + + async addSubscription(baseUrl, topic) { + const url = accountSubscriptionUrl(config.base_url); + const body = JSON.stringify({ + base_url: baseUrl, + topic, + }); + console.log(`[AccountApi] Adding user subscription ${url}: ${body}`); + const response = await fetchOrThrow(url, { + method: "POST", + headers: withBearerAuth({}, session.token()), + body, + }); + const subscription = await response.json(); // May throw SyntaxError + console.log(`[AccountApi] Subscription`, subscription); + return subscription; + } + + async updateSubscription(baseUrl, topic, payload) { + const url = accountSubscriptionUrl(config.base_url); + const body = JSON.stringify({ + base_url: baseUrl, + topic, + ...payload, + }); + console.log(`[AccountApi] Updating user subscription ${url}: ${body}`); + const response = await fetchOrThrow(url, { + method: "PATCH", + headers: withBearerAuth({}, session.token()), + body, + }); + const subscription = await response.json(); // May throw SyntaxError + console.log(`[AccountApi] Subscription`, subscription); + return subscription; + } + + async deleteSubscription(baseUrl, topic) { + const url = accountSubscriptionUrl(config.base_url); + console.log(`[AccountApi] Removing user subscription ${url}`); + const headers = { + "X-BaseURL": baseUrl, + "X-Topic": topic, + }; + await fetchOrThrow(url, { + method: "DELETE", + headers: withBearerAuth(headers, session.token()), + }); + } + + async upsertReservation(topic, everyone) { + const url = accountReservationUrl(config.base_url); + console.log(`[AccountApi] Upserting user access to topic ${topic}, everyone=${everyone}`); + await fetchOrThrow(url, { + method: "POST", + headers: withBearerAuth({}, session.token()), + body: JSON.stringify({ + topic, + everyone, + }), + }); + } + + async deleteReservation(topic, deleteMessages) { + const url = accountReservationSingleUrl(config.base_url, topic); + console.log(`[AccountApi] Removing topic reservation ${url}`); + const headers = { + "X-Delete-Messages": deleteMessages ? "true" : "false", + }; + await fetchOrThrow(url, { + method: "DELETE", + headers: withBearerAuth(headers, session.token()), + }); + } + + async billingTiers() { + if (this.tiers) { + return this.tiers; + } + const url = tiersUrl(config.base_url); + console.log(`[AccountApi] Fetching billing tiers`); + const response = await fetchOrThrow(url); // No auth needed! + this.tiers = await response.json(); // May throw SyntaxError + return this.tiers; + } + + async createBillingSubscription(tier, interval) { + console.log(`[AccountApi] Creating billing subscription with ${tier} and interval ${interval}`); + return this.upsertBillingSubscription("POST", tier, interval); + } + + async updateBillingSubscription(tier, interval) { + console.log(`[AccountApi] Updating billing subscription with ${tier} and interval ${interval}`); + return this.upsertBillingSubscription("PUT", tier, interval); + } + + async upsertBillingSubscription(method, tier, interval) { + const url = accountBillingSubscriptionUrl(config.base_url); + const response = await fetchOrThrow(url, { + method, + headers: withBearerAuth({}, session.token()), + body: JSON.stringify({ + tier, + interval, + }), + }); + return response.json(); // May throw SyntaxError + } + + async deleteBillingSubscription() { + const url = accountBillingSubscriptionUrl(config.base_url); + console.log(`[AccountApi] Cancelling billing subscription`); + await fetchOrThrow(url, { + method: "DELETE", + headers: withBearerAuth({}, session.token()), + }); + } + + async createBillingPortalSession() { + const url = accountBillingPortalUrl(config.base_url); + console.log(`[AccountApi] Creating billing portal session`); + const response = await fetchOrThrow(url, { + method: "POST", + headers: withBearerAuth({}, session.token()), + }); + return response.json(); // May throw SyntaxError + } + + async verifyPhoneNumber(phoneNumber, channel) { + const url = accountPhoneVerifyUrl(config.base_url); + console.log(`[AccountApi] Sending phone verification ${url}`); + await fetchOrThrow(url, { + method: "PUT", + headers: withBearerAuth({}, session.token()), + body: JSON.stringify({ + number: phoneNumber, + channel, + }), + }); + } + + async addPhoneNumber(phoneNumber, code) { + const url = accountPhoneUrl(config.base_url); + console.log(`[AccountApi] Adding phone number with verification code ${url}`); + await fetchOrThrow(url, { + method: "PUT", + headers: withBearerAuth({}, session.token()), + body: JSON.stringify({ + number: phoneNumber, + code, + }), + }); + } + + async deletePhoneNumber(phoneNumber) { + const url = accountPhoneUrl(config.base_url); + console.log(`[AccountApi] Deleting phone number ${url}`); + await fetchOrThrow(url, { + method: "DELETE", + headers: withBearerAuth({}, session.token()), + body: JSON.stringify({ + number: phoneNumber, + }), + }); + } + + async sync() { + try { + if (!session.token()) { + return null; + } + console.log(`[AccountApi] Syncing account`); + const account = await this.get(); + if (account.language) { + await i18n.changeLanguage(account.language); + } + if (account.notification) { + if (account.notification.sound) { + await prefs.setSound(account.notification.sound); } - return json.token; - } - - async logout() { - const url = accountTokenUrl(config.base_url); - console.log(`[AccountApi] Logging out from ${url} using token ${session.token()}`); - await fetchOrThrow(url, { - method: "DELETE", - headers: withBearerAuth({}, session.token()) - }); - } - - async create(username, password) { - const url = accountUrl(config.base_url); - const body = JSON.stringify({ - username: username, - password: password - }); - console.log(`[AccountApi] Creating user account ${url}`); - await fetchOrThrow(url, { - method: "POST", - body: body - }); - } - - async get() { - const url = accountUrl(config.base_url); - console.log(`[AccountApi] Fetching user account ${url}`); - const response = await fetchOrThrow(url, { - headers: maybeWithBearerAuth({}, session.token()) // GET /v1/account endpoint can be called by anonymous - }); - const account = await response.json(); // May throw SyntaxError - console.log(`[AccountApi] Account`, account); - if (this.listener) { - this.listener(account); + if (account.notification.delete_after) { + await prefs.setDeleteAfter(account.notification.delete_after); } - return account; - } - - async delete(password) { - const url = accountUrl(config.base_url); - console.log(`[AccountApi] Deleting user account ${url}`); - await fetchOrThrow(url, { - method: "DELETE", - headers: withBearerAuth({}, session.token()), - body: JSON.stringify({ - password: password - }) - }); - } - - async changePassword(currentPassword, newPassword) { - const url = accountPasswordUrl(config.base_url); - console.log(`[AccountApi] Changing account password ${url}`); - await fetchOrThrow(url, { - method: "POST", - headers: withBearerAuth({}, session.token()), - body: JSON.stringify({ - password: currentPassword, - new_password: newPassword - }) - }); - } - - async createToken(label, expires) { - const url = accountTokenUrl(config.base_url); - const body = { - label: label, - expires: (expires > 0) ? Math.floor(Date.now() / 1000) + expires : 0 - }; - console.log(`[AccountApi] Creating user access token ${url}`); - await fetchOrThrow(url, { - method: "POST", - headers: withBearerAuth({}, session.token()), - body: JSON.stringify(body) - }); - } - - async updateToken(token, label, expires) { - const url = accountTokenUrl(config.base_url); - const body = { - token: token, - label: label - }; - if (expires > 0) { - body.expires = Math.floor(Date.now() / 1000) + expires; + if (account.notification.min_priority) { + await prefs.setMinPriority(account.notification.min_priority); } - console.log(`[AccountApi] Creating user access token ${url}`); - await fetchOrThrow(url, { - method: "PATCH", - headers: withBearerAuth({}, session.token()), - body: JSON.stringify(body) - }); + } + if (account.subscriptions) { + await subscriptionManager.syncFromRemote(account.subscriptions, account.reservations); + } + return account; + } catch (e) { + console.log(`[AccountApi] Error fetching account`, e); + if (e instanceof UnauthorizedError) { + await session.resetAndRedirect(routes.login); + } + return undefined; } + } - async extendToken() { - const url = accountTokenUrl(config.base_url); - console.log(`[AccountApi] Extending user access token ${url}`); - await fetchOrThrow(url, { - method: "PATCH", - headers: withBearerAuth({}, session.token()) - }); + startWorker() { + if (this.timer !== null) { + return; } + console.log(`[AccountApi] Starting worker`); + this.timer = setInterval(() => this.runWorker(), intervalMillis); + setTimeout(() => this.runWorker(), delayMillis); + } - async deleteToken(token) { - const url = accountTokenUrl(config.base_url); - console.log(`[AccountApi] Deleting user access token ${url}`); - await fetchOrThrow(url, { - method: "DELETE", - headers: withBearerAuth({"X-Token": token}, session.token()) - }); - } + stopWorker() { + clearTimeout(this.timer); + } - async updateSettings(payload) { - const url = accountSettingsUrl(config.base_url); - const body = JSON.stringify(payload); - console.log(`[AccountApi] Updating user account ${url}: ${body}`); - await fetchOrThrow(url, { - method: "PATCH", - headers: withBearerAuth({}, session.token()), - body: body - }); + async runWorker() { + if (!session.token()) { + return; } - - async addSubscription(baseUrl, topic) { - const url = accountSubscriptionUrl(config.base_url); - const body = JSON.stringify({ - base_url: baseUrl, - topic: topic - }); - console.log(`[AccountApi] Adding user subscription ${url}: ${body}`); - const response = await fetchOrThrow(url, { - method: "POST", - headers: withBearerAuth({}, session.token()), - body: body - }); - const subscription = await response.json(); // May throw SyntaxError - console.log(`[AccountApi] Subscription`, subscription); - return subscription; - } - - async updateSubscription(baseUrl, topic, payload) { - const url = accountSubscriptionUrl(config.base_url); - const body = JSON.stringify({ - base_url: baseUrl, - topic: topic, - ...payload - }); - console.log(`[AccountApi] Updating user subscription ${url}: ${body}`); - const response = await fetchOrThrow(url, { - method: "PATCH", - headers: withBearerAuth({}, session.token()), - body: body - }); - const subscription = await response.json(); // May throw SyntaxError - console.log(`[AccountApi] Subscription`, subscription); - return subscription; - } - - async deleteSubscription(baseUrl, topic) { - const url = accountSubscriptionUrl(config.base_url); - console.log(`[AccountApi] Removing user subscription ${url}`); - const headers = { - "X-BaseURL": baseUrl, - "X-Topic": topic, - } - await fetchOrThrow(url, { - method: "DELETE", - headers: withBearerAuth(headers, session.token()), - }); - } - - async upsertReservation(topic, everyone) { - const url = accountReservationUrl(config.base_url); - console.log(`[AccountApi] Upserting user access to topic ${topic}, everyone=${everyone}`); - await fetchOrThrow(url, { - method: "POST", - headers: withBearerAuth({}, session.token()), - body: JSON.stringify({ - topic: topic, - everyone: everyone - }) - }); - } - - async deleteReservation(topic, deleteMessages) { - const url = accountReservationSingleUrl(config.base_url, topic); - console.log(`[AccountApi] Removing topic reservation ${url}`); - const headers = { - "X-Delete-Messages": deleteMessages ? "true" : "false" - } - await fetchOrThrow(url, { - method: "DELETE", - headers: withBearerAuth(headers, session.token()) - }); - } - - async billingTiers() { - if (this.tiers) { - return this.tiers; - } - const url = tiersUrl(config.base_url); - console.log(`[AccountApi] Fetching billing tiers`); - const response = await fetchOrThrow(url); // No auth needed! - this.tiers = await response.json(); // May throw SyntaxError - return this.tiers; - } - - async createBillingSubscription(tier, interval) { - console.log(`[AccountApi] Creating billing subscription with ${tier} and interval ${interval}`); - return await this.upsertBillingSubscription("POST", tier, interval) - } - - async updateBillingSubscription(tier, interval) { - console.log(`[AccountApi] Updating billing subscription with ${tier} and interval ${interval}`); - return await this.upsertBillingSubscription("PUT", tier, interval) - } - - async upsertBillingSubscription(method, tier, interval) { - const url = accountBillingSubscriptionUrl(config.base_url); - const response = await fetchOrThrow(url, { - method: method, - headers: withBearerAuth({}, session.token()), - body: JSON.stringify({ - tier: tier, - interval: interval - }) - }); - return await response.json(); // May throw SyntaxError - } - - async deleteBillingSubscription() { - const url = accountBillingSubscriptionUrl(config.base_url); - console.log(`[AccountApi] Cancelling billing subscription`); - await fetchOrThrow(url, { - method: "DELETE", - headers: withBearerAuth({}, session.token()) - }); - } - - async createBillingPortalSession() { - const url = accountBillingPortalUrl(config.base_url); - console.log(`[AccountApi] Creating billing portal session`); - const response = await fetchOrThrow(url, { - method: "POST", - headers: withBearerAuth({}, session.token()) - }); - return await response.json(); // May throw SyntaxError - } - - async sync() { - try { - if (!session.token()) { - return null; - } - console.log(`[AccountApi] Syncing account`); - const account = await this.get(); - if (account.language) { - await i18n.changeLanguage(account.language); - } - if (account.notification) { - if (account.notification.sound) { - await prefs.setSound(account.notification.sound); - } - if (account.notification.delete_after) { - await prefs.setDeleteAfter(account.notification.delete_after); - } - if (account.notification.min_priority) { - await prefs.setMinPriority(account.notification.min_priority); - } - } - if (account.subscriptions) { - await subscriptionManager.syncFromRemote(account.subscriptions, account.reservations); - } - return account; - } catch (e) { - console.log(`[AccountApi] Error fetching account`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } - } - } - - startWorker() { - if (this.timer !== null) { - return; - } - console.log(`[AccountApi] Starting worker`); - this.timer = setInterval(() => this.runWorker(), intervalMillis); - setTimeout(() => this.runWorker(), delayMillis); - } - - async runWorker() { - if (!session.token()) { - return; - } - console.log(`[AccountApi] Extending user access token`); - try { - await this.extendToken(); - } catch (e) { - console.log(`[AccountApi] Error extending user access token`, e); - } + console.log(`[AccountApi] Extending user access token`); + try { + await this.extendToken(); + } catch (e) { + console.log(`[AccountApi] Error extending user access token`, e); } + } } // Maps to user.Role in user/types.go export const Role = { - ADMIN: "admin", - USER: "user" + ADMIN: "admin", + USER: "user", }; // Maps to server.visitorLimitBasis in server/visitor.go export const LimitBasis = { - IP: "ip", - TIER: "tier" + IP: "ip", + TIER: "tier", }; // Maps to stripe.SubscriptionStatus export const SubscriptionStatus = { - ACTIVE: "active", - PAST_DUE: "past_due" + ACTIVE: "active", + PAST_DUE: "past_due", }; // Maps to stripe.PriceRecurringInterval export const SubscriptionInterval = { - MONTH: "month", - YEAR: "year" + MONTH: "month", + YEAR: "year", }; // Maps to user.Permission in user/types.go export const Permission = { - READ_WRITE: "read-write", - READ_ONLY: "read-only", - WRITE_ONLY: "write-only", - DENY_ALL: "deny-all" + READ_WRITE: "read-write", + READ_ONLY: "read-only", + WRITE_ONLY: "write-only", + DENY_ALL: "deny-all", }; const accountApi = new AccountApi(); diff --git a/web/src/app/Api.js b/web/src/app/Api.js index 3d20d922..b2bfd06f 100644 --- a/web/src/app/Api.js +++ b/web/src/app/Api.js @@ -1,115 +1,149 @@ import { - fetchLinesIterator, - maybeWithAuth, - topicShortUrl, - topicUrl, - topicUrlAuth, - topicUrlJsonPoll, - topicUrlJsonPollWithSince + fetchLinesIterator, + maybeWithAuth, + topicShortUrl, + topicUrl, + topicUrlAuth, + topicUrlJsonPoll, + topicUrlJsonPollWithSince, + webPushUrl, } from "./utils"; import userManager from "./UserManager"; -import {fetchOrThrow} from "./errors"; +import { fetchOrThrow } from "./errors"; class Api { - async poll(baseUrl, topic, since) { - const user = await userManager.get(baseUrl); - const shortUrl = topicShortUrl(baseUrl, topic); - const url = (since) - ? topicUrlJsonPollWithSince(baseUrl, topic, since) - : topicUrlJsonPoll(baseUrl, topic); - const messages = []; - const headers = maybeWithAuth({}, user); - console.log(`[Api] Polling ${url}`); - for await (let line of fetchLinesIterator(url, headers)) { - console.log(`[Api, ${shortUrl}] Received message ${line}`); - messages.push(JSON.parse(line)); - } - return messages; + async poll(baseUrl, topic, since) { + const user = await userManager.get(baseUrl); + const shortUrl = topicShortUrl(baseUrl, topic); + const url = since ? topicUrlJsonPollWithSince(baseUrl, topic, since) : topicUrlJsonPoll(baseUrl, topic); + const messages = []; + const headers = maybeWithAuth({}, user); + console.log(`[Api] Polling ${url}`); + for await (const line of fetchLinesIterator(url, headers)) { + const message = JSON.parse(line); + if (message.id) { + console.log(`[Api, ${shortUrl}] Received message ${line}`); + messages.push(message); + } } + return messages; + } - async publish(baseUrl, topic, message, options) { - const user = await userManager.get(baseUrl); - console.log(`[Api] Publishing message to ${topicUrl(baseUrl, topic)}`); - const headers = {}; - const body = { - topic: topic, - message: message, - ...options - }; - await fetchOrThrow(baseUrl, { - method: 'PUT', - body: JSON.stringify(body), - headers: maybeWithAuth(headers, user) - }); - } + async publish(baseUrl, topic, message, options) { + const user = await userManager.get(baseUrl); + console.log(`[Api] Publishing message to ${topicUrl(baseUrl, topic)}`); + const headers = {}; + const body = { + topic, + message, + ...options, + }; + await fetchOrThrow(baseUrl, { + method: "PUT", + body: JSON.stringify(body), + headers: maybeWithAuth(headers, user), + }); + } - /** - * Publishes to a topic using XMLHttpRequest (XHR), and returns a Promise with the active request. - * Unfortunately, fetch() does not support a progress hook, which is why XHR has to be used. - * - * Firefox XHR bug: - * Firefox has a bug(?), which returns 0 and "" for all fields of the XHR response in the case of an error, - * so we cannot determine the exact error. It also sometimes complains about CORS violations, even when the - * correct headers are clearly set. It's quite the odd behavior. - * - * There is an example, and the bug report here: - * - https://bugzilla.mozilla.org/show_bug.cgi?id=1733755 - * - https://gist.github.com/binwiederhier/627f146d1959799be207ad8c17a8f345 - */ - publishXHR(url, body, headers, onProgress) { - console.log(`[Api] Publishing message to ${url}`); - const xhr = new XMLHttpRequest(); - const send = new Promise(function (resolve, reject) { - xhr.open("PUT", url); - if (body.type) { - xhr.overrideMimeType(body.type); + /** + * Publishes to a topic using XMLHttpRequest (XHR), and returns a Promise with the active request. + * Unfortunately, fetch() does not support a progress hook, which is why XHR has to be used. + * + * Firefox XHR bug: + * Firefox has a bug(?), which returns 0 and "" for all fields of the XHR response in the case of an error, + * so we cannot determine the exact error. It also sometimes complains about CORS violations, even when the + * correct headers are clearly set. It's quite the odd behavior. + * + * There is an example, and the bug report here: + * - https://bugzilla.mozilla.org/show_bug.cgi?id=1733755 + * - https://gist.github.com/binwiederhier/627f146d1959799be207ad8c17a8f345 + */ + publishXHR(url, body, headers, onProgress) { + console.log(`[Api] Publishing message to ${url}`); + const xhr = new XMLHttpRequest(); + const send = new Promise((resolve, reject) => { + xhr.open("PUT", url); + if (body.type) { + xhr.overrideMimeType(body.type); + } + for (const [key, value] of Object.entries(headers)) { + xhr.setRequestHeader(key, value); + } + xhr.upload.addEventListener("progress", onProgress); + xhr.addEventListener("readystatechange", () => { + if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status <= 299) { + console.log(`[Api] Publish successful (HTTP ${xhr.status})`, xhr.response); + resolve(xhr.response); + } else if (xhr.readyState === 4) { + // Firefox bug; see description above! + console.log(`[Api] Publish failed (HTTP ${xhr.status})`, xhr.responseText); + let errorText; + try { + const error = JSON.parse(xhr.responseText); + if (error.code && error.error) { + errorText = `Error ${error.code}: ${error.error}`; } - for (const [key, value] of Object.entries(headers)) { - xhr.setRequestHeader(key, value); - } - xhr.upload.addEventListener("progress", onProgress); - xhr.addEventListener('readystatechange', () => { - if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status <= 299) { - console.log(`[Api] Publish successful (HTTP ${xhr.status})`, xhr.response); - resolve(xhr.response); - } else if (xhr.readyState === 4) { - // Firefox bug; see description above! - console.log(`[Api] Publish failed (HTTP ${xhr.status})`, xhr.responseText); - let errorText; - try { - const error = JSON.parse(xhr.responseText); - if (error.code && error.error) { - errorText = `Error ${error.code}: ${error.error}`; - } - } catch (e) { - // Nothing - } - xhr.abort(); - reject(errorText ?? "An error occurred"); - } - }) - xhr.send(body); - }); - send.abort = () => { - console.log(`[Api] Publish aborted by user`); - xhr.abort(); + } catch (e) { + // Nothing + } + xhr.abort(); + reject(errorText ?? "An error occurred"); } - return send; - } + }); + xhr.send(body); + }); + send.abort = () => { + console.log(`[Api] Publish aborted by user`); + xhr.abort(); + }; + return send; + } - async topicAuth(baseUrl, topic, user) { - const url = topicUrlAuth(baseUrl, topic); - console.log(`[Api] Checking auth for ${url}`); - const response = await fetch(url, { - headers: maybeWithAuth({}, user) - }); - if (response.status >= 200 && response.status <= 299) { - return true; - } else if (response.status === 401 || response.status === 403) { // See server/server.go - return false; - } - throw new Error(`Unexpected server response ${response.status}`); + async topicAuth(baseUrl, topic, user) { + const url = topicUrlAuth(baseUrl, topic); + console.log(`[Api] Checking auth for ${url}`); + const response = await fetch(url, { + headers: maybeWithAuth({}, user), + }); + if (response.status >= 200 && response.status <= 299) { + return true; } + if (response.status === 401 || response.status === 403) { + // See server/server.go + return false; + } + throw new Error(`Unexpected server response ${response.status}`); + } + + async updateWebPush(pushSubscription, topics) { + const user = await userManager.get(config.base_url); + const url = webPushUrl(config.base_url); + console.log(`[Api] Updating Web Push subscription`, { url, topics, endpoint: pushSubscription.endpoint }); + const serializedSubscription = JSON.parse(JSON.stringify(pushSubscription)); // Ugh ... https://stackoverflow.com/a/40525434/1440785 + await fetchOrThrow(url, { + method: "POST", + headers: maybeWithAuth({}, user), + body: JSON.stringify({ + endpoint: serializedSubscription.endpoint, + auth: serializedSubscription.keys.auth, + p256dh: serializedSubscription.keys.p256dh, + topics, + }), + }); + } + + async deleteWebPush(pushSubscription) { + const user = await userManager.get(config.base_url); + const url = webPushUrl(config.base_url); + console.log(`[Api] Deleting Web Push subscription`, { url, endpoint: pushSubscription.endpoint }); + await fetchOrThrow(url, { + method: "DELETE", + headers: maybeWithAuth({}, user), + body: JSON.stringify({ + endpoint: pushSubscription.endpoint, + }), + }); + } } const api = new Api(); diff --git a/web/src/app/Connection.js b/web/src/app/Connection.js index e86af78a..5358cdde 100644 --- a/web/src/app/Connection.js +++ b/web/src/app/Connection.js @@ -1,7 +1,14 @@ -import {basicAuth, bearerAuth, encodeBase64Url, topicShortUrl, topicUrlWs} from "./utils"; +/* eslint-disable max-classes-per-file */ +import { basicAuth, bearerAuth, encodeBase64Url, topicShortUrl, topicUrlWs } from "./utils"; const retryBackoffSeconds = [5, 10, 20, 30, 60, 120]; +export class ConnectionState { + static Connected = "connected"; + + static Connecting = "connecting"; +} + /** * A connection contains a single WebSocket connection for one topic. It handles its connection * status itself, including reconnect attempts and backoff. @@ -9,110 +16,103 @@ const retryBackoffSeconds = [5, 10, 20, 30, 60, 120]; * Incoming messages and state changes are forwarded via listeners. */ class Connection { - constructor(connectionId, subscriptionId, baseUrl, topic, user, since, onNotification, onStateChanged) { - this.connectionId = connectionId; - this.subscriptionId = subscriptionId; - this.baseUrl = baseUrl; - this.topic = topic; - this.user = user; - this.since = since; - this.shortUrl = topicShortUrl(baseUrl, topic); - this.onNotification = onNotification; - this.onStateChanged = onStateChanged; + constructor(connectionId, subscriptionId, baseUrl, topic, user, since, onNotification, onStateChanged) { + this.connectionId = connectionId; + this.subscriptionId = subscriptionId; + this.baseUrl = baseUrl; + this.topic = topic; + this.user = user; + this.since = since; + this.shortUrl = topicShortUrl(baseUrl, topic); + this.onNotification = onNotification; + this.onStateChanged = onStateChanged; + this.ws = null; + this.retryCount = 0; + this.retryTimeout = null; + } + + start() { + // Don't fetch old messages; we do that as a poll() when adding a subscription; + // we don't want to re-trigger the main view re-render potentially hundreds of times. + + const wsUrl = this.wsUrl(); + console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Opening connection to ${wsUrl}`); + + this.ws = new WebSocket(wsUrl); + this.ws.onopen = (event) => { + console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection established`, event); + this.retryCount = 0; + this.onStateChanged(this.subscriptionId, ConnectionState.Connected); + }; + this.ws.onmessage = (event) => { + console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Message received from server: ${event.data}`); + try { + const data = JSON.parse(event.data); + if (data.event === "open") { + return; + } + const relevantAndValid = data.event === "message" && "id" in data && "time" in data && "message" in data; + if (!relevantAndValid) { + console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Unexpected message. Ignoring.`); + return; + } + this.since = data.id; + this.onNotification(this.subscriptionId, data); + } catch (e) { + console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Error handling message: ${e}`); + } + }; + this.ws.onclose = (event) => { + if (event.wasClean) { + console.log( + `[Connection, ${this.shortUrl}, ${this.connectionId}] Connection closed cleanly, code=${event.code} reason=${event.reason}` + ); this.ws = null; - this.retryCount = 0; - this.retryTimeout = null; + } else { + const retrySeconds = retryBackoffSeconds[Math.min(this.retryCount, retryBackoffSeconds.length - 1)]; + this.retryCount += 1; + console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection died, retrying in ${retrySeconds} seconds`); + this.retryTimeout = setTimeout(() => this.start(), retrySeconds * 1000); + this.onStateChanged(this.subscriptionId, ConnectionState.Connecting); + } + }; + this.ws.onerror = (event) => { + console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Error occurred: ${event}`, event); + }; + } + + close() { + console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Closing connection`); + const socket = this.ws; + const { retryTimeout } = this; + if (socket !== null) { + socket.close(); } - - start() { - // Don't fetch old messages; we do that as a poll() when adding a subscription; - // we don't want to re-trigger the main view re-render potentially hundreds of times. - - const wsUrl = this.wsUrl(); - console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Opening connection to ${wsUrl}`); - - this.ws = new WebSocket(wsUrl); - this.ws.onopen = (event) => { - console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection established`, event); - this.retryCount = 0; - this.onStateChanged(this.subscriptionId, ConnectionState.Connected); - } - this.ws.onmessage = (event) => { - console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Message received from server: ${event.data}`); - try { - const data = JSON.parse(event.data); - if (data.event === 'open') { - return; - } - const relevantAndValid = - data.event === 'message' && - 'id' in data && - 'time' in data && - 'message' in data; - if (!relevantAndValid) { - console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Unexpected message. Ignoring.`); - return; - } - this.since = data.id; - this.onNotification(this.subscriptionId, data); - } catch (e) { - console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Error handling message: ${e}`); - } - }; - this.ws.onclose = (event) => { - if (event.wasClean) { - console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection closed cleanly, code=${event.code} reason=${event.reason}`); - this.ws = null; - } else { - const retrySeconds = retryBackoffSeconds[Math.min(this.retryCount, retryBackoffSeconds.length-1)]; - this.retryCount++; - console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection died, retrying in ${retrySeconds} seconds`); - this.retryTimeout = setTimeout(() => this.start(), retrySeconds * 1000); - this.onStateChanged(this.subscriptionId, ConnectionState.Connecting); - } - }; - this.ws.onerror = (event) => { - console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Error occurred: ${event}`, event); - }; + if (retryTimeout !== null) { + clearTimeout(retryTimeout); } + this.retryTimeout = null; + this.ws = null; + } - close() { - console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Closing connection`); - const socket = this.ws; - const retryTimeout = this.retryTimeout; - if (socket !== null) { - socket.close(); - } - if (retryTimeout !== null) { - clearTimeout(retryTimeout); - } - this.retryTimeout = null; - this.ws = null; + wsUrl() { + const params = []; + if (this.since) { + params.push(`since=${this.since}`); } - - wsUrl() { - const params = []; - if (this.since) { - params.push(`since=${this.since}`); - } - if (this.user) { - params.push(`auth=${this.authParam()}`); - } - const wsUrl = topicUrlWs(this.baseUrl, this.topic); - return (params.length === 0) ? wsUrl : `${wsUrl}?${params.join('&')}`; + if (this.user) { + params.push(`auth=${this.authParam()}`); } + const wsUrl = topicUrlWs(this.baseUrl, this.topic); + return params.length === 0 ? wsUrl : `${wsUrl}?${params.join("&")}`; + } - authParam() { - if (this.user.password) { - return encodeBase64Url(basicAuth(this.user.username, this.user.password)); - } - return encodeBase64Url(bearerAuth(this.user.token)); + authParam() { + if (this.user.password) { + return encodeBase64Url(basicAuth(this.user.username, this.user.password)); } -} - -export class ConnectionState { - static Connected = "connected"; - static Connecting = "connecting"; + return encodeBase64Url(bearerAuth(this.user.token)); + } } export default Connection; diff --git a/web/src/app/ConnectionManager.js b/web/src/app/ConnectionManager.js index 1e805eb7..32ffe807 100644 --- a/web/src/app/ConnectionManager.js +++ b/web/src/app/ConnectionManager.js @@ -1,5 +1,8 @@ import Connection from "./Connection"; -import {hashCode} from "./utils"; +import { hashCode } from "./utils"; + +const makeConnectionId = (subscription, user) => + user ? hashCode(`${subscription.id}|${user.username}|${user.password ?? ""}|${user.token ?? ""}`) : hashCode(`${subscription.id}`); /** * The connection manager keeps track of active connections (WebSocket connections, see Connection). @@ -8,109 +11,105 @@ import {hashCode} from "./utils"; * as required. This is done pretty much exactly the same way as in the Android app. */ class ConnectionManager { - constructor() { - this.connections = new Map(); // ConnectionId -> Connection (hash, see below) - this.stateListener = null; // Fired when connection state changes - this.messageListener = null; // Fired when new notifications arrive + constructor() { + this.connections = new Map(); // ConnectionId -> Connection (hash, see below) + this.stateListener = null; // Fired when connection state changes + this.messageListener = null; // Fired when new notifications arrive + } + + registerStateListener(listener) { + this.stateListener = listener; + } + + resetStateListener() { + this.stateListener = null; + } + + registerMessageListener(listener) { + this.messageListener = listener; + } + + resetMessageListener() { + this.messageListener = null; + } + + /** + * This function figures out which websocket connections should be running by comparing the + * current state of the world (connections) with the target state (targetIds). + * + * It uses a "connectionId", which is sha256($subscriptionId|$username|$password) to identify + * connections. If any of them change, the connection is closed/replaced. + */ + async refresh(subscriptions, users) { + if (!subscriptions || !users) { + return; } + console.log(`[ConnectionManager] Refreshing connections`); + const subscriptionsWithUsersAndConnectionId = subscriptions.map((s) => { + const [user] = users.filter((u) => u.baseUrl === s.baseUrl); + const connectionId = makeConnectionId(s, user); + return { ...s, user, connectionId }; + }); - registerStateListener(listener) { - this.stateListener = listener; + const targetIds = subscriptionsWithUsersAndConnectionId.map((s) => s.connectionId); + const deletedIds = Array.from(this.connections.keys()).filter((id) => !targetIds.includes(id)); + + // Create and add new connections + subscriptionsWithUsersAndConnectionId.forEach((subscription) => { + const subscriptionId = subscription.id; + const { connectionId } = subscription; + const added = !this.connections.get(connectionId); + if (added) { + const { baseUrl, topic, user } = subscription; + const since = subscription.last; + const connection = new Connection( + connectionId, + subscriptionId, + baseUrl, + topic, + user, + since, + (subId, notification) => this.notificationReceived(subId, notification), + (subId, state) => this.stateChanged(subId, state) + ); + this.connections.set(connectionId, connection); + console.log( + `[ConnectionManager] Starting new connection ${connectionId} (subscription ${subscriptionId} with user ${ + user ? user.username : "anonymous" + })` + ); + connection.start(); + } + }); + + // Delete old connections + deletedIds.forEach((id) => { + console.log(`[ConnectionManager] Closing connection ${id}`); + const connection = this.connections.get(id); + this.connections.delete(id); + connection.close(); + }); + } + + stateChanged(subscriptionId, state) { + if (this.stateListener) { + try { + this.stateListener(subscriptionId, state); + } catch (e) { + console.error(`[ConnectionManager] Error updating state of ${subscriptionId} to ${state}`, e); + } } + } - resetStateListener() { - this.stateListener = null; + notificationReceived(subscriptionId, notification) { + if (this.messageListener) { + try { + this.messageListener(subscriptionId, notification); + } catch (e) { + console.error(`[ConnectionManager] Error handling notification for ${subscriptionId}`, e); + } } - - registerMessageListener(listener) { - this.messageListener = listener; - } - - resetMessageListener() { - this.messageListener = null; - } - - /** - * This function figures out which websocket connections should be running by comparing the - * current state of the world (connections) with the target state (targetIds). - * - * It uses a "connectionId", which is sha256($subscriptionId|$username|$password) to identify - * connections. If any of them change, the connection is closed/replaced. - */ - async refresh(subscriptions, users) { - if (!subscriptions || !users) { - return; - } - console.log(`[ConnectionManager] Refreshing connections`); - const subscriptionsWithUsersAndConnectionId = await Promise.all(subscriptions - .map(async s => { - const [user] = users.filter(u => u.baseUrl === s.baseUrl); - const connectionId = await makeConnectionId(s, user); - return {...s, user, connectionId}; - })); - const targetIds = subscriptionsWithUsersAndConnectionId.map(s => s.connectionId); - const deletedIds = Array.from(this.connections.keys()).filter(id => !targetIds.includes(id)); - - // Create and add new connections - subscriptionsWithUsersAndConnectionId.forEach(subscription => { - const subscriptionId = subscription.id; - const connectionId = subscription.connectionId; - const added = !this.connections.get(connectionId) - if (added) { - const baseUrl = subscription.baseUrl; - const topic = subscription.topic; - const user = subscription.user; - const since = subscription.last; - const connection = new Connection( - connectionId, - subscriptionId, - baseUrl, - topic, - user, - since, - (subscriptionId, notification) => this.notificationReceived(subscriptionId, notification), - (subscriptionId, state) => this.stateChanged(subscriptionId, state) - ); - this.connections.set(connectionId, connection); - console.log(`[ConnectionManager] Starting new connection ${connectionId} (subscription ${subscriptionId} with user ${user ? user.username : "anonymous"})`); - connection.start(); - } - }); - - // Delete old connections - deletedIds.forEach(id => { - console.log(`[ConnectionManager] Closing connection ${id}`); - const connection = this.connections.get(id); - this.connections.delete(id); - connection.close(); - }); - } - - stateChanged(subscriptionId, state) { - if (this.stateListener) { - try { - this.stateListener(subscriptionId, state); - } catch (e) { - console.error(`[ConnectionManager] Error updating state of ${subscriptionId} to ${state}`, e); - } - } - } - - notificationReceived(subscriptionId, notification) { - if (this.messageListener) { - try { - this.messageListener(subscriptionId, notification); - } catch (e) { - console.error(`[ConnectionManager] Error handling notification for ${subscriptionId}`, e); - } - } - } -} - -const makeConnectionId = async (subscription, user) => { - return (user) - ? hashCode(`${subscription.id}|${user.username}|${user.password ?? ""}|${user.token ?? ""}`) - : hashCode(`${subscription.id}`); + } } const connectionManager = new ConnectionManager(); diff --git a/web/src/app/Notifier.js b/web/src/app/Notifier.js index 613340cb..77bbdb1e 100644 --- a/web/src/app/Notifier.js +++ b/web/src/app/Notifier.js @@ -1,96 +1,141 @@ -import {formatMessage, formatTitleWithDefault, openUrl, playSound, topicDisplayName, topicShortUrl} from "./utils"; +import { playSound, topicDisplayName, topicShortUrl, urlB64ToUint8Array } from "./utils"; +import { toNotificationParams } from "./notificationUtils"; import prefs from "./Prefs"; -import subscriptionManager from "./SubscriptionManager"; -import logo from "../img/ntfy.png"; +import routes from "../components/routes"; /** * The notifier is responsible for displaying desktop notifications. Note that not all modern browsers * support this; most importantly, all iOS browsers do not support window.Notification. */ class Notifier { - async notify(subscriptionId, notification, onClickFallback) { - if (!this.supported()) { - return; - } - const subscription = await subscriptionManager.get(subscriptionId); - const shouldNotify = await this.shouldNotify(subscription, notification); - if (!shouldNotify) { - return; - } - const shortUrl = topicShortUrl(subscription.baseUrl, subscription.topic); - const displayName = topicDisplayName(subscription); - const message = formatMessage(notification); - const title = formatTitleWithDefault(notification, displayName); - - // Show notification - console.log(`[Notifier, ${shortUrl}] Displaying notification ${notification.id}: ${message}`); - const n = new Notification(title, { - body: message, - icon: logo - }); - if (notification.click) { - n.onclick = (e) => openUrl(notification.click); - } else { - n.onclick = () => onClickFallback(subscription); - } - - // Play sound - const sound = await prefs.sound(); - if (sound && sound !== "none") { - try { - await playSound(sound); - } catch (e) { - console.log(`[Notifier, ${shortUrl}] Error playing audio`, e); - } - } + async notify(subscription, notification) { + if (!this.supported()) { + return; } - granted() { - return this.supported() && Notification.permission === 'granted'; + await this.playSound(); + + const shortUrl = topicShortUrl(subscription.baseUrl, subscription.topic); + const defaultTitle = topicDisplayName(subscription); + + console.log(`[Notifier, ${shortUrl}] Displaying notification ${notification.id}`); + + const registration = await this.serviceWorkerRegistration(); + await registration.showNotification( + ...toNotificationParams({ + subscriptionId: subscription.id, + message: notification, + defaultTitle, + topicRoute: new URL(routes.forSubscription(subscription), window.location.origin).toString(), + }) + ); + } + + async playSound() { + // Play sound + const sound = await prefs.sound(); + if (sound && sound !== "none") { + try { + await playSound(sound); + } catch (e) { + console.log(`[Notifier] Error playing audio`, e); + } + } + } + + async webPushSubscription(hasWebPushTopics) { + const pushManager = await this.pushManager(); + const existingSubscription = await pushManager.getSubscription(); + if (existingSubscription) { + return existingSubscription; } - maybeRequestPermission(cb) { - if (!this.supported()) { - cb(false); - return; - } - if (!this.granted()) { - Notification.requestPermission().then((permission) => { - const granted = permission === 'granted'; - cb(granted); - }); - } + // Create a new subscription only if there are new topics to subscribe to. It is possible that Web Push + // was previously enabled and then disabled again in which case there would be an existingSubscription. + // If, however, it was _not_ enabled previously, we create a new subscription if it is now enabled. + + if (hasWebPushTopics) { + return pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: urlB64ToUint8Array(config.web_push_public_key), + }); } - async shouldNotify(subscription, notification) { - if (subscription.mutedUntil === 1) { - return false; - } - const priority = (notification.priority) ? notification.priority : 3; - const minPriority = await prefs.minPriority(); - if (priority < minPriority) { - return false; - } - return true; + return undefined; + } + + async pushManager() { + return (await this.serviceWorkerRegistration()).pushManager; + } + + async serviceWorkerRegistration() { + const registration = await navigator.serviceWorker.getRegistration(); + if (!registration) { + throw new Error("No service worker registration found"); + } + return registration; + } + + notRequested() { + return this.supported() && Notification.permission === "default"; + } + + granted() { + return this.supported() && Notification.permission === "granted"; + } + + denied() { + return this.supported() && Notification.permission === "denied"; + } + + async maybeRequestPermission() { + if (!this.supported()) { + return false; } - supported() { - return this.browserSupported() && this.contextSupported(); - } + return new Promise((resolve) => { + Notification.requestPermission((permission) => { + resolve(permission === "granted"); + }); + }); + } - browserSupported() { - return 'Notification' in window; - } + supported() { + return this.browserSupported() && this.contextSupported(); + } - /** - * Returns true if this is a HTTPS site, or served over localhost. Otherwise the Notification API - * is not supported, see https://developer.mozilla.org/en-US/docs/Web/API/notification - */ - contextSupported() { - return location.protocol === 'https:' - || location.hostname.match('^127.') - || location.hostname === 'localhost'; - } + browserSupported() { + return "Notification" in window; + } + + pushSupported() { + return config.enable_web_push && "serviceWorker" in navigator && "PushManager" in window; + } + + pushPossible() { + return this.pushSupported() && this.contextSupported() && this.granted() && !this.iosSupportedButInstallRequired(); + } + + /** + * Returns true if this is a HTTPS site, or served over localhost. Otherwise the Notification API + * is not supported, see https://developer.mozilla.org/en-US/docs/Web/API/notification + */ + contextSupported() { + return window.location.protocol === "https:" || window.location.hostname.match("^127.") || window.location.hostname === "localhost"; + } + + // no PushManager when not installed, but it _is_ supported. + iosSupportedButInstallRequired() { + return ( + config.enable_web_push && + // a service worker exists + "serviceWorker" in navigator && + // but the pushmanager API is missing, which implies we're on an iOS device without installing + !("PushManager" in window) && + // check that this is the case by checking for `standalone`, which only exists on Safari + window.navigator.standalone === false + ); + } } const notifier = new Notifier(); diff --git a/web/src/app/Poller.js b/web/src/app/Poller.js index a7eed032..2261dddc 100644 --- a/web/src/app/Poller.js +++ b/web/src/app/Poller.js @@ -5,54 +5,60 @@ const delayMillis = 2000; // 2 seconds const intervalMillis = 300000; // 5 minutes class Poller { - constructor() { - this.timer = null; - } + constructor() { + this.timer = null; + } - startWorker() { - if (this.timer !== null) { - return; + startWorker() { + if (this.timer !== null) { + return; + } + console.log(`[Poller] Starting worker`); + this.timer = setInterval(() => this.pollAll(), intervalMillis); + setTimeout(() => this.pollAll(), delayMillis); + } + + stopWorker() { + clearTimeout(this.timer); + } + + async pollAll() { + console.log(`[Poller] Polling all subscriptions`); + const subscriptions = await subscriptionManager.all(); + + await Promise.all( + subscriptions.map(async (s) => { + try { + await this.poll(s); + } catch (e) { + console.log(`[Poller] Error polling ${s.id}`, e); } - console.log(`[Poller] Starting worker`); - this.timer = setInterval(() => this.pollAll(), intervalMillis); - setTimeout(() => this.pollAll(), delayMillis); - } + }) + ); + } - async pollAll() { - console.log(`[Poller] Polling all subscriptions`); - const subscriptions = await subscriptionManager.all(); - for (const s of subscriptions) { - try { - await this.poll(s); - } catch (e) { - console.log(`[Poller] Error polling ${s.id}`, e); - } - } - } + async poll(subscription) { + console.log(`[Poller] Polling ${subscription.id}`); - async poll(subscription) { - console.log(`[Poller] Polling ${subscription.id}`); - - const since = subscription.last; - const notifications = await api.poll(subscription.baseUrl, subscription.topic, since); - if (!notifications || notifications.length === 0) { - console.log(`[Poller] No new notifications found for ${subscription.id}`); - return; - } - console.log(`[Poller] Adding ${notifications.length} notification(s) for ${subscription.id}`); - await subscriptionManager.addNotifications(subscription.id, notifications); + const since = subscription.last; + const notifications = await api.poll(subscription.baseUrl, subscription.topic, since); + if (!notifications || notifications.length === 0) { + console.log(`[Poller] No new notifications found for ${subscription.id}`); + return; } + console.log(`[Poller] Adding ${notifications.length} notification(s) for ${subscription.id}`); + await subscriptionManager.addNotifications(subscription.id, notifications); + } - pollInBackground(subscription) { - const fn = async () => { - try { - await this.poll(subscription); - } catch (e) { - console.error(`[App] Error polling subscription ${subscription.id}`, e); - } - }; - setTimeout(() => fn(), 0); - } + pollInBackground(subscription) { + (async () => { + try { + await this.poll(subscription); + } catch (e) { + console.error(`[App] Error polling subscription ${subscription.id}`, e); + } + })(); + } } const poller = new Poller(); diff --git a/web/src/app/Prefs.js b/web/src/app/Prefs.js index b444c6f8..4f28f87e 100644 --- a/web/src/app/Prefs.js +++ b/web/src/app/Prefs.js @@ -1,33 +1,61 @@ import db from "./db"; +export const THEME = { + DARK: "dark", + LIGHT: "light", + SYSTEM: "system", +}; + class Prefs { - async setSound(sound) { - db.prefs.put({key: 'sound', value: sound.toString()}); - } + constructor(dbImpl) { + this.db = dbImpl; + } - async sound() { - const sound = await db.prefs.get('sound'); - return (sound) ? sound.value : "ding"; - } + async setSound(sound) { + this.db.prefs.put({ key: "sound", value: sound.toString() }); + } - async setMinPriority(minPriority) { - db.prefs.put({key: 'minPriority', value: minPriority.toString()}); - } + async sound() { + const sound = await this.db.prefs.get("sound"); + return sound ? sound.value : "ding"; + } - async minPriority() { - const minPriority = await db.prefs.get('minPriority'); - return (minPriority) ? Number(minPriority.value) : 1; - } + async setMinPriority(minPriority) { + this.db.prefs.put({ key: "minPriority", value: minPriority.toString() }); + } - async setDeleteAfter(deleteAfter) { - db.prefs.put({key:'deleteAfter', value: deleteAfter.toString()}); - } + async minPriority() { + const minPriority = await this.db.prefs.get("minPriority"); + return minPriority ? Number(minPriority.value) : 1; + } - async deleteAfter() { - const deleteAfter = await db.prefs.get('deleteAfter'); - return (deleteAfter) ? Number(deleteAfter.value) : 604800; // Default is one week - } + async setDeleteAfter(deleteAfter) { + await this.db.prefs.put({ key: "deleteAfter", value: deleteAfter.toString() }); + } + + async deleteAfter() { + const deleteAfter = await this.db.prefs.get("deleteAfter"); + return deleteAfter ? Number(deleteAfter.value) : 604800; // Default is one week + } + + async webPushEnabled() { + const webPushEnabled = await this.db.prefs.get("webPushEnabled"); + return webPushEnabled?.value; + } + + async setWebPushEnabled(enabled) { + await this.db.prefs.put({ key: "webPushEnabled", value: enabled }); + } + + async theme() { + const theme = await this.db.prefs.get("theme"); + return theme?.value ?? THEME.SYSTEM; + } + + async setTheme(mode) { + await this.db.prefs.put({ key: "theme", value: mode }); + } } -const prefs = new Prefs(); +const prefs = new Prefs(db()); export default prefs; diff --git a/web/src/app/Pruner.js b/web/src/app/Pruner.js index 45948057..f9568a33 100644 --- a/web/src/app/Pruner.js +++ b/web/src/app/Pruner.js @@ -5,33 +5,37 @@ const delayMillis = 25000; // 25 seconds const intervalMillis = 1800000; // 30 minutes class Pruner { - constructor() { - this.timer = null; - } + constructor() { + this.timer = null; + } - startWorker() { - if (this.timer !== null) { - return; - } - console.log(`[Pruner] Starting worker`); - this.timer = setInterval(() => this.prune(), intervalMillis); - setTimeout(() => this.prune(), delayMillis); + startWorker() { + if (this.timer !== null) { + return; } + console.log(`[Pruner] Starting worker`); + this.timer = setInterval(() => this.prune(), intervalMillis); + setTimeout(() => this.prune(), delayMillis); + } - async prune() { - const deleteAfterSeconds = await prefs.deleteAfter(); - const pruneThresholdTimestamp = Math.round(Date.now()/1000) - deleteAfterSeconds; - if (deleteAfterSeconds === 0) { - console.log(`[Pruner] Pruning is disabled. Skipping.`); - return; - } - console.log(`[Pruner] Pruning notifications older than ${deleteAfterSeconds}s (timestamp ${pruneThresholdTimestamp})`); - try { - await subscriptionManager.pruneNotifications(pruneThresholdTimestamp); - } catch (e) { - console.log(`[Pruner] Error pruning old subscriptions`, e); - } + stopWorker() { + clearTimeout(this.timer); + } + + async prune() { + const deleteAfterSeconds = await prefs.deleteAfter(); + const pruneThresholdTimestamp = Math.round(Date.now() / 1000) - deleteAfterSeconds; + if (deleteAfterSeconds === 0) { + console.log(`[Pruner] Pruning is disabled. Skipping.`); + return; } + console.log(`[Pruner] Pruning notifications older than ${deleteAfterSeconds}s (timestamp ${pruneThresholdTimestamp})`); + try { + await subscriptionManager.pruneNotifications(pruneThresholdTimestamp); + } catch (e) { + console.log(`[Pruner] Error pruning old subscriptions`, e); + } + } } const pruner = new Pruner(); diff --git a/web/src/app/Session.js b/web/src/app/Session.js index 45f48421..7464150c 100644 --- a/web/src/app/Session.js +++ b/web/src/app/Session.js @@ -1,30 +1,68 @@ +import Dexie from "dexie"; + +/** + * Manages the logged-in user's session and access token. + * The session replica is stored in IndexedDB so that the service worker can access it. + */ class Session { - store(username, token) { - localStorage.setItem("user", username); - localStorage.setItem("token", token); - } + constructor() { + const db = new Dexie("session-replica"); + db.version(1).stores({ + kv: "&key", + }); + this.db = db; - reset() { - localStorage.removeItem("user"); - localStorage.removeItem("token"); - } + // existing sessions (pre-v2.6.0) haven't called `store` with the session-replica, + // so attempt to sync any values from localStorage to IndexedDB + if (typeof localStorage !== "undefined" && this.exists()) { + const username = this.username(); + const token = this.token(); - resetAndRedirect(url) { - this.reset(); - window.location.href = url; + this.db.kv + .bulkPut([ + { key: "user", value: username }, + { key: "token", value: token }, + ]) + .then(() => { + console.log("[Session] Synced localStorage session to IndexedDB", { username }); + }) + .catch((e) => { + console.error("[Session] Failed to sync localStorage session to IndexedDB", e); + }); } + } - exists() { - return this.username() && this.token(); - } + async store(username, token) { + await this.db.kv.bulkPut([ + { key: "user", value: username }, + { key: "token", value: token }, + ]); + localStorage.setItem("user", username); + localStorage.setItem("token", token); + } - username() { - return localStorage.getItem("user"); - } + async resetAndRedirect(url) { + await this.db.delete(); + localStorage.removeItem("user"); + localStorage.removeItem("token"); + window.location.href = url; + } - token() { - return localStorage.getItem("token"); - } + async usernameAsync() { + return (await this.db.kv.get({ key: "user" }))?.value; + } + + exists() { + return this.username() && this.token(); + } + + username() { + return localStorage.getItem("user"); + } + + token() { + return localStorage.getItem("token"); + } } const session = new Session(); diff --git a/web/src/app/SubscriptionManager.js b/web/src/app/SubscriptionManager.js index cdfe50e2..de99b642 100644 --- a/web/src/app/SubscriptionManager.js +++ b/web/src/app/SubscriptionManager.js @@ -1,193 +1,262 @@ +import api from "./Api"; +import notifier from "./Notifier"; +import prefs from "./Prefs"; import db from "./db"; -import {topicUrl} from "./utils"; +import { topicUrl } from "./utils"; class SubscriptionManager { - /** All subscriptions, including "new count"; this is a JOIN, see https://dexie.org/docs/API-Reference#joining */ - async all() { - const subscriptions = await db.subscriptions.toArray(); - await Promise.all(subscriptions.map(async s => { - s.new = await db.notifications - .where({ subscriptionId: s.id, new: 1 }) - .count(); - })); - return subscriptions; + constructor(dbImpl) { + this.db = dbImpl; + } + + /** All subscriptions, including "new count"; this is a JOIN, see https://dexie.org/docs/API-Reference#joining */ + async all() { + const subscriptions = await this.db.subscriptions.toArray(); + return Promise.all( + subscriptions.map(async (s) => ({ + ...s, + new: await this.db.notifications.where({ subscriptionId: s.id, new: 1 }).count(), + })) + ); + } + + /** + * List of topics for which Web Push is enabled. This excludes (a) internal topics, (b) topics that are muted, + * and (c) topics from other hosts. Returns an empty list if Web Push is disabled. + * + * It is important to note that "mutedUntil" must be part of the where() query, otherwise the Dexie live query + * will not react to it, and the Web Push topics will not be updated when the user mutes a topic. + */ + async webPushTopics(pushPossible) { + if (!pushPossible) { + return []; } - async get(subscriptionId) { - return await db.subscriptions.get(subscriptionId) + // the Promise.resolve wrapper is not superfluous, without it the live query breaks: + // https://dexie.org/docs/dexie-react-hooks/useLiveQuery()#calling-non-dexie-apis-from-querier + const enabled = await Promise.resolve(prefs.webPushEnabled()); + if (!enabled) { + return []; } - async add(baseUrl, topic, internal) { - const id = topicUrl(baseUrl, topic); - const existingSubscription = await this.get(id); - if (existingSubscription) { - return existingSubscription; - } - const subscription = { - id: topicUrl(baseUrl, topic), - baseUrl: baseUrl, - topic: topic, - mutedUntil: 0, - last: null, - internal: internal || false - }; - await db.subscriptions.put(subscription); - return subscription; + const subscriptions = await this.db.subscriptions.where({ baseUrl: config.base_url, mutedUntil: 0 }).toArray(); + return subscriptions.filter(({ internal }) => !internal).map(({ topic }) => topic); + } + + async get(subscriptionId) { + return this.db.subscriptions.get(subscriptionId); + } + + async notify(subscriptionId, notification) { + const subscription = await this.get(subscriptionId); + if (subscription.mutedUntil > 0) { + return; } - async syncFromRemote(remoteSubscriptions, remoteReservations) { - console.log(`[SubscriptionManager] Syncing subscriptions from remote`, remoteSubscriptions); - - // Add remote subscriptions - let remoteIds = []; // = topicUrl(baseUrl, topic) - for (let i = 0; i < remoteSubscriptions.length; i++) { - const remote = remoteSubscriptions[i]; - const local = await this.add(remote.base_url, remote.topic, false); - const reservation = remoteReservations?.find(r => remote.base_url === config.base_url && remote.topic === r.topic) || null; - await this.update(local.id, { - displayName: remote.display_name, // May be undefined - reservation: reservation // May be null! - }); - remoteIds.push(local.id); - } - - // Remove local subscriptions that do not exist remotely - const localSubscriptions = await db.subscriptions.toArray(); - for (let i = 0; i < localSubscriptions.length; i++) { - const local = localSubscriptions[i]; - const remoteExists = remoteIds.includes(local.id); - if (!local.internal && !remoteExists) { - await this.remove(local.id); - } - } + const priority = notification.priority ?? 3; + if (priority < (await prefs.minPriority())) { + return; } - async updateState(subscriptionId, state) { - db.subscriptions.update(subscriptionId, { state: state }); + await notifier.notify(subscription, notification); + } + + /** + * @param {string} baseUrl + * @param {string} topic + * @param {object} opts + * @param {boolean} opts.internal + * @returns + */ + async add(baseUrl, topic, opts = {}) { + const id = topicUrl(baseUrl, topic); + + const existingSubscription = await this.get(id); + if (existingSubscription) { + return existingSubscription; } - async remove(subscriptionId) { - await db.subscriptions.delete(subscriptionId); - await db.notifications - .where({subscriptionId: subscriptionId}) - .delete(); - } + const subscription = { + ...opts, + id: topicUrl(baseUrl, topic), + baseUrl, + topic, + mutedUntil: 0, + last: null, + }; - async first() { - return db.subscriptions.toCollection().first(); // May be undefined - } + await this.db.subscriptions.put(subscription); - async getNotifications(subscriptionId) { - // This is quite awkward, but it is the recommended approach as per the Dexie docs. - // It's actually fine, because the reading and filtering is quite fast. The rendering is what's - // killing performance. See https://dexie.org/docs/Collection/Collection.offset()#a-better-paging-approach + return subscription; + } - return db.notifications - .orderBy("time") // Sort by time first - .filter(n => n.subscriptionId === subscriptionId) - .reverse() - .toArray(); - } + async syncFromRemote(remoteSubscriptions, remoteReservations) { + console.log(`[SubscriptionManager] Syncing subscriptions from remote`, remoteSubscriptions); - async getAllNotifications() { - return db.notifications - .orderBy("time") // Efficient, see docs - .reverse() - .toArray(); - } + // Add remote subscriptions + const remoteIds = await Promise.all( + remoteSubscriptions.map(async (remote) => { + const reservation = remoteReservations?.find((r) => remote.base_url === config.base_url && remote.topic === r.topic) || null; - /** Adds notification, or returns false if it already exists */ - async addNotification(subscriptionId, notification) { - const exists = await db.notifications.get(notification.id); - if (exists) { - return false; - } - try { - notification.new = 1; // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation - await db.notifications.add({ ...notification, subscriptionId }); // FIXME consider put() for double tab - await db.subscriptions.update(subscriptionId, { - last: notification.id - }); - } catch (e) { - console.error(`[SubscriptionManager] Error adding notification`, e); - } - return true; - } - - /** Adds/replaces notifications, will not throw if they exist */ - async addNotifications(subscriptionId, notifications) { - const notificationsWithSubscriptionId = notifications - .map(notification => ({ ...notification, subscriptionId })); - const lastNotificationId = notifications.at(-1).id; - await db.notifications.bulkPut(notificationsWithSubscriptionId); - await db.subscriptions.update(subscriptionId, { - last: lastNotificationId + const local = await this.add(remote.base_url, remote.topic, { + displayName: remote.display_name, // May be undefined + reservation, // May be null! }); - } - async updateNotification(notification) { - const exists = await db.notifications.get(notification.id); - if (!exists) { - return false; + return local.id; + }) + ); + + // Remove local subscriptions that do not exist remotely + const localSubscriptions = await this.db.subscriptions.toArray(); + + await Promise.all( + localSubscriptions.map(async (local) => { + const remoteExists = remoteIds.includes(local.id); + if (!local.internal && !remoteExists) { + await this.remove(local); } - try { - await db.notifications.put({ ...notification }); - } catch (e) { - console.error(`[SubscriptionManager] Error updating notification`, e); - } - return true; + }) + ); + } + + async updateWebPushSubscriptions(topics) { + const hasWebPushTopics = topics.length > 0; + const browserSubscription = await notifier.webPushSubscription(hasWebPushTopics); + + if (!browserSubscription) { + console.log( + "[SubscriptionManager] No browser subscription currently exists, so web push was never enabled or the notification permission was removed. Skipping." + ); + return; } - async deleteNotification(notificationId) { - await db.notifications.delete(notificationId); + if (hasWebPushTopics) { + await api.updateWebPush(browserSubscription, topics); + } else { + await api.deleteWebPush(browserSubscription); } + } - async deleteNotifications(subscriptionId) { - await db.notifications - .where({subscriptionId: subscriptionId}) - .delete(); - } + async updateState(subscriptionId, state) { + this.db.subscriptions.update(subscriptionId, { state }); + } - async markNotificationRead(notificationId) { - await db.notifications - .where({id: notificationId}) - .modify({new: 0}); - } + async remove(subscription) { + await this.db.subscriptions.delete(subscription.id); + await this.db.notifications.where({ subscriptionId: subscription.id }).delete(); + } - async markNotificationsRead(subscriptionId) { - await db.notifications - .where({subscriptionId: subscriptionId, new: 1}) - .modify({new: 0}); - } + async first() { + return this.db.subscriptions.toCollection().first(); // May be undefined + } - async setMutedUntil(subscriptionId, mutedUntil) { - await db.subscriptions.update(subscriptionId, { - mutedUntil: mutedUntil - }); - } + async getNotifications(subscriptionId) { + // This is quite awkward, but it is the recommended approach as per the Dexie docs. + // It's actually fine, because the reading and filtering is quite fast. The rendering is what's + // killing performance. See https://dexie.org/docs/Collection/Collection.offset()#a-better-paging-approach - async setDisplayName(subscriptionId, displayName) { - await db.subscriptions.update(subscriptionId, { - displayName: displayName - }); - } + return this.db.notifications + .orderBy("time") // Sort by time first + .filter((n) => n.subscriptionId === subscriptionId) + .reverse() + .toArray(); + } - async setReservation(subscriptionId, reservation) { - await db.subscriptions.update(subscriptionId, { - reservation: reservation - }); - } + async getAllNotifications() { + return this.db.notifications + .orderBy("time") // Efficient, see docs + .reverse() + .toArray(); + } - async update(subscriptionId, params) { - await db.subscriptions.update(subscriptionId, params); + /** Adds notification, or returns false if it already exists */ + async addNotification(subscriptionId, notification) { + const exists = await this.db.notifications.get(notification.id); + if (exists) { + return false; } + try { + // sw.js duplicates this logic, so if you change it here, change it there too + await this.db.notifications.add({ + ...notification, + subscriptionId, + // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation + new: 1, + }); // FIXME consider put() for double tab + await this.db.subscriptions.update(subscriptionId, { + last: notification.id, + }); + } catch (e) { + console.error(`[SubscriptionManager] Error adding notification`, e); + } + return true; + } - async pruneNotifications(thresholdTimestamp) { - await db.notifications - .where("time").below(thresholdTimestamp) - .delete(); + /** Adds/replaces notifications, will not throw if they exist */ + async addNotifications(subscriptionId, notifications) { + const notificationsWithSubscriptionId = notifications.map((notification) => ({ ...notification, subscriptionId })); + const lastNotificationId = notifications.at(-1).id; + await this.db.notifications.bulkPut(notificationsWithSubscriptionId); + await this.db.subscriptions.update(subscriptionId, { + last: lastNotificationId, + }); + } + + async updateNotification(notification) { + const exists = await this.db.notifications.get(notification.id); + if (!exists) { + return false; } + try { + await this.db.notifications.put({ ...notification }); + } catch (e) { + console.error(`[SubscriptionManager] Error updating notification`, e); + } + return true; + } + + async deleteNotification(notificationId) { + await this.db.notifications.delete(notificationId); + } + + async deleteNotifications(subscriptionId) { + await this.db.notifications.where({ subscriptionId }).delete(); + } + + async markNotificationRead(notificationId) { + await this.db.notifications.where({ id: notificationId }).modify({ new: 0 }); + } + + async markNotificationsRead(subscriptionId) { + await this.db.notifications.where({ subscriptionId, new: 1 }).modify({ new: 0 }); + } + + async setMutedUntil(subscriptionId, mutedUntil) { + await this.db.subscriptions.update(subscriptionId, { + mutedUntil, + }); + } + + async setDisplayName(subscriptionId, displayName) { + await this.db.subscriptions.update(subscriptionId, { + displayName, + }); + } + + async setReservation(subscriptionId, reservation) { + await this.db.subscriptions.update(subscriptionId, { + reservation, + }); + } + + async update(subscriptionId, params) { + await this.db.subscriptions.update(subscriptionId, params); + } + + async pruneNotifications(thresholdTimestamp) { + await this.db.notifications.where("time").below(thresholdTimestamp).delete(); + } } -const subscriptionManager = new SubscriptionManager(); -export default subscriptionManager; +export default new SubscriptionManager(db()); diff --git a/web/src/app/UserManager.js b/web/src/app/UserManager.js index 1e54eb0a..b53b1da8 100644 --- a/web/src/app/UserManager.js +++ b/web/src/app/UserManager.js @@ -2,46 +2,49 @@ import db from "./db"; import session from "./Session"; class UserManager { - async all() { - const users = await db.users.toArray(); - if (session.exists()) { - users.unshift(this.localUser()); - } - return users; - } + constructor(dbImpl) { + this.db = dbImpl; + } - async get(baseUrl) { - if (session.exists() && baseUrl === config.base_url) { - return this.localUser(); - } - return db.users.get(baseUrl); + async all() { + const users = await this.db.users.toArray(); + if (session.exists()) { + users.unshift(this.localUser()); } + return users; + } - async save(user) { - if (session.exists() && user.baseUrl === config.base_url) { - return; - } - await db.users.put(user); + async get(baseUrl) { + if (session.exists() && baseUrl === config.base_url) { + return this.localUser(); } + return this.db.users.get(baseUrl); + } - async delete(baseUrl) { - if (session.exists() && baseUrl === config.base_url) { - return; - } - await db.users.delete(baseUrl); + async save(user) { + if (session.exists() && user.baseUrl === config.base_url) { + return; } + await this.db.users.put(user); + } - localUser() { - if (!session.exists()) { - return null; - } - return { - baseUrl: config.base_url, - username: session.username(), - token: session.token() // Not "password"! - }; + async delete(baseUrl) { + if (session.exists() && baseUrl === config.base_url) { + return; } + await this.db.users.delete(baseUrl); + } + + localUser() { + if (!session.exists()) { + return null; + } + return { + baseUrl: config.base_url, + username: session.username(), + token: session.token(), // Not "password"! + }; + } } -const userManager = new UserManager(); -export default userManager; +export default new UserManager(db()); diff --git a/web/src/app/config.js b/web/src/app/config.js index bdec53ed..24e86f3a 100644 --- a/web/src/app/config.js +++ b/web/src/app/config.js @@ -1,9 +1,9 @@ -const config = window.config; +const { config } = window; // The backend returns an empty base_url for the config struct, // so the frontend (hey, that's us!) can use the current location. if (!config.base_url || config.base_url === "") { - config.base_url = window.location.origin; + config.base_url = window.location.origin; } export default config; diff --git a/web/src/app/db.js b/web/src/app/db.js index 564ee1ce..b28fb716 100644 --- a/web/src/app/db.js +++ b/web/src/app/db.js @@ -1,4 +1,4 @@ -import Dexie from 'dexie'; +import Dexie from "dexie"; import session from "./Session"; // Uses Dexie.js @@ -7,15 +7,25 @@ import session from "./Session"; // Notes: // - As per docs, we only declare the indexable columns, not all columns -// The IndexedDB database name is based on the logged-in user -const dbName = (session.username()) ? `ntfy-${session.username()}` : "ntfy"; -const db = new Dexie(dbName); +const createDatabase = (username) => { + const dbName = username ? `ntfy-${username}` : "ntfy"; // IndexedDB database is based on the logged-in user + const db = new Dexie(dbName); -db.version(1).stores({ - subscriptions: '&id,baseUrl', - notifications: '&id,subscriptionId,time,new,[subscriptionId+new]', // compound key for query performance - users: '&baseUrl,username', - prefs: '&key' -}); + db.version(2).stores({ + subscriptions: "&id,baseUrl,[baseUrl+mutedUntil]", + notifications: "&id,subscriptionId,time,new,[subscriptionId+new]", // compound key for query performance + users: "&baseUrl,username", + prefs: "&key", + }); + + return db; +}; + +export const dbAsync = async () => { + const username = await session.usernameAsync(); + return createDatabase(username); +}; + +const db = () => createDatabase(session.username()); export default db; diff --git a/web/src/app/emojis.js b/web/src/app/emojis.js index f6dac7b1..b7912c35 100644 --- a/web/src/app/emojis.js +++ b/web/src/app/emojis.js @@ -1,3 +1,14500 @@ // This file is generated by scripts/emoji-convert.sh to reduce the size // Original data source: https://github.com/github/gemoji/blob/master/db/emoji.json -export const rawEmojis = [{"emoji":"😀","aliases":["grinning"],"tags":["smile","happy"],"category":"Smileys & Emotion","description":"grinning face","unicode_version":"6.1"},{"emoji":"😃","aliases":["smiley"],"tags":["happy","joy","haha"],"category":"Smileys & Emotion","description":"grinning face with big eyes","unicode_version":"6.0"},{"emoji":"😄","aliases":["smile"],"tags":["happy","joy","laugh","pleased"],"category":"Smileys & Emotion","description":"grinning face with smiling eyes","unicode_version":"6.0"},{"emoji":"😁","aliases":["grin"],"tags":[],"category":"Smileys & Emotion","description":"beaming face with smiling eyes","unicode_version":"6.0"},{"emoji":"😆","aliases":["laughing","satisfied"],"tags":["happy","haha"],"category":"Smileys & Emotion","description":"grinning squinting face","unicode_version":"6.0"},{"emoji":"😅","aliases":["sweat_smile"],"tags":["hot"],"category":"Smileys & Emotion","description":"grinning face with sweat","unicode_version":"6.0"},{"emoji":"🤣","aliases":["rofl"],"tags":["lol","laughing"],"category":"Smileys & Emotion","description":"rolling on the floor laughing","unicode_version":"9.0"},{"emoji":"😂","aliases":["joy"],"tags":["tears"],"category":"Smileys & Emotion","description":"face with tears of joy","unicode_version":"6.0"},{"emoji":"🙂","aliases":["slightly_smiling_face"],"tags":[],"category":"Smileys & Emotion","description":"slightly smiling face","unicode_version":"7.0"},{"emoji":"🙃","aliases":["upside_down_face"],"tags":[],"category":"Smileys & Emotion","description":"upside-down face","unicode_version":"8.0"},{"emoji":"😉","aliases":["wink"],"tags":["flirt"],"category":"Smileys & Emotion","description":"winking face","unicode_version":"6.0"},{"emoji":"😊","aliases":["blush"],"tags":["proud"],"category":"Smileys & Emotion","description":"smiling face with smiling eyes","unicode_version":"6.0"},{"emoji":"😇","aliases":["innocent"],"tags":["angel"],"category":"Smileys & Emotion","description":"smiling face with halo","unicode_version":"6.0"},{"emoji":"🥰","aliases":["smiling_face_with_three_hearts"],"tags":["love"],"category":"Smileys & Emotion","description":"smiling face with hearts","unicode_version":"11.0"},{"emoji":"😍","aliases":["heart_eyes"],"tags":["love","crush"],"category":"Smileys & Emotion","description":"smiling face with heart-eyes","unicode_version":"6.0"},{"emoji":"🤩","aliases":["star_struck"],"tags":["eyes"],"category":"Smileys & Emotion","description":"star-struck","unicode_version":"11.0"},{"emoji":"😘","aliases":["kissing_heart"],"tags":["flirt"],"category":"Smileys & Emotion","description":"face blowing a kiss","unicode_version":"6.0"},{"emoji":"😗","aliases":["kissing"],"tags":[],"category":"Smileys & Emotion","description":"kissing face","unicode_version":"6.1"},{"emoji":"☺️","aliases":["relaxed"],"tags":["blush","pleased"],"category":"Smileys & Emotion","description":"smiling face","unicode_version":""},{"emoji":"😚","aliases":["kissing_closed_eyes"],"tags":[],"category":"Smileys & Emotion","description":"kissing face with closed eyes","unicode_version":"6.0"},{"emoji":"😙","aliases":["kissing_smiling_eyes"],"tags":[],"category":"Smileys & Emotion","description":"kissing face with smiling eyes","unicode_version":"6.1"},{"emoji":"🥲","aliases":["smiling_face_with_tear"],"tags":[],"category":"Smileys & Emotion","description":"smiling face with tear","unicode_version":"13.0"},{"emoji":"😋","aliases":["yum"],"tags":["tongue","lick"],"category":"Smileys & Emotion","description":"face savoring food","unicode_version":"6.0"},{"emoji":"😛","aliases":["stuck_out_tongue"],"tags":[],"category":"Smileys & Emotion","description":"face with tongue","unicode_version":"6.1"},{"emoji":"😜","aliases":["stuck_out_tongue_winking_eye"],"tags":["prank","silly"],"category":"Smileys & Emotion","description":"winking face with tongue","unicode_version":"6.0"},{"emoji":"🤪","aliases":["zany_face"],"tags":["goofy","wacky"],"category":"Smileys & Emotion","description":"zany face","unicode_version":"11.0"},{"emoji":"😝","aliases":["stuck_out_tongue_closed_eyes"],"tags":["prank"],"category":"Smileys & Emotion","description":"squinting face with tongue","unicode_version":"6.0"},{"emoji":"🤑","aliases":["money_mouth_face"],"tags":["rich"],"category":"Smileys & Emotion","description":"money-mouth face","unicode_version":"8.0"},{"emoji":"🤗","aliases":["hugs"],"tags":[],"category":"Smileys & Emotion","description":"hugging face","unicode_version":"8.0"},{"emoji":"🤭","aliases":["hand_over_mouth"],"tags":["quiet","whoops"],"category":"Smileys & Emotion","description":"face with hand over mouth","unicode_version":"11.0"},{"emoji":"🤫","aliases":["shushing_face"],"tags":["silence","quiet"],"category":"Smileys & Emotion","description":"shushing face","unicode_version":"11.0"},{"emoji":"🤔","aliases":["thinking"],"tags":[],"category":"Smileys & Emotion","description":"thinking face","unicode_version":"8.0"},{"emoji":"🤐","aliases":["zipper_mouth_face"],"tags":["silence","hush"],"category":"Smileys & Emotion","description":"zipper-mouth face","unicode_version":"8.0"},{"emoji":"🤨","aliases":["raised_eyebrow"],"tags":["suspicious"],"category":"Smileys & Emotion","description":"face with raised eyebrow","unicode_version":"11.0"},{"emoji":"😐","aliases":["neutral_face"],"tags":["meh"],"category":"Smileys & Emotion","description":"neutral face","unicode_version":"6.0"},{"emoji":"😑","aliases":["expressionless"],"tags":[],"category":"Smileys & Emotion","description":"expressionless face","unicode_version":"6.1"},{"emoji":"😶","aliases":["no_mouth"],"tags":["mute","silence"],"category":"Smileys & Emotion","description":"face without mouth","unicode_version":"6.0"},{"emoji":"😶‍🌫️","aliases":["face_in_clouds"],"tags":[],"category":"Smileys & Emotion","description":"face in clouds","unicode_version":"13.1"},{"emoji":"😏","aliases":["smirk"],"tags":["smug"],"category":"Smileys & Emotion","description":"smirking face","unicode_version":"6.0"},{"emoji":"😒","aliases":["unamused"],"tags":["meh"],"category":"Smileys & Emotion","description":"unamused face","unicode_version":"6.0"},{"emoji":"🙄","aliases":["roll_eyes"],"tags":[],"category":"Smileys & Emotion","description":"face with rolling eyes","unicode_version":"8.0"},{"emoji":"😬","aliases":["grimacing"],"tags":[],"category":"Smileys & Emotion","description":"grimacing face","unicode_version":"6.1"},{"emoji":"😮‍💨","aliases":["face_exhaling"],"tags":[],"category":"Smileys & Emotion","description":"face exhaling","unicode_version":"13.1"},{"emoji":"🤥","aliases":["lying_face"],"tags":["liar"],"category":"Smileys & Emotion","description":"lying face","unicode_version":"9.0"},{"emoji":"😌","aliases":["relieved"],"tags":["whew"],"category":"Smileys & Emotion","description":"relieved face","unicode_version":"6.0"},{"emoji":"😔","aliases":["pensive"],"tags":[],"category":"Smileys & Emotion","description":"pensive face","unicode_version":"6.0"},{"emoji":"😪","aliases":["sleepy"],"tags":["tired"],"category":"Smileys & Emotion","description":"sleepy face","unicode_version":"6.0"},{"emoji":"🤤","aliases":["drooling_face"],"tags":[],"category":"Smileys & Emotion","description":"drooling face","unicode_version":"9.0"},{"emoji":"😴","aliases":["sleeping"],"tags":["zzz"],"category":"Smileys & Emotion","description":"sleeping face","unicode_version":"6.1"},{"emoji":"😷","aliases":["mask"],"tags":["sick","ill"],"category":"Smileys & Emotion","description":"face with medical mask","unicode_version":"6.0"},{"emoji":"🤒","aliases":["face_with_thermometer"],"tags":["sick"],"category":"Smileys & Emotion","description":"face with thermometer","unicode_version":"8.0"},{"emoji":"🤕","aliases":["face_with_head_bandage"],"tags":["hurt"],"category":"Smileys & Emotion","description":"face with head-bandage","unicode_version":"8.0"},{"emoji":"🤢","aliases":["nauseated_face"],"tags":["sick","barf","disgusted"],"category":"Smileys & Emotion","description":"nauseated face","unicode_version":"9.0"},{"emoji":"🤮","aliases":["vomiting_face"],"tags":["barf","sick"],"category":"Smileys & Emotion","description":"face vomiting","unicode_version":"11.0"},{"emoji":"🤧","aliases":["sneezing_face"],"tags":["achoo","sick"],"category":"Smileys & Emotion","description":"sneezing face","unicode_version":"9.0"},{"emoji":"🥵","aliases":["hot_face"],"tags":["heat","sweating"],"category":"Smileys & Emotion","description":"hot face","unicode_version":"11.0"},{"emoji":"🥶","aliases":["cold_face"],"tags":["freezing","ice"],"category":"Smileys & Emotion","description":"cold face","unicode_version":"11.0"},{"emoji":"🥴","aliases":["woozy_face"],"tags":["groggy"],"category":"Smileys & Emotion","description":"woozy face","unicode_version":"11.0"},{"emoji":"😵","aliases":["dizzy_face"],"tags":[],"category":"Smileys & Emotion","description":"knocked-out face","unicode_version":"6.0"},{"emoji":"😵‍💫","aliases":["face_with_spiral_eyes"],"tags":[],"category":"Smileys & Emotion","description":"face with spiral eyes","unicode_version":"13.1"},{"emoji":"🤯","aliases":["exploding_head"],"tags":["mind","blown"],"category":"Smileys & Emotion","description":"exploding head","unicode_version":"11.0"},{"emoji":"🤠","aliases":["cowboy_hat_face"],"tags":[],"category":"Smileys & Emotion","description":"cowboy hat face","unicode_version":"9.0"},{"emoji":"🥳","aliases":["partying_face"],"tags":["celebration","birthday"],"category":"Smileys & Emotion","description":"partying face","unicode_version":"11.0"},{"emoji":"🥸","aliases":["disguised_face"],"tags":[],"category":"Smileys & Emotion","description":"disguised face","unicode_version":"13.0"},{"emoji":"😎","aliases":["sunglasses"],"tags":["cool"],"category":"Smileys & Emotion","description":"smiling face with sunglasses","unicode_version":"6.0"},{"emoji":"🤓","aliases":["nerd_face"],"tags":["geek","glasses"],"category":"Smileys & Emotion","description":"nerd face","unicode_version":"8.0"},{"emoji":"🧐","aliases":["monocle_face"],"tags":[],"category":"Smileys & Emotion","description":"face with monocle","unicode_version":"11.0"},{"emoji":"😕","aliases":["confused"],"tags":[],"category":"Smileys & Emotion","description":"confused face","unicode_version":"6.1"},{"emoji":"😟","aliases":["worried"],"tags":["nervous"],"category":"Smileys & Emotion","description":"worried face","unicode_version":"6.1"},{"emoji":"🙁","aliases":["slightly_frowning_face"],"tags":[],"category":"Smileys & Emotion","description":"slightly frowning face","unicode_version":"7.0"},{"emoji":"☹️","aliases":["frowning_face"],"tags":[],"category":"Smileys & Emotion","description":"frowning face","unicode_version":""},{"emoji":"😮","aliases":["open_mouth"],"tags":["surprise","impressed","wow"],"category":"Smileys & Emotion","description":"face with open mouth","unicode_version":"6.1"},{"emoji":"😯","aliases":["hushed"],"tags":["silence","speechless"],"category":"Smileys & Emotion","description":"hushed face","unicode_version":"6.1"},{"emoji":"😲","aliases":["astonished"],"tags":["amazed","gasp"],"category":"Smileys & Emotion","description":"astonished face","unicode_version":"6.0"},{"emoji":"😳","aliases":["flushed"],"tags":[],"category":"Smileys & Emotion","description":"flushed face","unicode_version":"6.0"},{"emoji":"🥺","aliases":["pleading_face"],"tags":["puppy","eyes"],"category":"Smileys & Emotion","description":"pleading face","unicode_version":"11.0"},{"emoji":"😦","aliases":["frowning"],"tags":[],"category":"Smileys & Emotion","description":"frowning face with open mouth","unicode_version":"6.1"},{"emoji":"😧","aliases":["anguished"],"tags":["stunned"],"category":"Smileys & Emotion","description":"anguished face","unicode_version":"6.1"},{"emoji":"😨","aliases":["fearful"],"tags":["scared","shocked","oops"],"category":"Smileys & Emotion","description":"fearful face","unicode_version":"6.0"},{"emoji":"😰","aliases":["cold_sweat"],"tags":["nervous"],"category":"Smileys & Emotion","description":"anxious face with sweat","unicode_version":"6.0"},{"emoji":"😥","aliases":["disappointed_relieved"],"tags":["phew","sweat","nervous"],"category":"Smileys & Emotion","description":"sad but relieved face","unicode_version":"6.0"},{"emoji":"😢","aliases":["cry"],"tags":["sad","tear"],"category":"Smileys & Emotion","description":"crying face","unicode_version":"6.0"},{"emoji":"😭","aliases":["sob"],"tags":["sad","cry","bawling"],"category":"Smileys & Emotion","description":"loudly crying face","unicode_version":"6.0"},{"emoji":"😱","aliases":["scream"],"tags":["horror","shocked"],"category":"Smileys & Emotion","description":"face screaming in fear","unicode_version":"6.0"},{"emoji":"😖","aliases":["confounded"],"tags":[],"category":"Smileys & Emotion","description":"confounded face","unicode_version":"6.0"},{"emoji":"😣","aliases":["persevere"],"tags":["struggling"],"category":"Smileys & Emotion","description":"persevering face","unicode_version":"6.0"},{"emoji":"😞","aliases":["disappointed"],"tags":["sad"],"category":"Smileys & Emotion","description":"disappointed face","unicode_version":"6.0"},{"emoji":"😓","aliases":["sweat"],"tags":[],"category":"Smileys & Emotion","description":"downcast face with sweat","unicode_version":"6.0"},{"emoji":"😩","aliases":["weary"],"tags":["tired"],"category":"Smileys & Emotion","description":"weary face","unicode_version":"6.0"},{"emoji":"😫","aliases":["tired_face"],"tags":["upset","whine"],"category":"Smileys & Emotion","description":"tired face","unicode_version":"6.0"},{"emoji":"🥱","aliases":["yawning_face"],"tags":[],"category":"Smileys & Emotion","description":"yawning face","unicode_version":"12.0"},{"emoji":"😤","aliases":["triumph"],"tags":["smug"],"category":"Smileys & Emotion","description":"face with steam from nose","unicode_version":"6.0"},{"emoji":"😡","aliases":["rage","pout"],"tags":["angry"],"category":"Smileys & Emotion","description":"pouting face","unicode_version":"6.0"},{"emoji":"😠","aliases":["angry"],"tags":["mad","annoyed"],"category":"Smileys & Emotion","description":"angry face","unicode_version":"6.0"},{"emoji":"🤬","aliases":["cursing_face"],"tags":["foul"],"category":"Smileys & Emotion","description":"face with symbols on mouth","unicode_version":"11.0"},{"emoji":"😈","aliases":["smiling_imp"],"tags":["devil","evil","horns"],"category":"Smileys & Emotion","description":"smiling face with horns","unicode_version":"6.0"},{"emoji":"👿","aliases":["imp"],"tags":["angry","devil","evil","horns"],"category":"Smileys & Emotion","description":"angry face with horns","unicode_version":"6.0"},{"emoji":"💀","aliases":["skull"],"tags":["dead","danger","poison"],"category":"Smileys & Emotion","description":"skull","unicode_version":"6.0"},{"emoji":"☠️","aliases":["skull_and_crossbones"],"tags":["danger","pirate"],"category":"Smileys & Emotion","description":"skull and crossbones","unicode_version":""},{"emoji":"💩","aliases":["hankey","poop","shit"],"tags":["crap"],"category":"Smileys & Emotion","description":"pile of poo","unicode_version":"6.0"},{"emoji":"🤡","aliases":["clown_face"],"tags":[],"category":"Smileys & Emotion","description":"clown face","unicode_version":"9.0"},{"emoji":"👹","aliases":["japanese_ogre"],"tags":["monster"],"category":"Smileys & Emotion","description":"ogre","unicode_version":"6.0"},{"emoji":"👺","aliases":["japanese_goblin"],"tags":[],"category":"Smileys & Emotion","description":"goblin","unicode_version":"6.0"},{"emoji":"👻","aliases":["ghost"],"tags":["halloween"],"category":"Smileys & Emotion","description":"ghost","unicode_version":"6.0"},{"emoji":"👽","aliases":["alien"],"tags":["ufo"],"category":"Smileys & Emotion","description":"alien","unicode_version":"6.0"},{"emoji":"👾","aliases":["space_invader"],"tags":["game","retro"],"category":"Smileys & Emotion","description":"alien monster","unicode_version":"6.0"},{"emoji":"🤖","aliases":["robot"],"tags":[],"category":"Smileys & Emotion","description":"robot","unicode_version":"8.0"},{"emoji":"😺","aliases":["smiley_cat"],"tags":[],"category":"Smileys & Emotion","description":"grinning cat","unicode_version":"6.0"},{"emoji":"😸","aliases":["smile_cat"],"tags":[],"category":"Smileys & Emotion","description":"grinning cat with smiling eyes","unicode_version":"6.0"},{"emoji":"😹","aliases":["joy_cat"],"tags":[],"category":"Smileys & Emotion","description":"cat with tears of joy","unicode_version":"6.0"},{"emoji":"😻","aliases":["heart_eyes_cat"],"tags":[],"category":"Smileys & Emotion","description":"smiling cat with heart-eyes","unicode_version":"6.0"},{"emoji":"😼","aliases":["smirk_cat"],"tags":[],"category":"Smileys & Emotion","description":"cat with wry smile","unicode_version":"6.0"},{"emoji":"😽","aliases":["kissing_cat"],"tags":[],"category":"Smileys & Emotion","description":"kissing cat","unicode_version":"6.0"},{"emoji":"🙀","aliases":["scream_cat"],"tags":["horror"],"category":"Smileys & Emotion","description":"weary cat","unicode_version":"6.0"},{"emoji":"😿","aliases":["crying_cat_face"],"tags":["sad","tear"],"category":"Smileys & Emotion","description":"crying cat","unicode_version":"6.0"},{"emoji":"😾","aliases":["pouting_cat"],"tags":[],"category":"Smileys & Emotion","description":"pouting cat","unicode_version":"6.0"},{"emoji":"🙈","aliases":["see_no_evil"],"tags":["monkey","blind","ignore"],"category":"Smileys & Emotion","description":"see-no-evil monkey","unicode_version":"6.0"},{"emoji":"🙉","aliases":["hear_no_evil"],"tags":["monkey","deaf"],"category":"Smileys & Emotion","description":"hear-no-evil monkey","unicode_version":"6.0"},{"emoji":"🙊","aliases":["speak_no_evil"],"tags":["monkey","mute","hush"],"category":"Smileys & Emotion","description":"speak-no-evil monkey","unicode_version":"6.0"},{"emoji":"💋","aliases":["kiss"],"tags":["lipstick"],"category":"Smileys & Emotion","description":"kiss mark","unicode_version":"6.0"},{"emoji":"💌","aliases":["love_letter"],"tags":["email","envelope"],"category":"Smileys & Emotion","description":"love letter","unicode_version":"6.0"},{"emoji":"💘","aliases":["cupid"],"tags":["love","heart"],"category":"Smileys & Emotion","description":"heart with arrow","unicode_version":"6.0"},{"emoji":"💝","aliases":["gift_heart"],"tags":["chocolates"],"category":"Smileys & Emotion","description":"heart with ribbon","unicode_version":"6.0"},{"emoji":"💖","aliases":["sparkling_heart"],"tags":[],"category":"Smileys & Emotion","description":"sparkling heart","unicode_version":"6.0"},{"emoji":"💗","aliases":["heartpulse"],"tags":[],"category":"Smileys & Emotion","description":"growing heart","unicode_version":"6.0"},{"emoji":"💓","aliases":["heartbeat"],"tags":[],"category":"Smileys & Emotion","description":"beating heart","unicode_version":"6.0"},{"emoji":"💞","aliases":["revolving_hearts"],"tags":[],"category":"Smileys & Emotion","description":"revolving hearts","unicode_version":"6.0"},{"emoji":"💕","aliases":["two_hearts"],"tags":[],"category":"Smileys & Emotion","description":"two hearts","unicode_version":"6.0"},{"emoji":"💟","aliases":["heart_decoration"],"tags":[],"category":"Smileys & Emotion","description":"heart decoration","unicode_version":"6.0"},{"emoji":"❣️","aliases":["heavy_heart_exclamation"],"tags":[],"category":"Smileys & Emotion","description":"heart exclamation","unicode_version":""},{"emoji":"💔","aliases":["broken_heart"],"tags":[],"category":"Smileys & Emotion","description":"broken heart","unicode_version":"6.0"},{"emoji":"❤️‍🔥","aliases":["heart_on_fire"],"tags":[],"category":"Smileys & Emotion","description":"heart on fire","unicode_version":"13.1"},{"emoji":"❤️‍🩹","aliases":["mending_heart"],"tags":[],"category":"Smileys & Emotion","description":"mending heart","unicode_version":"13.1"},{"emoji":"❤️","aliases":["heart"],"tags":["love"],"category":"Smileys & Emotion","description":"red heart","unicode_version":""},{"emoji":"🧡","aliases":["orange_heart"],"tags":[],"category":"Smileys & Emotion","description":"orange heart","unicode_version":"11.0"},{"emoji":"💛","aliases":["yellow_heart"],"tags":[],"category":"Smileys & Emotion","description":"yellow heart","unicode_version":"6.0"},{"emoji":"💚","aliases":["green_heart"],"tags":[],"category":"Smileys & Emotion","description":"green heart","unicode_version":"6.0"},{"emoji":"💙","aliases":["blue_heart"],"tags":[],"category":"Smileys & Emotion","description":"blue heart","unicode_version":"6.0"},{"emoji":"💜","aliases":["purple_heart"],"tags":[],"category":"Smileys & Emotion","description":"purple heart","unicode_version":"6.0"},{"emoji":"🤎","aliases":["brown_heart"],"tags":[],"category":"Smileys & Emotion","description":"brown heart","unicode_version":"12.0"},{"emoji":"🖤","aliases":["black_heart"],"tags":[],"category":"Smileys & Emotion","description":"black heart","unicode_version":"9.0"},{"emoji":"🤍","aliases":["white_heart"],"tags":[],"category":"Smileys & Emotion","description":"white heart","unicode_version":"12.0"},{"emoji":"💯","aliases":["100"],"tags":["score","perfect"],"category":"Smileys & Emotion","description":"hundred points","unicode_version":"6.0"},{"emoji":"💢","aliases":["anger"],"tags":["angry"],"category":"Smileys & Emotion","description":"anger symbol","unicode_version":"6.0"},{"emoji":"💥","aliases":["boom","collision"],"tags":["explode"],"category":"Smileys & Emotion","description":"collision","unicode_version":"6.0"},{"emoji":"💫","aliases":["dizzy"],"tags":["star"],"category":"Smileys & Emotion","description":"dizzy","unicode_version":"6.0"},{"emoji":"💦","aliases":["sweat_drops"],"tags":["water","workout"],"category":"Smileys & Emotion","description":"sweat droplets","unicode_version":"6.0"},{"emoji":"💨","aliases":["dash"],"tags":["wind","blow","fast"],"category":"Smileys & Emotion","description":"dashing away","unicode_version":"6.0"},{"emoji":"🕳️","aliases":["hole"],"tags":[],"category":"Smileys & Emotion","description":"hole","unicode_version":"7.0"},{"emoji":"💣","aliases":["bomb"],"tags":["boom"],"category":"Smileys & Emotion","description":"bomb","unicode_version":"6.0"},{"emoji":"💬","aliases":["speech_balloon"],"tags":["comment"],"category":"Smileys & Emotion","description":"speech balloon","unicode_version":"6.0"},{"emoji":"👁️‍🗨️","aliases":["eye_speech_bubble"],"tags":[],"category":"Smileys & Emotion","description":"eye in speech bubble","unicode_version":"11.0"},{"emoji":"🗨️","aliases":["left_speech_bubble"],"tags":[],"category":"Smileys & Emotion","description":"left speech bubble","unicode_version":"11.0"},{"emoji":"🗯️","aliases":["right_anger_bubble"],"tags":[],"category":"Smileys & Emotion","description":"right anger bubble","unicode_version":"7.0"},{"emoji":"💭","aliases":["thought_balloon"],"tags":["thinking"],"category":"Smileys & Emotion","description":"thought balloon","unicode_version":"6.0"},{"emoji":"💤","aliases":["zzz"],"tags":["sleeping"],"category":"Smileys & Emotion","description":"zzz","unicode_version":"6.0"},{"emoji":"👋","aliases":["wave"],"tags":["goodbye"],"category":"People & Body","description":"waving hand","unicode_version":"6.0"},{"emoji":"🤚","aliases":["raised_back_of_hand"],"tags":[],"category":"People & Body","description":"raised back of hand","unicode_version":"9.0"},{"emoji":"🖐️","aliases":["raised_hand_with_fingers_splayed"],"tags":[],"category":"People & Body","description":"hand with fingers splayed","unicode_version":"7.0"},{"emoji":"✋","aliases":["hand","raised_hand"],"tags":["highfive","stop"],"category":"People & Body","description":"raised hand","unicode_version":"6.0"},{"emoji":"🖖","aliases":["vulcan_salute"],"tags":["prosper","spock"],"category":"People & Body","description":"vulcan salute","unicode_version":"7.0"},{"emoji":"👌","aliases":["ok_hand"],"tags":[],"category":"People & Body","description":"OK hand","unicode_version":"6.0"},{"emoji":"🤌","aliases":["pinched_fingers"],"tags":[],"category":"People & Body","description":"pinched fingers","unicode_version":"13.0"},{"emoji":"🤏","aliases":["pinching_hand"],"tags":[],"category":"People & Body","description":"pinching hand","unicode_version":"12.0"},{"emoji":"✌️","aliases":["v"],"tags":["victory","peace"],"category":"People & Body","description":"victory hand","unicode_version":""},{"emoji":"🤞","aliases":["crossed_fingers"],"tags":["luck","hopeful"],"category":"People & Body","description":"crossed fingers","unicode_version":"9.0"},{"emoji":"🤟","aliases":["love_you_gesture"],"tags":[],"category":"People & Body","description":"love-you gesture","unicode_version":"11.0"},{"emoji":"🤘","aliases":["metal"],"tags":[],"category":"People & Body","description":"sign of the horns","unicode_version":"8.0"},{"emoji":"🤙","aliases":["call_me_hand"],"tags":[],"category":"People & Body","description":"call me hand","unicode_version":"9.0"},{"emoji":"👈","aliases":["point_left"],"tags":[],"category":"People & Body","description":"backhand index pointing left","unicode_version":"6.0"},{"emoji":"👉","aliases":["point_right"],"tags":[],"category":"People & Body","description":"backhand index pointing right","unicode_version":"6.0"},{"emoji":"👆","aliases":["point_up_2"],"tags":[],"category":"People & Body","description":"backhand index pointing up","unicode_version":"6.0"},{"emoji":"🖕","aliases":["middle_finger","fu"],"tags":[],"category":"People & Body","description":"middle finger","unicode_version":"7.0"},{"emoji":"👇","aliases":["point_down"],"tags":[],"category":"People & Body","description":"backhand index pointing down","unicode_version":"6.0"},{"emoji":"☝️","aliases":["point_up"],"tags":[],"category":"People & Body","description":"index pointing up","unicode_version":""},{"emoji":"👍","aliases":["+1","thumbsup"],"tags":["approve","ok"],"category":"People & Body","description":"thumbs up","unicode_version":"6.0"},{"emoji":"👎","aliases":["-1","thumbsdown"],"tags":["disapprove","bury"],"category":"People & Body","description":"thumbs down","unicode_version":"6.0"},{"emoji":"✊","aliases":["fist_raised","fist"],"tags":["power"],"category":"People & Body","description":"raised fist","unicode_version":"6.0"},{"emoji":"👊","aliases":["fist_oncoming","facepunch","punch"],"tags":["attack"],"category":"People & Body","description":"oncoming fist","unicode_version":"6.0"},{"emoji":"🤛","aliases":["fist_left"],"tags":[],"category":"People & Body","description":"left-facing fist","unicode_version":"9.0"},{"emoji":"🤜","aliases":["fist_right"],"tags":[],"category":"People & Body","description":"right-facing fist","unicode_version":"9.0"},{"emoji":"👏","aliases":["clap"],"tags":["praise","applause"],"category":"People & Body","description":"clapping hands","unicode_version":"6.0"},{"emoji":"🙌","aliases":["raised_hands"],"tags":["hooray"],"category":"People & Body","description":"raising hands","unicode_version":"6.0"},{"emoji":"👐","aliases":["open_hands"],"tags":[],"category":"People & Body","description":"open hands","unicode_version":"6.0"},{"emoji":"🤲","aliases":["palms_up_together"],"tags":[],"category":"People & Body","description":"palms up together","unicode_version":"11.0"},{"emoji":"🤝","aliases":["handshake"],"tags":["deal"],"category":"People & Body","description":"handshake","unicode_version":"9.0"},{"emoji":"🙏","aliases":["pray"],"tags":["please","hope","wish"],"category":"People & Body","description":"folded hands","unicode_version":"6.0"},{"emoji":"✍️","aliases":["writing_hand"],"tags":[],"category":"People & Body","description":"writing hand","unicode_version":""},{"emoji":"💅","aliases":["nail_care"],"tags":["beauty","manicure"],"category":"People & Body","description":"nail polish","unicode_version":"6.0"},{"emoji":"🤳","aliases":["selfie"],"tags":[],"category":"People & Body","description":"selfie","unicode_version":"9.0"},{"emoji":"💪","aliases":["muscle"],"tags":["flex","bicep","strong","workout"],"category":"People & Body","description":"flexed biceps","unicode_version":"6.0"},{"emoji":"🦾","aliases":["mechanical_arm"],"tags":[],"category":"People & Body","description":"mechanical arm","unicode_version":"12.0"},{"emoji":"🦿","aliases":["mechanical_leg"],"tags":[],"category":"People & Body","description":"mechanical leg","unicode_version":"12.0"},{"emoji":"🦵","aliases":["leg"],"tags":[],"category":"People & Body","description":"leg","unicode_version":"11.0"},{"emoji":"🦶","aliases":["foot"],"tags":[],"category":"People & Body","description":"foot","unicode_version":"11.0"},{"emoji":"👂","aliases":["ear"],"tags":["hear","sound","listen"],"category":"People & Body","description":"ear","unicode_version":"6.0"},{"emoji":"🦻","aliases":["ear_with_hearing_aid"],"tags":[],"category":"People & Body","description":"ear with hearing aid","unicode_version":"12.0"},{"emoji":"👃","aliases":["nose"],"tags":["smell"],"category":"People & Body","description":"nose","unicode_version":"6.0"},{"emoji":"🧠","aliases":["brain"],"tags":[],"category":"People & Body","description":"brain","unicode_version":"11.0"},{"emoji":"🫀","aliases":["anatomical_heart"],"tags":[],"category":"People & Body","description":"anatomical heart","unicode_version":"13.0"},{"emoji":"🫁","aliases":["lungs"],"tags":[],"category":"People & Body","description":"lungs","unicode_version":"13.0"},{"emoji":"🦷","aliases":["tooth"],"tags":[],"category":"People & Body","description":"tooth","unicode_version":"11.0"},{"emoji":"🦴","aliases":["bone"],"tags":[],"category":"People & Body","description":"bone","unicode_version":"11.0"},{"emoji":"👀","aliases":["eyes"],"tags":["look","see","watch"],"category":"People & Body","description":"eyes","unicode_version":"6.0"},{"emoji":"👁️","aliases":["eye"],"tags":[],"category":"People & Body","description":"eye","unicode_version":"7.0"},{"emoji":"👅","aliases":["tongue"],"tags":["taste"],"category":"People & Body","description":"tongue","unicode_version":"6.0"},{"emoji":"👄","aliases":["lips"],"tags":["kiss"],"category":"People & Body","description":"mouth","unicode_version":"6.0"},{"emoji":"👶","aliases":["baby"],"tags":["child","newborn"],"category":"People & Body","description":"baby","unicode_version":"6.0"},{"emoji":"🧒","aliases":["child"],"tags":[],"category":"People & Body","description":"child","unicode_version":"11.0"},{"emoji":"👦","aliases":["boy"],"tags":["child"],"category":"People & Body","description":"boy","unicode_version":"6.0"},{"emoji":"👧","aliases":["girl"],"tags":["child"],"category":"People & Body","description":"girl","unicode_version":"6.0"},{"emoji":"🧑","aliases":["adult"],"tags":[],"category":"People & Body","description":"person","unicode_version":"11.0"},{"emoji":"👱","aliases":["blond_haired_person"],"tags":[],"category":"People & Body","description":"person: blond hair","unicode_version":"6.0"},{"emoji":"👨","aliases":["man"],"tags":["mustache","father","dad"],"category":"People & Body","description":"man","unicode_version":"6.0"},{"emoji":"🧔","aliases":["bearded_person"],"tags":[],"category":"People & Body","description":"person: beard","unicode_version":"11.0"},{"emoji":"🧔‍♂️","aliases":["man_beard"],"tags":[],"category":"People & Body","description":"man: beard","unicode_version":"13.1"},{"emoji":"🧔‍♀️","aliases":["woman_beard"],"tags":[],"category":"People & Body","description":"woman: beard","unicode_version":"13.1"},{"emoji":"👨‍🦰","aliases":["red_haired_man"],"tags":[],"category":"People & Body","description":"man: red hair","unicode_version":"11.0"},{"emoji":"👨‍🦱","aliases":["curly_haired_man"],"tags":[],"category":"People & Body","description":"man: curly hair","unicode_version":"11.0"},{"emoji":"👨‍🦳","aliases":["white_haired_man"],"tags":[],"category":"People & Body","description":"man: white hair","unicode_version":"11.0"},{"emoji":"👨‍🦲","aliases":["bald_man"],"tags":[],"category":"People & Body","description":"man: bald","unicode_version":"11.0"},{"emoji":"👩","aliases":["woman"],"tags":["girls"],"category":"People & Body","description":"woman","unicode_version":"6.0"},{"emoji":"👩‍🦰","aliases":["red_haired_woman"],"tags":[],"category":"People & Body","description":"woman: red hair","unicode_version":"11.0"},{"emoji":"🧑‍🦰","aliases":["person_red_hair"],"tags":[],"category":"People & Body","description":"person: red hair","unicode_version":"12.1"},{"emoji":"👩‍🦱","aliases":["curly_haired_woman"],"tags":[],"category":"People & Body","description":"woman: curly hair","unicode_version":"11.0"},{"emoji":"🧑‍🦱","aliases":["person_curly_hair"],"tags":[],"category":"People & Body","description":"person: curly hair","unicode_version":"12.1"},{"emoji":"👩‍🦳","aliases":["white_haired_woman"],"tags":[],"category":"People & Body","description":"woman: white hair","unicode_version":"11.0"},{"emoji":"🧑‍🦳","aliases":["person_white_hair"],"tags":[],"category":"People & Body","description":"person: white hair","unicode_version":"12.1"},{"emoji":"👩‍🦲","aliases":["bald_woman"],"tags":[],"category":"People & Body","description":"woman: bald","unicode_version":"11.0"},{"emoji":"🧑‍🦲","aliases":["person_bald"],"tags":[],"category":"People & Body","description":"person: bald","unicode_version":"12.1"},{"emoji":"👱‍♀️","aliases":["blond_haired_woman","blonde_woman"],"tags":[],"category":"People & Body","description":"woman: blond hair","unicode_version":"6.0"},{"emoji":"👱‍♂️","aliases":["blond_haired_man"],"tags":[],"category":"People & Body","description":"man: blond hair","unicode_version":"11.0"},{"emoji":"🧓","aliases":["older_adult"],"tags":[],"category":"People & Body","description":"older person","unicode_version":"11.0"},{"emoji":"👴","aliases":["older_man"],"tags":[],"category":"People & Body","description":"old man","unicode_version":"6.0"},{"emoji":"👵","aliases":["older_woman"],"tags":[],"category":"People & Body","description":"old woman","unicode_version":"6.0"},{"emoji":"🙍","aliases":["frowning_person"],"tags":[],"category":"People & Body","description":"person frowning","unicode_version":"6.0"},{"emoji":"🙍‍♂️","aliases":["frowning_man"],"tags":[],"category":"People & Body","description":"man frowning","unicode_version":"6.0"},{"emoji":"🙍‍♀️","aliases":["frowning_woman"],"tags":[],"category":"People & Body","description":"woman frowning","unicode_version":"11.0"},{"emoji":"🙎","aliases":["pouting_face"],"tags":[],"category":"People & Body","description":"person pouting","unicode_version":"6.0"},{"emoji":"🙎‍♂️","aliases":["pouting_man"],"tags":[],"category":"People & Body","description":"man pouting","unicode_version":"6.0"},{"emoji":"🙎‍♀️","aliases":["pouting_woman"],"tags":[],"category":"People & Body","description":"woman pouting","unicode_version":"11.0"},{"emoji":"🙅","aliases":["no_good"],"tags":["stop","halt","denied"],"category":"People & Body","description":"person gesturing NO","unicode_version":"6.0"},{"emoji":"🙅‍♂️","aliases":["no_good_man","ng_man"],"tags":["stop","halt","denied"],"category":"People & Body","description":"man gesturing NO","unicode_version":"6.0"},{"emoji":"🙅‍♀️","aliases":["no_good_woman","ng_woman"],"tags":["stop","halt","denied"],"category":"People & Body","description":"woman gesturing NO","unicode_version":"11.0"},{"emoji":"🙆","aliases":["ok_person"],"tags":[],"category":"People & Body","description":"person gesturing OK","unicode_version":"6.0"},{"emoji":"🙆‍♂️","aliases":["ok_man"],"tags":[],"category":"People & Body","description":"man gesturing OK","unicode_version":"6.0"},{"emoji":"🙆‍♀️","aliases":["ok_woman"],"tags":[],"category":"People & Body","description":"woman gesturing OK","unicode_version":"11.0"},{"emoji":"💁","aliases":["tipping_hand_person","information_desk_person"],"tags":[],"category":"People & Body","description":"person tipping hand","unicode_version":"6.0"},{"emoji":"💁‍♂️","aliases":["tipping_hand_man","sassy_man"],"tags":["information"],"category":"People & Body","description":"man tipping hand","unicode_version":"6.0"},{"emoji":"💁‍♀️","aliases":["tipping_hand_woman","sassy_woman"],"tags":["information"],"category":"People & Body","description":"woman tipping hand","unicode_version":"11.0"},{"emoji":"🙋","aliases":["raising_hand"],"tags":[],"category":"People & Body","description":"person raising hand","unicode_version":"6.0"},{"emoji":"🙋‍♂️","aliases":["raising_hand_man"],"tags":[],"category":"People & Body","description":"man raising hand","unicode_version":"6.0"},{"emoji":"🙋‍♀️","aliases":["raising_hand_woman"],"tags":[],"category":"People & Body","description":"woman raising hand","unicode_version":"11.0"},{"emoji":"🧏","aliases":["deaf_person"],"tags":[],"category":"People & Body","description":"deaf person","unicode_version":"12.0"},{"emoji":"🧏‍♂️","aliases":["deaf_man"],"tags":[],"category":"People & Body","description":"deaf man","unicode_version":"12.0"},{"emoji":"🧏‍♀️","aliases":["deaf_woman"],"tags":[],"category":"People & Body","description":"deaf woman","unicode_version":"12.0"},{"emoji":"🙇","aliases":["bow"],"tags":["respect","thanks"],"category":"People & Body","description":"person bowing","unicode_version":"6.0"},{"emoji":"🙇‍♂️","aliases":["bowing_man"],"tags":["respect","thanks"],"category":"People & Body","description":"man bowing","unicode_version":"11.0"},{"emoji":"🙇‍♀️","aliases":["bowing_woman"],"tags":["respect","thanks"],"category":"People & Body","description":"woman bowing","unicode_version":"6.0"},{"emoji":"🤦","aliases":["facepalm"],"tags":[],"category":"People & Body","description":"person facepalming","unicode_version":"11.0"},{"emoji":"🤦‍♂️","aliases":["man_facepalming"],"tags":[],"category":"People & Body","description":"man facepalming","unicode_version":"9.0"},{"emoji":"🤦‍♀️","aliases":["woman_facepalming"],"tags":[],"category":"People & Body","description":"woman facepalming","unicode_version":"9.0"},{"emoji":"🤷","aliases":["shrug"],"tags":[],"category":"People & Body","description":"person shrugging","unicode_version":"11.0"},{"emoji":"🤷‍♂️","aliases":["man_shrugging"],"tags":[],"category":"People & Body","description":"man shrugging","unicode_version":"9.0"},{"emoji":"🤷‍♀️","aliases":["woman_shrugging"],"tags":[],"category":"People & Body","description":"woman shrugging","unicode_version":"9.0"},{"emoji":"🧑‍⚕️","aliases":["health_worker"],"tags":[],"category":"People & Body","description":"health worker","unicode_version":"12.1"},{"emoji":"👨‍⚕️","aliases":["man_health_worker"],"tags":["doctor","nurse"],"category":"People & Body","description":"man health worker","unicode_version":""},{"emoji":"👩‍⚕️","aliases":["woman_health_worker"],"tags":["doctor","nurse"],"category":"People & Body","description":"woman health worker","unicode_version":""},{"emoji":"🧑‍🎓","aliases":["student"],"tags":[],"category":"People & Body","description":"student","unicode_version":"12.1"},{"emoji":"👨‍🎓","aliases":["man_student"],"tags":["graduation"],"category":"People & Body","description":"man student","unicode_version":""},{"emoji":"👩‍🎓","aliases":["woman_student"],"tags":["graduation"],"category":"People & Body","description":"woman student","unicode_version":""},{"emoji":"🧑‍🏫","aliases":["teacher"],"tags":[],"category":"People & Body","description":"teacher","unicode_version":"12.1"},{"emoji":"👨‍🏫","aliases":["man_teacher"],"tags":["school","professor"],"category":"People & Body","description":"man teacher","unicode_version":""},{"emoji":"👩‍🏫","aliases":["woman_teacher"],"tags":["school","professor"],"category":"People & Body","description":"woman teacher","unicode_version":""},{"emoji":"🧑‍⚖️","aliases":["judge"],"tags":[],"category":"People & Body","description":"judge","unicode_version":"12.1"},{"emoji":"👨‍⚖️","aliases":["man_judge"],"tags":["justice"],"category":"People & Body","description":"man judge","unicode_version":""},{"emoji":"👩‍⚖️","aliases":["woman_judge"],"tags":["justice"],"category":"People & Body","description":"woman judge","unicode_version":""},{"emoji":"🧑‍🌾","aliases":["farmer"],"tags":[],"category":"People & Body","description":"farmer","unicode_version":"12.1"},{"emoji":"👨‍🌾","aliases":["man_farmer"],"tags":[],"category":"People & Body","description":"man farmer","unicode_version":""},{"emoji":"👩‍🌾","aliases":["woman_farmer"],"tags":[],"category":"People & Body","description":"woman farmer","unicode_version":""},{"emoji":"🧑‍🍳","aliases":["cook"],"tags":[],"category":"People & Body","description":"cook","unicode_version":"12.1"},{"emoji":"👨‍🍳","aliases":["man_cook"],"tags":["chef"],"category":"People & Body","description":"man cook","unicode_version":""},{"emoji":"👩‍🍳","aliases":["woman_cook"],"tags":["chef"],"category":"People & Body","description":"woman cook","unicode_version":""},{"emoji":"🧑‍🔧","aliases":["mechanic"],"tags":[],"category":"People & Body","description":"mechanic","unicode_version":"12.1"},{"emoji":"👨‍🔧","aliases":["man_mechanic"],"tags":[],"category":"People & Body","description":"man mechanic","unicode_version":""},{"emoji":"👩‍🔧","aliases":["woman_mechanic"],"tags":[],"category":"People & Body","description":"woman mechanic","unicode_version":""},{"emoji":"🧑‍🏭","aliases":["factory_worker"],"tags":[],"category":"People & Body","description":"factory worker","unicode_version":"12.1"},{"emoji":"👨‍🏭","aliases":["man_factory_worker"],"tags":[],"category":"People & Body","description":"man factory worker","unicode_version":""},{"emoji":"👩‍🏭","aliases":["woman_factory_worker"],"tags":[],"category":"People & Body","description":"woman factory worker","unicode_version":""},{"emoji":"🧑‍💼","aliases":["office_worker"],"tags":[],"category":"People & Body","description":"office worker","unicode_version":"12.1"},{"emoji":"👨‍💼","aliases":["man_office_worker"],"tags":["business"],"category":"People & Body","description":"man office worker","unicode_version":""},{"emoji":"👩‍💼","aliases":["woman_office_worker"],"tags":["business"],"category":"People & Body","description":"woman office worker","unicode_version":""},{"emoji":"🧑‍🔬","aliases":["scientist"],"tags":[],"category":"People & Body","description":"scientist","unicode_version":"12.1"},{"emoji":"👨‍🔬","aliases":["man_scientist"],"tags":["research"],"category":"People & Body","description":"man scientist","unicode_version":""},{"emoji":"👩‍🔬","aliases":["woman_scientist"],"tags":["research"],"category":"People & Body","description":"woman scientist","unicode_version":""},{"emoji":"🧑‍💻","aliases":["technologist"],"tags":[],"category":"People & Body","description":"technologist","unicode_version":"12.1"},{"emoji":"👨‍💻","aliases":["man_technologist"],"tags":["coder"],"category":"People & Body","description":"man technologist","unicode_version":""},{"emoji":"👩‍💻","aliases":["woman_technologist"],"tags":["coder"],"category":"People & Body","description":"woman technologist","unicode_version":""},{"emoji":"🧑‍🎤","aliases":["singer"],"tags":[],"category":"People & Body","description":"singer","unicode_version":"12.1"},{"emoji":"👨‍🎤","aliases":["man_singer"],"tags":["rockstar"],"category":"People & Body","description":"man singer","unicode_version":""},{"emoji":"👩‍🎤","aliases":["woman_singer"],"tags":["rockstar"],"category":"People & Body","description":"woman singer","unicode_version":""},{"emoji":"🧑‍🎨","aliases":["artist"],"tags":[],"category":"People & Body","description":"artist","unicode_version":"12.1"},{"emoji":"👨‍🎨","aliases":["man_artist"],"tags":["painter"],"category":"People & Body","description":"man artist","unicode_version":""},{"emoji":"👩‍🎨","aliases":["woman_artist"],"tags":["painter"],"category":"People & Body","description":"woman artist","unicode_version":""},{"emoji":"🧑‍✈️","aliases":["pilot"],"tags":[],"category":"People & Body","description":"pilot","unicode_version":"12.1"},{"emoji":"👨‍✈️","aliases":["man_pilot"],"tags":[],"category":"People & Body","description":"man pilot","unicode_version":""},{"emoji":"👩‍✈️","aliases":["woman_pilot"],"tags":[],"category":"People & Body","description":"woman pilot","unicode_version":""},{"emoji":"🧑‍🚀","aliases":["astronaut"],"tags":[],"category":"People & Body","description":"astronaut","unicode_version":"12.1"},{"emoji":"👨‍🚀","aliases":["man_astronaut"],"tags":["space"],"category":"People & Body","description":"man astronaut","unicode_version":""},{"emoji":"👩‍🚀","aliases":["woman_astronaut"],"tags":["space"],"category":"People & Body","description":"woman astronaut","unicode_version":""},{"emoji":"🧑‍🚒","aliases":["firefighter"],"tags":[],"category":"People & Body","description":"firefighter","unicode_version":"12.1"},{"emoji":"👨‍🚒","aliases":["man_firefighter"],"tags":[],"category":"People & Body","description":"man firefighter","unicode_version":""},{"emoji":"👩‍🚒","aliases":["woman_firefighter"],"tags":[],"category":"People & Body","description":"woman firefighter","unicode_version":""},{"emoji":"👮","aliases":["police_officer","cop"],"tags":["law"],"category":"People & Body","description":"police officer","unicode_version":"6.0"},{"emoji":"👮‍♂️","aliases":["policeman"],"tags":["law","cop"],"category":"People & Body","description":"man police officer","unicode_version":"11.0"},{"emoji":"👮‍♀️","aliases":["policewoman"],"tags":["law","cop"],"category":"People & Body","description":"woman police officer","unicode_version":"6.0"},{"emoji":"🕵️","aliases":["detective"],"tags":["sleuth"],"category":"People & Body","description":"detective","unicode_version":"7.0"},{"emoji":"🕵️‍♂️","aliases":["male_detective"],"tags":["sleuth"],"category":"People & Body","description":"man detective","unicode_version":"11.0"},{"emoji":"🕵️‍♀️","aliases":["female_detective"],"tags":["sleuth"],"category":"People & Body","description":"woman detective","unicode_version":"6.0"},{"emoji":"💂","aliases":["guard"],"tags":[],"category":"People & Body","description":"guard","unicode_version":"6.0"},{"emoji":"💂‍♂️","aliases":["guardsman"],"tags":[],"category":"People & Body","description":"man guard","unicode_version":"11.0"},{"emoji":"💂‍♀️","aliases":["guardswoman"],"tags":[],"category":"People & Body","description":"woman guard","unicode_version":"6.0"},{"emoji":"🥷","aliases":["ninja"],"tags":[],"category":"People & Body","description":"ninja","unicode_version":"13.0"},{"emoji":"👷","aliases":["construction_worker"],"tags":["helmet"],"category":"People & Body","description":"construction worker","unicode_version":"6.0"},{"emoji":"👷‍♂️","aliases":["construction_worker_man"],"tags":["helmet"],"category":"People & Body","description":"man construction worker","unicode_version":"11.0"},{"emoji":"👷‍♀️","aliases":["construction_worker_woman"],"tags":["helmet"],"category":"People & Body","description":"woman construction worker","unicode_version":"6.0"},{"emoji":"🤴","aliases":["prince"],"tags":["crown","royal"],"category":"People & Body","description":"prince","unicode_version":"9.0"},{"emoji":"👸","aliases":["princess"],"tags":["crown","royal"],"category":"People & Body","description":"princess","unicode_version":"6.0"},{"emoji":"👳","aliases":["person_with_turban"],"tags":[],"category":"People & Body","description":"person wearing turban","unicode_version":"6.0"},{"emoji":"👳‍♂️","aliases":["man_with_turban"],"tags":[],"category":"People & Body","description":"man wearing turban","unicode_version":"11.0"},{"emoji":"👳‍♀️","aliases":["woman_with_turban"],"tags":[],"category":"People & Body","description":"woman wearing turban","unicode_version":"6.0"},{"emoji":"👲","aliases":["man_with_gua_pi_mao"],"tags":[],"category":"People & Body","description":"person with skullcap","unicode_version":"6.0"},{"emoji":"🧕","aliases":["woman_with_headscarf"],"tags":["hijab"],"category":"People & Body","description":"woman with headscarf","unicode_version":"11.0"},{"emoji":"🤵","aliases":["person_in_tuxedo"],"tags":["groom","marriage","wedding"],"category":"People & Body","description":"person in tuxedo","unicode_version":"9.0"},{"emoji":"🤵‍♂️","aliases":["man_in_tuxedo"],"tags":[],"category":"People & Body","description":"man in tuxedo","unicode_version":"13.0"},{"emoji":"🤵‍♀️","aliases":["woman_in_tuxedo"],"tags":[],"category":"People & Body","description":"woman in tuxedo","unicode_version":"13.0"},{"emoji":"👰","aliases":["person_with_veil"],"tags":["marriage","wedding"],"category":"People & Body","description":"person with veil","unicode_version":"6.0"},{"emoji":"👰‍♂️","aliases":["man_with_veil"],"tags":[],"category":"People & Body","description":"man with veil","unicode_version":"13.0"},{"emoji":"👰‍♀️","aliases":["woman_with_veil","bride_with_veil"],"tags":[],"category":"People & Body","description":"woman with veil","unicode_version":"13.0"},{"emoji":"🤰","aliases":["pregnant_woman"],"tags":[],"category":"People & Body","description":"pregnant woman","unicode_version":"9.0"},{"emoji":"🤱","aliases":["breast_feeding"],"tags":["nursing"],"category":"People & Body","description":"breast-feeding","unicode_version":"11.0"},{"emoji":"👩‍🍼","aliases":["woman_feeding_baby"],"tags":[],"category":"People & Body","description":"woman feeding baby","unicode_version":"13.0"},{"emoji":"👨‍🍼","aliases":["man_feeding_baby"],"tags":[],"category":"People & Body","description":"man feeding baby","unicode_version":"13.0"},{"emoji":"🧑‍🍼","aliases":["person_feeding_baby"],"tags":[],"category":"People & Body","description":"person feeding baby","unicode_version":"13.0"},{"emoji":"👼","aliases":["angel"],"tags":[],"category":"People & Body","description":"baby angel","unicode_version":"6.0"},{"emoji":"🎅","aliases":["santa"],"tags":["christmas"],"category":"People & Body","description":"Santa Claus","unicode_version":"6.0"},{"emoji":"🤶","aliases":["mrs_claus"],"tags":["santa"],"category":"People & Body","description":"Mrs. Claus","unicode_version":"9.0"},{"emoji":"🧑‍🎄","aliases":["mx_claus"],"tags":[],"category":"People & Body","description":"mx claus","unicode_version":"13.0"},{"emoji":"🦸","aliases":["superhero"],"tags":[],"category":"People & Body","description":"superhero","unicode_version":"11.0"},{"emoji":"🦸‍♂️","aliases":["superhero_man"],"tags":[],"category":"People & Body","description":"man superhero","unicode_version":"11.0"},{"emoji":"🦸‍♀️","aliases":["superhero_woman"],"tags":[],"category":"People & Body","description":"woman superhero","unicode_version":"11.0"},{"emoji":"🦹","aliases":["supervillain"],"tags":[],"category":"People & Body","description":"supervillain","unicode_version":"11.0"},{"emoji":"🦹‍♂️","aliases":["supervillain_man"],"tags":[],"category":"People & Body","description":"man supervillain","unicode_version":"11.0"},{"emoji":"🦹‍♀️","aliases":["supervillain_woman"],"tags":[],"category":"People & Body","description":"woman supervillain","unicode_version":"11.0"},{"emoji":"🧙","aliases":["mage"],"tags":["wizard"],"category":"People & Body","description":"mage","unicode_version":"11.0"},{"emoji":"🧙‍♂️","aliases":["mage_man"],"tags":["wizard"],"category":"People & Body","description":"man mage","unicode_version":"11.0"},{"emoji":"🧙‍♀️","aliases":["mage_woman"],"tags":["wizard"],"category":"People & Body","description":"woman mage","unicode_version":"11.0"},{"emoji":"🧚","aliases":["fairy"],"tags":[],"category":"People & Body","description":"fairy","unicode_version":"11.0"},{"emoji":"🧚‍♂️","aliases":["fairy_man"],"tags":[],"category":"People & Body","description":"man fairy","unicode_version":"11.0"},{"emoji":"🧚‍♀️","aliases":["fairy_woman"],"tags":[],"category":"People & Body","description":"woman fairy","unicode_version":"11.0"},{"emoji":"🧛","aliases":["vampire"],"tags":[],"category":"People & Body","description":"vampire","unicode_version":"11.0"},{"emoji":"🧛‍♂️","aliases":["vampire_man"],"tags":[],"category":"People & Body","description":"man vampire","unicode_version":"11.0"},{"emoji":"🧛‍♀️","aliases":["vampire_woman"],"tags":[],"category":"People & Body","description":"woman vampire","unicode_version":"11.0"},{"emoji":"🧜","aliases":["merperson"],"tags":[],"category":"People & Body","description":"merperson","unicode_version":"11.0"},{"emoji":"🧜‍♂️","aliases":["merman"],"tags":[],"category":"People & Body","description":"merman","unicode_version":"11.0"},{"emoji":"🧜‍♀️","aliases":["mermaid"],"tags":[],"category":"People & Body","description":"mermaid","unicode_version":"11.0"},{"emoji":"🧝","aliases":["elf"],"tags":[],"category":"People & Body","description":"elf","unicode_version":"11.0"},{"emoji":"🧝‍♂️","aliases":["elf_man"],"tags":[],"category":"People & Body","description":"man elf","unicode_version":"11.0"},{"emoji":"🧝‍♀️","aliases":["elf_woman"],"tags":[],"category":"People & Body","description":"woman elf","unicode_version":"11.0"},{"emoji":"🧞","aliases":["genie"],"tags":[],"category":"People & Body","description":"genie","unicode_version":"11.0"},{"emoji":"🧞‍♂️","aliases":["genie_man"],"tags":[],"category":"People & Body","description":"man genie","unicode_version":"11.0"},{"emoji":"🧞‍♀️","aliases":["genie_woman"],"tags":[],"category":"People & Body","description":"woman genie","unicode_version":"11.0"},{"emoji":"🧟","aliases":["zombie"],"tags":[],"category":"People & Body","description":"zombie","unicode_version":"11.0"},{"emoji":"🧟‍♂️","aliases":["zombie_man"],"tags":[],"category":"People & Body","description":"man zombie","unicode_version":"11.0"},{"emoji":"🧟‍♀️","aliases":["zombie_woman"],"tags":[],"category":"People & Body","description":"woman zombie","unicode_version":"11.0"},{"emoji":"💆","aliases":["massage"],"tags":["spa"],"category":"People & Body","description":"person getting massage","unicode_version":"6.0"},{"emoji":"💆‍♂️","aliases":["massage_man"],"tags":["spa"],"category":"People & Body","description":"man getting massage","unicode_version":"6.0"},{"emoji":"💆‍♀️","aliases":["massage_woman"],"tags":["spa"],"category":"People & Body","description":"woman getting massage","unicode_version":"11.0"},{"emoji":"💇","aliases":["haircut"],"tags":["beauty"],"category":"People & Body","description":"person getting haircut","unicode_version":"6.0"},{"emoji":"💇‍♂️","aliases":["haircut_man"],"tags":[],"category":"People & Body","description":"man getting haircut","unicode_version":"6.0"},{"emoji":"💇‍♀️","aliases":["haircut_woman"],"tags":[],"category":"People & Body","description":"woman getting haircut","unicode_version":"11.0"},{"emoji":"🚶","aliases":["walking"],"tags":[],"category":"People & Body","description":"person walking","unicode_version":"6.0"},{"emoji":"🚶‍♂️","aliases":["walking_man"],"tags":[],"category":"People & Body","description":"man walking","unicode_version":"11.0"},{"emoji":"🚶‍♀️","aliases":["walking_woman"],"tags":[],"category":"People & Body","description":"woman walking","unicode_version":"6.0"},{"emoji":"🧍","aliases":["standing_person"],"tags":[],"category":"People & Body","description":"person standing","unicode_version":"12.0"},{"emoji":"🧍‍♂️","aliases":["standing_man"],"tags":[],"category":"People & Body","description":"man standing","unicode_version":"12.0"},{"emoji":"🧍‍♀️","aliases":["standing_woman"],"tags":[],"category":"People & Body","description":"woman standing","unicode_version":"12.0"},{"emoji":"🧎","aliases":["kneeling_person"],"tags":[],"category":"People & Body","description":"person kneeling","unicode_version":"12.0"},{"emoji":"🧎‍♂️","aliases":["kneeling_man"],"tags":[],"category":"People & Body","description":"man kneeling","unicode_version":"12.0"},{"emoji":"🧎‍♀️","aliases":["kneeling_woman"],"tags":[],"category":"People & Body","description":"woman kneeling","unicode_version":"12.0"},{"emoji":"🧑‍🦯","aliases":["person_with_probing_cane"],"tags":[],"category":"People & Body","description":"person with white cane","unicode_version":"12.1"},{"emoji":"👨‍🦯","aliases":["man_with_probing_cane"],"tags":[],"category":"People & Body","description":"man with white cane","unicode_version":"12.0"},{"emoji":"👩‍🦯","aliases":["woman_with_probing_cane"],"tags":[],"category":"People & Body","description":"woman with white cane","unicode_version":"12.0"},{"emoji":"🧑‍🦼","aliases":["person_in_motorized_wheelchair"],"tags":[],"category":"People & Body","description":"person in motorized wheelchair","unicode_version":"12.1"},{"emoji":"👨‍🦼","aliases":["man_in_motorized_wheelchair"],"tags":[],"category":"People & Body","description":"man in motorized wheelchair","unicode_version":"12.0"},{"emoji":"👩‍🦼","aliases":["woman_in_motorized_wheelchair"],"tags":[],"category":"People & Body","description":"woman in motorized wheelchair","unicode_version":"12.0"},{"emoji":"🧑‍🦽","aliases":["person_in_manual_wheelchair"],"tags":[],"category":"People & Body","description":"person in manual wheelchair","unicode_version":"12.1"},{"emoji":"👨‍🦽","aliases":["man_in_manual_wheelchair"],"tags":[],"category":"People & Body","description":"man in manual wheelchair","unicode_version":"12.0"},{"emoji":"👩‍🦽","aliases":["woman_in_manual_wheelchair"],"tags":[],"category":"People & Body","description":"woman in manual wheelchair","unicode_version":"12.0"},{"emoji":"🏃","aliases":["runner","running"],"tags":["exercise","workout","marathon"],"category":"People & Body","description":"person running","unicode_version":"6.0"},{"emoji":"🏃‍♂️","aliases":["running_man"],"tags":["exercise","workout","marathon"],"category":"People & Body","description":"man running","unicode_version":"11.0"},{"emoji":"🏃‍♀️","aliases":["running_woman"],"tags":["exercise","workout","marathon"],"category":"People & Body","description":"woman running","unicode_version":"6.0"},{"emoji":"💃","aliases":["woman_dancing","dancer"],"tags":["dress"],"category":"People & Body","description":"woman dancing","unicode_version":"6.0"},{"emoji":"🕺","aliases":["man_dancing"],"tags":["dancer"],"category":"People & Body","description":"man dancing","unicode_version":"9.0"},{"emoji":"🕴️","aliases":["business_suit_levitating"],"tags":[],"category":"People & Body","description":"person in suit levitating","unicode_version":"7.0"},{"emoji":"👯","aliases":["dancers"],"tags":["bunny"],"category":"People & Body","description":"people with bunny ears","unicode_version":"6.0"},{"emoji":"👯‍♂️","aliases":["dancing_men"],"tags":["bunny"],"category":"People & Body","description":"men with bunny ears","unicode_version":"6.0"},{"emoji":"👯‍♀️","aliases":["dancing_women"],"tags":["bunny"],"category":"People & Body","description":"women with bunny ears","unicode_version":"11.0"},{"emoji":"🧖","aliases":["sauna_person"],"tags":["steamy"],"category":"People & Body","description":"person in steamy room","unicode_version":"11.0"},{"emoji":"🧖‍♂️","aliases":["sauna_man"],"tags":["steamy"],"category":"People & Body","description":"man in steamy room","unicode_version":"11.0"},{"emoji":"🧖‍♀️","aliases":["sauna_woman"],"tags":["steamy"],"category":"People & Body","description":"woman in steamy room","unicode_version":"11.0"},{"emoji":"🧗","aliases":["climbing"],"tags":["bouldering"],"category":"People & Body","description":"person climbing","unicode_version":"11.0"},{"emoji":"🧗‍♂️","aliases":["climbing_man"],"tags":["bouldering"],"category":"People & Body","description":"man climbing","unicode_version":"11.0"},{"emoji":"🧗‍♀️","aliases":["climbing_woman"],"tags":["bouldering"],"category":"People & Body","description":"woman climbing","unicode_version":"11.0"},{"emoji":"🤺","aliases":["person_fencing"],"tags":[],"category":"People & Body","description":"person fencing","unicode_version":"9.0"},{"emoji":"🏇","aliases":["horse_racing"],"tags":[],"category":"People & Body","description":"horse racing","unicode_version":"6.0"},{"emoji":"⛷️","aliases":["skier"],"tags":[],"category":"People & Body","description":"skier","unicode_version":"5.2"},{"emoji":"🏂","aliases":["snowboarder"],"tags":[],"category":"People & Body","description":"snowboarder","unicode_version":"6.0"},{"emoji":"🏌️","aliases":["golfing"],"tags":[],"category":"People & Body","description":"person golfing","unicode_version":"7.0"},{"emoji":"🏌️‍♂️","aliases":["golfing_man"],"tags":[],"category":"People & Body","description":"man golfing","unicode_version":"11.0"},{"emoji":"🏌️‍♀️","aliases":["golfing_woman"],"tags":[],"category":"People & Body","description":"woman golfing","unicode_version":""},{"emoji":"🏄","aliases":["surfer"],"tags":[],"category":"People & Body","description":"person surfing","unicode_version":"6.0"},{"emoji":"🏄‍♂️","aliases":["surfing_man"],"tags":[],"category":"People & Body","description":"man surfing","unicode_version":"11.0"},{"emoji":"🏄‍♀️","aliases":["surfing_woman"],"tags":[],"category":"People & Body","description":"woman surfing","unicode_version":"7.0"},{"emoji":"🚣","aliases":["rowboat"],"tags":[],"category":"People & Body","description":"person rowing boat","unicode_version":"6.0"},{"emoji":"🚣‍♂️","aliases":["rowing_man"],"tags":[],"category":"People & Body","description":"man rowing boat","unicode_version":"11.0"},{"emoji":"🚣‍♀️","aliases":["rowing_woman"],"tags":[],"category":"People & Body","description":"woman rowing boat","unicode_version":"6.0"},{"emoji":"🏊","aliases":["swimmer"],"tags":[],"category":"People & Body","description":"person swimming","unicode_version":"6.0"},{"emoji":"🏊‍♂️","aliases":["swimming_man"],"tags":[],"category":"People & Body","description":"man swimming","unicode_version":"11.0"},{"emoji":"🏊‍♀️","aliases":["swimming_woman"],"tags":[],"category":"People & Body","description":"woman swimming","unicode_version":"6.0"},{"emoji":"⛹️","aliases":["bouncing_ball_person"],"tags":["basketball"],"category":"People & Body","description":"person bouncing ball","unicode_version":"5.2"},{"emoji":"⛹️‍♂️","aliases":["bouncing_ball_man","basketball_man"],"tags":[],"category":"People & Body","description":"man bouncing ball","unicode_version":"11.0"},{"emoji":"⛹️‍♀️","aliases":["bouncing_ball_woman","basketball_woman"],"tags":[],"category":"People & Body","description":"woman bouncing ball","unicode_version":"7.0"},{"emoji":"🏋️","aliases":["weight_lifting"],"tags":["gym","workout"],"category":"People & Body","description":"person lifting weights","unicode_version":"7.0"},{"emoji":"🏋️‍♂️","aliases":["weight_lifting_man"],"tags":["gym","workout"],"category":"People & Body","description":"man lifting weights","unicode_version":"11.0"},{"emoji":"🏋️‍♀️","aliases":["weight_lifting_woman"],"tags":["gym","workout"],"category":"People & Body","description":"woman lifting weights","unicode_version":"6.0"},{"emoji":"🚴","aliases":["bicyclist"],"tags":[],"category":"People & Body","description":"person biking","unicode_version":"6.0"},{"emoji":"🚴‍♂️","aliases":["biking_man"],"tags":[],"category":"People & Body","description":"man biking","unicode_version":"11.0"},{"emoji":"🚴‍♀️","aliases":["biking_woman"],"tags":[],"category":"People & Body","description":"woman biking","unicode_version":"6.0"},{"emoji":"🚵","aliases":["mountain_bicyclist"],"tags":[],"category":"People & Body","description":"person mountain biking","unicode_version":"6.0"},{"emoji":"🚵‍♂️","aliases":["mountain_biking_man"],"tags":[],"category":"People & Body","description":"man mountain biking","unicode_version":"11.0"},{"emoji":"🚵‍♀️","aliases":["mountain_biking_woman"],"tags":[],"category":"People & Body","description":"woman mountain biking","unicode_version":"6.0"},{"emoji":"🤸","aliases":["cartwheeling"],"tags":[],"category":"People & Body","description":"person cartwheeling","unicode_version":"11.0"},{"emoji":"🤸‍♂️","aliases":["man_cartwheeling"],"tags":[],"category":"People & Body","description":"man cartwheeling","unicode_version":""},{"emoji":"🤸‍♀️","aliases":["woman_cartwheeling"],"tags":[],"category":"People & Body","description":"woman cartwheeling","unicode_version":""},{"emoji":"🤼","aliases":["wrestling"],"tags":[],"category":"People & Body","description":"people wrestling","unicode_version":"11.0"},{"emoji":"🤼‍♂️","aliases":["men_wrestling"],"tags":[],"category":"People & Body","description":"men wrestling","unicode_version":"9.0"},{"emoji":"🤼‍♀️","aliases":["women_wrestling"],"tags":[],"category":"People & Body","description":"women wrestling","unicode_version":"9.0"},{"emoji":"🤽","aliases":["water_polo"],"tags":[],"category":"People & Body","description":"person playing water polo","unicode_version":"11.0"},{"emoji":"🤽‍♂️","aliases":["man_playing_water_polo"],"tags":[],"category":"People & Body","description":"man playing water polo","unicode_version":"9.0"},{"emoji":"🤽‍♀️","aliases":["woman_playing_water_polo"],"tags":[],"category":"People & Body","description":"woman playing water polo","unicode_version":"9.0"},{"emoji":"🤾","aliases":["handball_person"],"tags":[],"category":"People & Body","description":"person playing handball","unicode_version":"11.0"},{"emoji":"🤾‍♂️","aliases":["man_playing_handball"],"tags":[],"category":"People & Body","description":"man playing handball","unicode_version":"9.0"},{"emoji":"🤾‍♀️","aliases":["woman_playing_handball"],"tags":[],"category":"People & Body","description":"woman playing handball","unicode_version":"9.0"},{"emoji":"🤹","aliases":["juggling_person"],"tags":[],"category":"People & Body","description":"person juggling","unicode_version":"11.0"},{"emoji":"🤹‍♂️","aliases":["man_juggling"],"tags":[],"category":"People & Body","description":"man juggling","unicode_version":"9.0"},{"emoji":"🤹‍♀️","aliases":["woman_juggling"],"tags":[],"category":"People & Body","description":"woman juggling","unicode_version":"9.0"},{"emoji":"🧘","aliases":["lotus_position"],"tags":["meditation"],"category":"People & Body","description":"person in lotus position","unicode_version":"11.0"},{"emoji":"🧘‍♂️","aliases":["lotus_position_man"],"tags":["meditation"],"category":"People & Body","description":"man in lotus position","unicode_version":"11.0"},{"emoji":"🧘‍♀️","aliases":["lotus_position_woman"],"tags":["meditation"],"category":"People & Body","description":"woman in lotus position","unicode_version":"11.0"},{"emoji":"🛀","aliases":["bath"],"tags":["shower"],"category":"People & Body","description":"person taking bath","unicode_version":"6.0"},{"emoji":"🛌","aliases":["sleeping_bed"],"tags":[],"category":"People & Body","description":"person in bed","unicode_version":"7.0"},{"emoji":"🧑‍🤝‍🧑","aliases":["people_holding_hands"],"tags":["couple","date"],"category":"People & Body","description":"people holding hands","unicode_version":"12.0"},{"emoji":"👭","aliases":["two_women_holding_hands"],"tags":["couple","date"],"category":"People & Body","description":"women holding hands","unicode_version":"6.0"},{"emoji":"👫","aliases":["couple"],"tags":["date"],"category":"People & Body","description":"woman and man holding hands","unicode_version":"6.0"},{"emoji":"👬","aliases":["two_men_holding_hands"],"tags":["couple","date"],"category":"People & Body","description":"men holding hands","unicode_version":"6.0"},{"emoji":"💏","aliases":["couplekiss"],"tags":[],"category":"People & Body","description":"kiss","unicode_version":"6.0"},{"emoji":"👩‍❤️‍💋‍👨","aliases":["couplekiss_man_woman"],"tags":[],"category":"People & Body","description":"kiss: woman, man","unicode_version":"11.0"},{"emoji":"👨‍❤️‍💋‍👨","aliases":["couplekiss_man_man"],"tags":[],"category":"People & Body","description":"kiss: man, man","unicode_version":"6.0"},{"emoji":"👩‍❤️‍💋‍👩","aliases":["couplekiss_woman_woman"],"tags":[],"category":"People & Body","description":"kiss: woman, woman","unicode_version":"6.0"},{"emoji":"💑","aliases":["couple_with_heart"],"tags":[],"category":"People & Body","description":"couple with heart","unicode_version":"6.0"},{"emoji":"👩‍❤️‍👨","aliases":["couple_with_heart_woman_man"],"tags":[],"category":"People & Body","description":"couple with heart: woman, man","unicode_version":"11.0"},{"emoji":"👨‍❤️‍👨","aliases":["couple_with_heart_man_man"],"tags":[],"category":"People & Body","description":"couple with heart: man, man","unicode_version":"6.0"},{"emoji":"👩‍❤️‍👩","aliases":["couple_with_heart_woman_woman"],"tags":[],"category":"People & Body","description":"couple with heart: woman, woman","unicode_version":"6.0"},{"emoji":"👪","aliases":["family"],"tags":["home","parents","child"],"category":"People & Body","description":"family","unicode_version":"6.0"},{"emoji":"👨‍👩‍👦","aliases":["family_man_woman_boy"],"tags":[],"category":"People & Body","description":"family: man, woman, boy","unicode_version":"11.0"},{"emoji":"👨‍👩‍👧","aliases":["family_man_woman_girl"],"tags":[],"category":"People & Body","description":"family: man, woman, girl","unicode_version":"6.0"},{"emoji":"👨‍👩‍👧‍👦","aliases":["family_man_woman_girl_boy"],"tags":[],"category":"People & Body","description":"family: man, woman, girl, boy","unicode_version":"6.0"},{"emoji":"👨‍👩‍👦‍👦","aliases":["family_man_woman_boy_boy"],"tags":[],"category":"People & Body","description":"family: man, woman, boy, boy","unicode_version":"6.0"},{"emoji":"👨‍👩‍👧‍👧","aliases":["family_man_woman_girl_girl"],"tags":[],"category":"People & Body","description":"family: man, woman, girl, girl","unicode_version":"6.0"},{"emoji":"👨‍👨‍👦","aliases":["family_man_man_boy"],"tags":[],"category":"People & Body","description":"family: man, man, boy","unicode_version":"6.0"},{"emoji":"👨‍👨‍👧","aliases":["family_man_man_girl"],"tags":[],"category":"People & Body","description":"family: man, man, girl","unicode_version":"6.0"},{"emoji":"👨‍👨‍👧‍👦","aliases":["family_man_man_girl_boy"],"tags":[],"category":"People & Body","description":"family: man, man, girl, boy","unicode_version":"6.0"},{"emoji":"👨‍👨‍👦‍👦","aliases":["family_man_man_boy_boy"],"tags":[],"category":"People & Body","description":"family: man, man, boy, boy","unicode_version":"6.0"},{"emoji":"👨‍👨‍👧‍👧","aliases":["family_man_man_girl_girl"],"tags":[],"category":"People & Body","description":"family: man, man, girl, girl","unicode_version":"6.0"},{"emoji":"👩‍👩‍👦","aliases":["family_woman_woman_boy"],"tags":[],"category":"People & Body","description":"family: woman, woman, boy","unicode_version":"6.0"},{"emoji":"👩‍👩‍👧","aliases":["family_woman_woman_girl"],"tags":[],"category":"People & Body","description":"family: woman, woman, girl","unicode_version":"6.0"},{"emoji":"👩‍👩‍👧‍👦","aliases":["family_woman_woman_girl_boy"],"tags":[],"category":"People & Body","description":"family: woman, woman, girl, boy","unicode_version":"6.0"},{"emoji":"👩‍👩‍👦‍👦","aliases":["family_woman_woman_boy_boy"],"tags":[],"category":"People & Body","description":"family: woman, woman, boy, boy","unicode_version":"6.0"},{"emoji":"👩‍👩‍👧‍👧","aliases":["family_woman_woman_girl_girl"],"tags":[],"category":"People & Body","description":"family: woman, woman, girl, girl","unicode_version":"6.0"},{"emoji":"👨‍👦","aliases":["family_man_boy"],"tags":[],"category":"People & Body","description":"family: man, boy","unicode_version":"6.0"},{"emoji":"👨‍👦‍👦","aliases":["family_man_boy_boy"],"tags":[],"category":"People & Body","description":"family: man, boy, boy","unicode_version":"6.0"},{"emoji":"👨‍👧","aliases":["family_man_girl"],"tags":[],"category":"People & Body","description":"family: man, girl","unicode_version":"6.0"},{"emoji":"👨‍👧‍👦","aliases":["family_man_girl_boy"],"tags":[],"category":"People & Body","description":"family: man, girl, boy","unicode_version":"6.0"},{"emoji":"👨‍👧‍👧","aliases":["family_man_girl_girl"],"tags":[],"category":"People & Body","description":"family: man, girl, girl","unicode_version":"6.0"},{"emoji":"👩‍👦","aliases":["family_woman_boy"],"tags":[],"category":"People & Body","description":"family: woman, boy","unicode_version":"6.0"},{"emoji":"👩‍👦‍👦","aliases":["family_woman_boy_boy"],"tags":[],"category":"People & Body","description":"family: woman, boy, boy","unicode_version":"6.0"},{"emoji":"👩‍👧","aliases":["family_woman_girl"],"tags":[],"category":"People & Body","description":"family: woman, girl","unicode_version":"6.0"},{"emoji":"👩‍👧‍👦","aliases":["family_woman_girl_boy"],"tags":[],"category":"People & Body","description":"family: woman, girl, boy","unicode_version":"6.0"},{"emoji":"👩‍👧‍👧","aliases":["family_woman_girl_girl"],"tags":[],"category":"People & Body","description":"family: woman, girl, girl","unicode_version":"6.0"},{"emoji":"🗣️","aliases":["speaking_head"],"tags":[],"category":"People & Body","description":"speaking head","unicode_version":"7.0"},{"emoji":"👤","aliases":["bust_in_silhouette"],"tags":["user"],"category":"People & Body","description":"bust in silhouette","unicode_version":"6.0"},{"emoji":"👥","aliases":["busts_in_silhouette"],"tags":["users","group","team"],"category":"People & Body","description":"busts in silhouette","unicode_version":"6.0"},{"emoji":"🫂","aliases":["people_hugging"],"tags":[],"category":"People & Body","description":"people hugging","unicode_version":"13.0"},{"emoji":"👣","aliases":["footprints"],"tags":["feet","tracks"],"category":"People & Body","description":"footprints","unicode_version":"6.0"},{"emoji":"🐵","aliases":["monkey_face"],"tags":[],"category":"Animals & Nature","description":"monkey face","unicode_version":"6.0"},{"emoji":"🐒","aliases":["monkey"],"tags":[],"category":"Animals & Nature","description":"monkey","unicode_version":"6.0"},{"emoji":"🦍","aliases":["gorilla"],"tags":[],"category":"Animals & Nature","description":"gorilla","unicode_version":"9.0"},{"emoji":"🦧","aliases":["orangutan"],"tags":[],"category":"Animals & Nature","description":"orangutan","unicode_version":"12.0"},{"emoji":"🐶","aliases":["dog"],"tags":["pet"],"category":"Animals & Nature","description":"dog face","unicode_version":"6.0"},{"emoji":"🐕","aliases":["dog2"],"tags":[],"category":"Animals & Nature","description":"dog","unicode_version":"6.0"},{"emoji":"🦮","aliases":["guide_dog"],"tags":[],"category":"Animals & Nature","description":"guide dog","unicode_version":"12.0"},{"emoji":"🐕‍🦺","aliases":["service_dog"],"tags":[],"category":"Animals & Nature","description":"service dog","unicode_version":"12.0"},{"emoji":"🐩","aliases":["poodle"],"tags":["dog"],"category":"Animals & Nature","description":"poodle","unicode_version":"6.0"},{"emoji":"🐺","aliases":["wolf"],"tags":[],"category":"Animals & Nature","description":"wolf","unicode_version":"6.0"},{"emoji":"🦊","aliases":["fox_face"],"tags":[],"category":"Animals & Nature","description":"fox","unicode_version":"9.0"},{"emoji":"🦝","aliases":["raccoon"],"tags":[],"category":"Animals & Nature","description":"raccoon","unicode_version":"11.0"},{"emoji":"🐱","aliases":["cat"],"tags":["pet"],"category":"Animals & Nature","description":"cat face","unicode_version":"6.0"},{"emoji":"🐈","aliases":["cat2"],"tags":[],"category":"Animals & Nature","description":"cat","unicode_version":"6.0"},{"emoji":"🐈‍⬛","aliases":["black_cat"],"tags":[],"category":"Animals & Nature","description":"black cat","unicode_version":"13.0"},{"emoji":"🦁","aliases":["lion"],"tags":[],"category":"Animals & Nature","description":"lion","unicode_version":"8.0"},{"emoji":"🐯","aliases":["tiger"],"tags":[],"category":"Animals & Nature","description":"tiger face","unicode_version":"6.0"},{"emoji":"🐅","aliases":["tiger2"],"tags":[],"category":"Animals & Nature","description":"tiger","unicode_version":"6.0"},{"emoji":"🐆","aliases":["leopard"],"tags":[],"category":"Animals & Nature","description":"leopard","unicode_version":"6.0"},{"emoji":"🐴","aliases":["horse"],"tags":[],"category":"Animals & Nature","description":"horse face","unicode_version":"6.0"},{"emoji":"🐎","aliases":["racehorse"],"tags":["speed"],"category":"Animals & Nature","description":"horse","unicode_version":"6.0"},{"emoji":"🦄","aliases":["unicorn"],"tags":[],"category":"Animals & Nature","description":"unicorn","unicode_version":"8.0"},{"emoji":"🦓","aliases":["zebra"],"tags":[],"category":"Animals & Nature","description":"zebra","unicode_version":"11.0"},{"emoji":"🦌","aliases":["deer"],"tags":[],"category":"Animals & Nature","description":"deer","unicode_version":"9.0"},{"emoji":"🦬","aliases":["bison"],"tags":[],"category":"Animals & Nature","description":"bison","unicode_version":"13.0"},{"emoji":"🐮","aliases":["cow"],"tags":[],"category":"Animals & Nature","description":"cow face","unicode_version":"6.0"},{"emoji":"🐂","aliases":["ox"],"tags":[],"category":"Animals & Nature","description":"ox","unicode_version":"6.0"},{"emoji":"🐃","aliases":["water_buffalo"],"tags":[],"category":"Animals & Nature","description":"water buffalo","unicode_version":"6.0"},{"emoji":"🐄","aliases":["cow2"],"tags":[],"category":"Animals & Nature","description":"cow","unicode_version":"6.0"},{"emoji":"🐷","aliases":["pig"],"tags":[],"category":"Animals & Nature","description":"pig face","unicode_version":"6.0"},{"emoji":"🐖","aliases":["pig2"],"tags":[],"category":"Animals & Nature","description":"pig","unicode_version":"6.0"},{"emoji":"🐗","aliases":["boar"],"tags":[],"category":"Animals & Nature","description":"boar","unicode_version":"6.0"},{"emoji":"🐽","aliases":["pig_nose"],"tags":[],"category":"Animals & Nature","description":"pig nose","unicode_version":"6.0"},{"emoji":"🐏","aliases":["ram"],"tags":[],"category":"Animals & Nature","description":"ram","unicode_version":"6.0"},{"emoji":"🐑","aliases":["sheep"],"tags":[],"category":"Animals & Nature","description":"ewe","unicode_version":"6.0"},{"emoji":"🐐","aliases":["goat"],"tags":[],"category":"Animals & Nature","description":"goat","unicode_version":"6.0"},{"emoji":"🐪","aliases":["dromedary_camel"],"tags":["desert"],"category":"Animals & Nature","description":"camel","unicode_version":"6.0"},{"emoji":"🐫","aliases":["camel"],"tags":[],"category":"Animals & Nature","description":"two-hump camel","unicode_version":"6.0"},{"emoji":"🦙","aliases":["llama"],"tags":[],"category":"Animals & Nature","description":"llama","unicode_version":"11.0"},{"emoji":"🦒","aliases":["giraffe"],"tags":[],"category":"Animals & Nature","description":"giraffe","unicode_version":"11.0"},{"emoji":"🐘","aliases":["elephant"],"tags":[],"category":"Animals & Nature","description":"elephant","unicode_version":"6.0"},{"emoji":"🦣","aliases":["mammoth"],"tags":[],"category":"Animals & Nature","description":"mammoth","unicode_version":"13.0"},{"emoji":"🦏","aliases":["rhinoceros"],"tags":[],"category":"Animals & Nature","description":"rhinoceros","unicode_version":"9.0"},{"emoji":"🦛","aliases":["hippopotamus"],"tags":[],"category":"Animals & Nature","description":"hippopotamus","unicode_version":"11.0"},{"emoji":"🐭","aliases":["mouse"],"tags":[],"category":"Animals & Nature","description":"mouse face","unicode_version":"6.0"},{"emoji":"🐁","aliases":["mouse2"],"tags":[],"category":"Animals & Nature","description":"mouse","unicode_version":"6.0"},{"emoji":"🐀","aliases":["rat"],"tags":[],"category":"Animals & Nature","description":"rat","unicode_version":"6.0"},{"emoji":"🐹","aliases":["hamster"],"tags":["pet"],"category":"Animals & Nature","description":"hamster","unicode_version":"6.0"},{"emoji":"🐰","aliases":["rabbit"],"tags":["bunny"],"category":"Animals & Nature","description":"rabbit face","unicode_version":"6.0"},{"emoji":"🐇","aliases":["rabbit2"],"tags":[],"category":"Animals & Nature","description":"rabbit","unicode_version":"6.0"},{"emoji":"🐿️","aliases":["chipmunk"],"tags":[],"category":"Animals & Nature","description":"chipmunk","unicode_version":"7.0"},{"emoji":"🦫","aliases":["beaver"],"tags":[],"category":"Animals & Nature","description":"beaver","unicode_version":"13.0"},{"emoji":"🦔","aliases":["hedgehog"],"tags":[],"category":"Animals & Nature","description":"hedgehog","unicode_version":"11.0"},{"emoji":"🦇","aliases":["bat"],"tags":[],"category":"Animals & Nature","description":"bat","unicode_version":"9.0"},{"emoji":"🐻","aliases":["bear"],"tags":[],"category":"Animals & Nature","description":"bear","unicode_version":"6.0"},{"emoji":"🐻‍❄️","aliases":["polar_bear"],"tags":[],"category":"Animals & Nature","description":"polar bear","unicode_version":"13.0"},{"emoji":"🐨","aliases":["koala"],"tags":[],"category":"Animals & Nature","description":"koala","unicode_version":"6.0"},{"emoji":"🐼","aliases":["panda_face"],"tags":[],"category":"Animals & Nature","description":"panda","unicode_version":"6.0"},{"emoji":"🦥","aliases":["sloth"],"tags":[],"category":"Animals & Nature","description":"sloth","unicode_version":"12.0"},{"emoji":"🦦","aliases":["otter"],"tags":[],"category":"Animals & Nature","description":"otter","unicode_version":"12.0"},{"emoji":"🦨","aliases":["skunk"],"tags":[],"category":"Animals & Nature","description":"skunk","unicode_version":"12.0"},{"emoji":"🦘","aliases":["kangaroo"],"tags":[],"category":"Animals & Nature","description":"kangaroo","unicode_version":"11.0"},{"emoji":"🦡","aliases":["badger"],"tags":[],"category":"Animals & Nature","description":"badger","unicode_version":"11.0"},{"emoji":"🐾","aliases":["feet","paw_prints"],"tags":[],"category":"Animals & Nature","description":"paw prints","unicode_version":"6.0"},{"emoji":"🦃","aliases":["turkey"],"tags":["thanksgiving"],"category":"Animals & Nature","description":"turkey","unicode_version":"8.0"},{"emoji":"🐔","aliases":["chicken"],"tags":[],"category":"Animals & Nature","description":"chicken","unicode_version":"6.0"},{"emoji":"🐓","aliases":["rooster"],"tags":[],"category":"Animals & Nature","description":"rooster","unicode_version":"6.0"},{"emoji":"🐣","aliases":["hatching_chick"],"tags":[],"category":"Animals & Nature","description":"hatching chick","unicode_version":"6.0"},{"emoji":"🐤","aliases":["baby_chick"],"tags":[],"category":"Animals & Nature","description":"baby chick","unicode_version":"6.0"},{"emoji":"🐥","aliases":["hatched_chick"],"tags":[],"category":"Animals & Nature","description":"front-facing baby chick","unicode_version":"6.0"},{"emoji":"🐦","aliases":["bird"],"tags":[],"category":"Animals & Nature","description":"bird","unicode_version":"6.0"},{"emoji":"🐧","aliases":["penguin"],"tags":[],"category":"Animals & Nature","description":"penguin","unicode_version":"6.0"},{"emoji":"🕊️","aliases":["dove"],"tags":["peace"],"category":"Animals & Nature","description":"dove","unicode_version":"7.0"},{"emoji":"🦅","aliases":["eagle"],"tags":[],"category":"Animals & Nature","description":"eagle","unicode_version":"9.0"},{"emoji":"🦆","aliases":["duck"],"tags":[],"category":"Animals & Nature","description":"duck","unicode_version":"9.0"},{"emoji":"🦢","aliases":["swan"],"tags":[],"category":"Animals & Nature","description":"swan","unicode_version":"11.0"},{"emoji":"🦉","aliases":["owl"],"tags":[],"category":"Animals & Nature","description":"owl","unicode_version":"9.0"},{"emoji":"🦤","aliases":["dodo"],"tags":[],"category":"Animals & Nature","description":"dodo","unicode_version":"13.0"},{"emoji":"🪶","aliases":["feather"],"tags":[],"category":"Animals & Nature","description":"feather","unicode_version":"13.0"},{"emoji":"🦩","aliases":["flamingo"],"tags":[],"category":"Animals & Nature","description":"flamingo","unicode_version":"12.0"},{"emoji":"🦚","aliases":["peacock"],"tags":[],"category":"Animals & Nature","description":"peacock","unicode_version":"11.0"},{"emoji":"🦜","aliases":["parrot"],"tags":[],"category":"Animals & Nature","description":"parrot","unicode_version":"11.0"},{"emoji":"🐸","aliases":["frog"],"tags":[],"category":"Animals & Nature","description":"frog","unicode_version":"6.0"},{"emoji":"🐊","aliases":["crocodile"],"tags":[],"category":"Animals & Nature","description":"crocodile","unicode_version":"6.0"},{"emoji":"🐢","aliases":["turtle"],"tags":["slow"],"category":"Animals & Nature","description":"turtle","unicode_version":"6.0"},{"emoji":"🦎","aliases":["lizard"],"tags":[],"category":"Animals & Nature","description":"lizard","unicode_version":"9.0"},{"emoji":"🐍","aliases":["snake"],"tags":[],"category":"Animals & Nature","description":"snake","unicode_version":"6.0"},{"emoji":"🐲","aliases":["dragon_face"],"tags":[],"category":"Animals & Nature","description":"dragon face","unicode_version":"6.0"},{"emoji":"🐉","aliases":["dragon"],"tags":[],"category":"Animals & Nature","description":"dragon","unicode_version":"6.0"},{"emoji":"🦕","aliases":["sauropod"],"tags":["dinosaur"],"category":"Animals & Nature","description":"sauropod","unicode_version":"11.0"},{"emoji":"🦖","aliases":["t-rex"],"tags":["dinosaur"],"category":"Animals & Nature","description":"T-Rex","unicode_version":"11.0"},{"emoji":"🐳","aliases":["whale"],"tags":["sea"],"category":"Animals & Nature","description":"spouting whale","unicode_version":"6.0"},{"emoji":"🐋","aliases":["whale2"],"tags":[],"category":"Animals & Nature","description":"whale","unicode_version":"6.0"},{"emoji":"🐬","aliases":["dolphin","flipper"],"tags":[],"category":"Animals & Nature","description":"dolphin","unicode_version":"6.0"},{"emoji":"🦭","aliases":["seal"],"tags":[],"category":"Animals & Nature","description":"seal","unicode_version":"13.0"},{"emoji":"🐟","aliases":["fish"],"tags":[],"category":"Animals & Nature","description":"fish","unicode_version":"6.0"},{"emoji":"🐠","aliases":["tropical_fish"],"tags":[],"category":"Animals & Nature","description":"tropical fish","unicode_version":"6.0"},{"emoji":"🐡","aliases":["blowfish"],"tags":[],"category":"Animals & Nature","description":"blowfish","unicode_version":"6.0"},{"emoji":"🦈","aliases":["shark"],"tags":[],"category":"Animals & Nature","description":"shark","unicode_version":"9.0"},{"emoji":"🐙","aliases":["octopus"],"tags":[],"category":"Animals & Nature","description":"octopus","unicode_version":"6.0"},{"emoji":"🐚","aliases":["shell"],"tags":["sea","beach"],"category":"Animals & Nature","description":"spiral shell","unicode_version":"6.0"},{"emoji":"🐌","aliases":["snail"],"tags":["slow"],"category":"Animals & Nature","description":"snail","unicode_version":"6.0"},{"emoji":"🦋","aliases":["butterfly"],"tags":[],"category":"Animals & Nature","description":"butterfly","unicode_version":"9.0"},{"emoji":"🐛","aliases":["bug"],"tags":[],"category":"Animals & Nature","description":"bug","unicode_version":"6.0"},{"emoji":"🐜","aliases":["ant"],"tags":[],"category":"Animals & Nature","description":"ant","unicode_version":"6.0"},{"emoji":"🐝","aliases":["bee","honeybee"],"tags":[],"category":"Animals & Nature","description":"honeybee","unicode_version":"6.0"},{"emoji":"🪲","aliases":["beetle"],"tags":[],"category":"Animals & Nature","description":"beetle","unicode_version":"13.0"},{"emoji":"🐞","aliases":["lady_beetle"],"tags":["bug"],"category":"Animals & Nature","description":"lady beetle","unicode_version":"6.0"},{"emoji":"🦗","aliases":["cricket"],"tags":[],"category":"Animals & Nature","description":"cricket","unicode_version":"11.0"},{"emoji":"🪳","aliases":["cockroach"],"tags":[],"category":"Animals & Nature","description":"cockroach","unicode_version":"13.0"},{"emoji":"🕷️","aliases":["spider"],"tags":[],"category":"Animals & Nature","description":"spider","unicode_version":"7.0"},{"emoji":"🕸️","aliases":["spider_web"],"tags":[],"category":"Animals & Nature","description":"spider web","unicode_version":"7.0"},{"emoji":"🦂","aliases":["scorpion"],"tags":[],"category":"Animals & Nature","description":"scorpion","unicode_version":"8.0"},{"emoji":"🦟","aliases":["mosquito"],"tags":[],"category":"Animals & Nature","description":"mosquito","unicode_version":"11.0"},{"emoji":"🪰","aliases":["fly"],"tags":[],"category":"Animals & Nature","description":"fly","unicode_version":"13.0"},{"emoji":"🪱","aliases":["worm"],"tags":[],"category":"Animals & Nature","description":"worm","unicode_version":"13.0"},{"emoji":"🦠","aliases":["microbe"],"tags":["germ"],"category":"Animals & Nature","description":"microbe","unicode_version":"11.0"},{"emoji":"💐","aliases":["bouquet"],"tags":["flowers"],"category":"Animals & Nature","description":"bouquet","unicode_version":"6.0"},{"emoji":"🌸","aliases":["cherry_blossom"],"tags":["flower","spring"],"category":"Animals & Nature","description":"cherry blossom","unicode_version":"6.0"},{"emoji":"💮","aliases":["white_flower"],"tags":[],"category":"Animals & Nature","description":"white flower","unicode_version":"6.0"},{"emoji":"🏵️","aliases":["rosette"],"tags":[],"category":"Animals & Nature","description":"rosette","unicode_version":"7.0"},{"emoji":"🌹","aliases":["rose"],"tags":["flower"],"category":"Animals & Nature","description":"rose","unicode_version":"6.0"},{"emoji":"🥀","aliases":["wilted_flower"],"tags":[],"category":"Animals & Nature","description":"wilted flower","unicode_version":"9.0"},{"emoji":"🌺","aliases":["hibiscus"],"tags":[],"category":"Animals & Nature","description":"hibiscus","unicode_version":"6.0"},{"emoji":"🌻","aliases":["sunflower"],"tags":[],"category":"Animals & Nature","description":"sunflower","unicode_version":"6.0"},{"emoji":"🌼","aliases":["blossom"],"tags":[],"category":"Animals & Nature","description":"blossom","unicode_version":"6.0"},{"emoji":"🌷","aliases":["tulip"],"tags":["flower"],"category":"Animals & Nature","description":"tulip","unicode_version":"6.0"},{"emoji":"🌱","aliases":["seedling"],"tags":["plant"],"category":"Animals & Nature","description":"seedling","unicode_version":"6.0"},{"emoji":"🪴","aliases":["potted_plant"],"tags":[],"category":"Animals & Nature","description":"potted plant","unicode_version":"13.0"},{"emoji":"🌲","aliases":["evergreen_tree"],"tags":["wood"],"category":"Animals & Nature","description":"evergreen tree","unicode_version":"6.0"},{"emoji":"🌳","aliases":["deciduous_tree"],"tags":["wood"],"category":"Animals & Nature","description":"deciduous tree","unicode_version":"6.0"},{"emoji":"🌴","aliases":["palm_tree"],"tags":[],"category":"Animals & Nature","description":"palm tree","unicode_version":"6.0"},{"emoji":"🌵","aliases":["cactus"],"tags":[],"category":"Animals & Nature","description":"cactus","unicode_version":"6.0"},{"emoji":"🌾","aliases":["ear_of_rice"],"tags":[],"category":"Animals & Nature","description":"sheaf of rice","unicode_version":"6.0"},{"emoji":"🌿","aliases":["herb"],"tags":[],"category":"Animals & Nature","description":"herb","unicode_version":"6.0"},{"emoji":"☘️","aliases":["shamrock"],"tags":[],"category":"Animals & Nature","description":"shamrock","unicode_version":"4.1"},{"emoji":"🍀","aliases":["four_leaf_clover"],"tags":["luck"],"category":"Animals & Nature","description":"four leaf clover","unicode_version":"6.0"},{"emoji":"🍁","aliases":["maple_leaf"],"tags":["canada"],"category":"Animals & Nature","description":"maple leaf","unicode_version":"6.0"},{"emoji":"🍂","aliases":["fallen_leaf"],"tags":["autumn"],"category":"Animals & Nature","description":"fallen leaf","unicode_version":"6.0"},{"emoji":"🍃","aliases":["leaves"],"tags":["leaf"],"category":"Animals & Nature","description":"leaf fluttering in wind","unicode_version":"6.0"},{"emoji":"🍇","aliases":["grapes"],"tags":[],"category":"Food & Drink","description":"grapes","unicode_version":"6.0"},{"emoji":"🍈","aliases":["melon"],"tags":[],"category":"Food & Drink","description":"melon","unicode_version":"6.0"},{"emoji":"🍉","aliases":["watermelon"],"tags":[],"category":"Food & Drink","description":"watermelon","unicode_version":"6.0"},{"emoji":"🍊","aliases":["tangerine","orange","mandarin"],"tags":[],"category":"Food & Drink","description":"tangerine","unicode_version":"6.0"},{"emoji":"🍋","aliases":["lemon"],"tags":[],"category":"Food & Drink","description":"lemon","unicode_version":"6.0"},{"emoji":"🍌","aliases":["banana"],"tags":["fruit"],"category":"Food & Drink","description":"banana","unicode_version":"6.0"},{"emoji":"🍍","aliases":["pineapple"],"tags":[],"category":"Food & Drink","description":"pineapple","unicode_version":"6.0"},{"emoji":"🥭","aliases":["mango"],"tags":[],"category":"Food & Drink","description":"mango","unicode_version":"11.0"},{"emoji":"🍎","aliases":["apple"],"tags":[],"category":"Food & Drink","description":"red apple","unicode_version":"6.0"},{"emoji":"🍏","aliases":["green_apple"],"tags":["fruit"],"category":"Food & Drink","description":"green apple","unicode_version":"6.0"},{"emoji":"🍐","aliases":["pear"],"tags":[],"category":"Food & Drink","description":"pear","unicode_version":"6.0"},{"emoji":"🍑","aliases":["peach"],"tags":[],"category":"Food & Drink","description":"peach","unicode_version":"6.0"},{"emoji":"🍒","aliases":["cherries"],"tags":["fruit"],"category":"Food & Drink","description":"cherries","unicode_version":"6.0"},{"emoji":"🍓","aliases":["strawberry"],"tags":["fruit"],"category":"Food & Drink","description":"strawberry","unicode_version":"6.0"},{"emoji":"🫐","aliases":["blueberries"],"tags":[],"category":"Food & Drink","description":"blueberries","unicode_version":"13.0"},{"emoji":"🥝","aliases":["kiwi_fruit"],"tags":[],"category":"Food & Drink","description":"kiwi fruit","unicode_version":"9.0"},{"emoji":"🍅","aliases":["tomato"],"tags":[],"category":"Food & Drink","description":"tomato","unicode_version":"6.0"},{"emoji":"🫒","aliases":["olive"],"tags":[],"category":"Food & Drink","description":"olive","unicode_version":"13.0"},{"emoji":"🥥","aliases":["coconut"],"tags":[],"category":"Food & Drink","description":"coconut","unicode_version":"11.0"},{"emoji":"🥑","aliases":["avocado"],"tags":[],"category":"Food & Drink","description":"avocado","unicode_version":"9.0"},{"emoji":"🍆","aliases":["eggplant"],"tags":["aubergine"],"category":"Food & Drink","description":"eggplant","unicode_version":"6.0"},{"emoji":"🥔","aliases":["potato"],"tags":[],"category":"Food & Drink","description":"potato","unicode_version":"9.0"},{"emoji":"🥕","aliases":["carrot"],"tags":[],"category":"Food & Drink","description":"carrot","unicode_version":"9.0"},{"emoji":"🌽","aliases":["corn"],"tags":[],"category":"Food & Drink","description":"ear of corn","unicode_version":"6.0"},{"emoji":"🌶️","aliases":["hot_pepper"],"tags":["spicy"],"category":"Food & Drink","description":"hot pepper","unicode_version":"7.0"},{"emoji":"🫑","aliases":["bell_pepper"],"tags":[],"category":"Food & Drink","description":"bell pepper","unicode_version":"13.0"},{"emoji":"🥒","aliases":["cucumber"],"tags":[],"category":"Food & Drink","description":"cucumber","unicode_version":"9.0"},{"emoji":"🥬","aliases":["leafy_green"],"tags":[],"category":"Food & Drink","description":"leafy green","unicode_version":"11.0"},{"emoji":"🥦","aliases":["broccoli"],"tags":[],"category":"Food & Drink","description":"broccoli","unicode_version":"11.0"},{"emoji":"🧄","aliases":["garlic"],"tags":[],"category":"Food & Drink","description":"garlic","unicode_version":"12.0"},{"emoji":"🧅","aliases":["onion"],"tags":[],"category":"Food & Drink","description":"onion","unicode_version":"12.0"},{"emoji":"🍄","aliases":["mushroom"],"tags":[],"category":"Food & Drink","description":"mushroom","unicode_version":"6.0"},{"emoji":"🥜","aliases":["peanuts"],"tags":[],"category":"Food & Drink","description":"peanuts","unicode_version":"9.0"},{"emoji":"🌰","aliases":["chestnut"],"tags":[],"category":"Food & Drink","description":"chestnut","unicode_version":"6.0"},{"emoji":"🍞","aliases":["bread"],"tags":["toast"],"category":"Food & Drink","description":"bread","unicode_version":"6.0"},{"emoji":"🥐","aliases":["croissant"],"tags":[],"category":"Food & Drink","description":"croissant","unicode_version":"9.0"},{"emoji":"🥖","aliases":["baguette_bread"],"tags":[],"category":"Food & Drink","description":"baguette bread","unicode_version":"9.0"},{"emoji":"🫓","aliases":["flatbread"],"tags":[],"category":"Food & Drink","description":"flatbread","unicode_version":"13.0"},{"emoji":"🥨","aliases":["pretzel"],"tags":[],"category":"Food & Drink","description":"pretzel","unicode_version":"11.0"},{"emoji":"🥯","aliases":["bagel"],"tags":[],"category":"Food & Drink","description":"bagel","unicode_version":"11.0"},{"emoji":"🥞","aliases":["pancakes"],"tags":[],"category":"Food & Drink","description":"pancakes","unicode_version":"9.0"},{"emoji":"🧇","aliases":["waffle"],"tags":[],"category":"Food & Drink","description":"waffle","unicode_version":"12.0"},{"emoji":"🧀","aliases":["cheese"],"tags":[],"category":"Food & Drink","description":"cheese wedge","unicode_version":"8.0"},{"emoji":"🍖","aliases":["meat_on_bone"],"tags":[],"category":"Food & Drink","description":"meat on bone","unicode_version":"6.0"},{"emoji":"🍗","aliases":["poultry_leg"],"tags":["meat","chicken"],"category":"Food & Drink","description":"poultry leg","unicode_version":"6.0"},{"emoji":"🥩","aliases":["cut_of_meat"],"tags":[],"category":"Food & Drink","description":"cut of meat","unicode_version":"11.0"},{"emoji":"🥓","aliases":["bacon"],"tags":[],"category":"Food & Drink","description":"bacon","unicode_version":"9.0"},{"emoji":"🍔","aliases":["hamburger"],"tags":["burger"],"category":"Food & Drink","description":"hamburger","unicode_version":"6.0"},{"emoji":"🍟","aliases":["fries"],"tags":[],"category":"Food & Drink","description":"french fries","unicode_version":"6.0"},{"emoji":"🍕","aliases":["pizza"],"tags":[],"category":"Food & Drink","description":"pizza","unicode_version":"6.0"},{"emoji":"🌭","aliases":["hotdog"],"tags":[],"category":"Food & Drink","description":"hot dog","unicode_version":"8.0"},{"emoji":"🥪","aliases":["sandwich"],"tags":[],"category":"Food & Drink","description":"sandwich","unicode_version":"11.0"},{"emoji":"🌮","aliases":["taco"],"tags":[],"category":"Food & Drink","description":"taco","unicode_version":"8.0"},{"emoji":"🌯","aliases":["burrito"],"tags":[],"category":"Food & Drink","description":"burrito","unicode_version":"8.0"},{"emoji":"🫔","aliases":["tamale"],"tags":[],"category":"Food & Drink","description":"tamale","unicode_version":"13.0"},{"emoji":"🥙","aliases":["stuffed_flatbread"],"tags":[],"category":"Food & Drink","description":"stuffed flatbread","unicode_version":"9.0"},{"emoji":"🧆","aliases":["falafel"],"tags":[],"category":"Food & Drink","description":"falafel","unicode_version":"12.0"},{"emoji":"🥚","aliases":["egg"],"tags":[],"category":"Food & Drink","description":"egg","unicode_version":"9.0"},{"emoji":"🍳","aliases":["fried_egg"],"tags":["breakfast"],"category":"Food & Drink","description":"cooking","unicode_version":"6.0"},{"emoji":"🥘","aliases":["shallow_pan_of_food"],"tags":["paella","curry"],"category":"Food & Drink","description":"shallow pan of food","unicode_version":""},{"emoji":"🍲","aliases":["stew"],"tags":[],"category":"Food & Drink","description":"pot of food","unicode_version":"6.0"},{"emoji":"🫕","aliases":["fondue"],"tags":[],"category":"Food & Drink","description":"fondue","unicode_version":"13.0"},{"emoji":"🥣","aliases":["bowl_with_spoon"],"tags":[],"category":"Food & Drink","description":"bowl with spoon","unicode_version":"11.0"},{"emoji":"🥗","aliases":["green_salad"],"tags":[],"category":"Food & Drink","description":"green salad","unicode_version":"9.0"},{"emoji":"🍿","aliases":["popcorn"],"tags":[],"category":"Food & Drink","description":"popcorn","unicode_version":"8.0"},{"emoji":"🧈","aliases":["butter"],"tags":[],"category":"Food & Drink","description":"butter","unicode_version":"12.0"},{"emoji":"🧂","aliases":["salt"],"tags":[],"category":"Food & Drink","description":"salt","unicode_version":"11.0"},{"emoji":"🥫","aliases":["canned_food"],"tags":[],"category":"Food & Drink","description":"canned food","unicode_version":"11.0"},{"emoji":"🍱","aliases":["bento"],"tags":[],"category":"Food & Drink","description":"bento box","unicode_version":"6.0"},{"emoji":"🍘","aliases":["rice_cracker"],"tags":[],"category":"Food & Drink","description":"rice cracker","unicode_version":"6.0"},{"emoji":"🍙","aliases":["rice_ball"],"tags":[],"category":"Food & Drink","description":"rice ball","unicode_version":"6.0"},{"emoji":"🍚","aliases":["rice"],"tags":[],"category":"Food & Drink","description":"cooked rice","unicode_version":"6.0"},{"emoji":"🍛","aliases":["curry"],"tags":[],"category":"Food & Drink","description":"curry rice","unicode_version":"6.0"},{"emoji":"🍜","aliases":["ramen"],"tags":["noodle"],"category":"Food & Drink","description":"steaming bowl","unicode_version":"6.0"},{"emoji":"🍝","aliases":["spaghetti"],"tags":["pasta"],"category":"Food & Drink","description":"spaghetti","unicode_version":"6.0"},{"emoji":"🍠","aliases":["sweet_potato"],"tags":[],"category":"Food & Drink","description":"roasted sweet potato","unicode_version":"6.0"},{"emoji":"🍢","aliases":["oden"],"tags":[],"category":"Food & Drink","description":"oden","unicode_version":"6.0"},{"emoji":"🍣","aliases":["sushi"],"tags":[],"category":"Food & Drink","description":"sushi","unicode_version":"6.0"},{"emoji":"🍤","aliases":["fried_shrimp"],"tags":["tempura"],"category":"Food & Drink","description":"fried shrimp","unicode_version":"6.0"},{"emoji":"🍥","aliases":["fish_cake"],"tags":[],"category":"Food & Drink","description":"fish cake with swirl","unicode_version":"6.0"},{"emoji":"🥮","aliases":["moon_cake"],"tags":[],"category":"Food & Drink","description":"moon cake","unicode_version":"11.0"},{"emoji":"🍡","aliases":["dango"],"tags":[],"category":"Food & Drink","description":"dango","unicode_version":"6.0"},{"emoji":"🥟","aliases":["dumpling"],"tags":[],"category":"Food & Drink","description":"dumpling","unicode_version":"11.0"},{"emoji":"🥠","aliases":["fortune_cookie"],"tags":[],"category":"Food & Drink","description":"fortune cookie","unicode_version":"11.0"},{"emoji":"🥡","aliases":["takeout_box"],"tags":[],"category":"Food & Drink","description":"takeout box","unicode_version":"11.0"},{"emoji":"🦀","aliases":["crab"],"tags":[],"category":"Food & Drink","description":"crab","unicode_version":"8.0"},{"emoji":"🦞","aliases":["lobster"],"tags":[],"category":"Food & Drink","description":"lobster","unicode_version":"11.0"},{"emoji":"🦐","aliases":["shrimp"],"tags":[],"category":"Food & Drink","description":"shrimp","unicode_version":"9.0"},{"emoji":"🦑","aliases":["squid"],"tags":[],"category":"Food & Drink","description":"squid","unicode_version":"9.0"},{"emoji":"🦪","aliases":["oyster"],"tags":[],"category":"Food & Drink","description":"oyster","unicode_version":"12.0"},{"emoji":"🍦","aliases":["icecream"],"tags":[],"category":"Food & Drink","description":"soft ice cream","unicode_version":"6.0"},{"emoji":"🍧","aliases":["shaved_ice"],"tags":[],"category":"Food & Drink","description":"shaved ice","unicode_version":"6.0"},{"emoji":"🍨","aliases":["ice_cream"],"tags":[],"category":"Food & Drink","description":"ice cream","unicode_version":"6.0"},{"emoji":"🍩","aliases":["doughnut"],"tags":[],"category":"Food & Drink","description":"doughnut","unicode_version":"6.0"},{"emoji":"🍪","aliases":["cookie"],"tags":[],"category":"Food & Drink","description":"cookie","unicode_version":"6.0"},{"emoji":"🎂","aliases":["birthday"],"tags":["party"],"category":"Food & Drink","description":"birthday cake","unicode_version":"6.0"},{"emoji":"🍰","aliases":["cake"],"tags":["dessert"],"category":"Food & Drink","description":"shortcake","unicode_version":"6.0"},{"emoji":"🧁","aliases":["cupcake"],"tags":[],"category":"Food & Drink","description":"cupcake","unicode_version":"11.0"},{"emoji":"🥧","aliases":["pie"],"tags":[],"category":"Food & Drink","description":"pie","unicode_version":"11.0"},{"emoji":"🍫","aliases":["chocolate_bar"],"tags":[],"category":"Food & Drink","description":"chocolate bar","unicode_version":"6.0"},{"emoji":"🍬","aliases":["candy"],"tags":["sweet"],"category":"Food & Drink","description":"candy","unicode_version":"6.0"},{"emoji":"🍭","aliases":["lollipop"],"tags":[],"category":"Food & Drink","description":"lollipop","unicode_version":"6.0"},{"emoji":"🍮","aliases":["custard"],"tags":[],"category":"Food & Drink","description":"custard","unicode_version":"6.0"},{"emoji":"🍯","aliases":["honey_pot"],"tags":[],"category":"Food & Drink","description":"honey pot","unicode_version":"6.0"},{"emoji":"🍼","aliases":["baby_bottle"],"tags":["milk"],"category":"Food & Drink","description":"baby bottle","unicode_version":"6.0"},{"emoji":"🥛","aliases":["milk_glass"],"tags":[],"category":"Food & Drink","description":"glass of milk","unicode_version":"9.0"},{"emoji":"☕","aliases":["coffee"],"tags":["cafe","espresso"],"category":"Food & Drink","description":"hot beverage","unicode_version":"4.0"},{"emoji":"🫖","aliases":["teapot"],"tags":[],"category":"Food & Drink","description":"teapot","unicode_version":"13.0"},{"emoji":"🍵","aliases":["tea"],"tags":["green","breakfast"],"category":"Food & Drink","description":"teacup without handle","unicode_version":"6.0"},{"emoji":"🍶","aliases":["sake"],"tags":[],"category":"Food & Drink","description":"sake","unicode_version":"6.0"},{"emoji":"🍾","aliases":["champagne"],"tags":["bottle","bubbly","celebration"],"category":"Food & Drink","description":"bottle with popping cork","unicode_version":"8.0"},{"emoji":"🍷","aliases":["wine_glass"],"tags":[],"category":"Food & Drink","description":"wine glass","unicode_version":"6.0"},{"emoji":"🍸","aliases":["cocktail"],"tags":["drink"],"category":"Food & Drink","description":"cocktail glass","unicode_version":"6.0"},{"emoji":"🍹","aliases":["tropical_drink"],"tags":["summer","vacation"],"category":"Food & Drink","description":"tropical drink","unicode_version":"6.0"},{"emoji":"🍺","aliases":["beer"],"tags":["drink"],"category":"Food & Drink","description":"beer mug","unicode_version":"6.0"},{"emoji":"🍻","aliases":["beers"],"tags":["drinks"],"category":"Food & Drink","description":"clinking beer mugs","unicode_version":"6.0"},{"emoji":"🥂","aliases":["clinking_glasses"],"tags":["cheers","toast"],"category":"Food & Drink","description":"clinking glasses","unicode_version":"9.0"},{"emoji":"🥃","aliases":["tumbler_glass"],"tags":["whisky"],"category":"Food & Drink","description":"tumbler glass","unicode_version":"9.0"},{"emoji":"🥤","aliases":["cup_with_straw"],"tags":[],"category":"Food & Drink","description":"cup with straw","unicode_version":"11.0"},{"emoji":"🧋","aliases":["bubble_tea"],"tags":[],"category":"Food & Drink","description":"bubble tea","unicode_version":"13.0"},{"emoji":"🧃","aliases":["beverage_box"],"tags":[],"category":"Food & Drink","description":"beverage box","unicode_version":"12.0"},{"emoji":"🧉","aliases":["mate"],"tags":[],"category":"Food & Drink","description":"mate","unicode_version":"12.0"},{"emoji":"🧊","aliases":["ice_cube"],"tags":[],"category":"Food & Drink","description":"ice","unicode_version":"12.0"},{"emoji":"🥢","aliases":["chopsticks"],"tags":[],"category":"Food & Drink","description":"chopsticks","unicode_version":"11.0"},{"emoji":"🍽️","aliases":["plate_with_cutlery"],"tags":["dining","dinner"],"category":"Food & Drink","description":"fork and knife with plate","unicode_version":"7.0"},{"emoji":"🍴","aliases":["fork_and_knife"],"tags":["cutlery"],"category":"Food & Drink","description":"fork and knife","unicode_version":"6.0"},{"emoji":"🥄","aliases":["spoon"],"tags":[],"category":"Food & Drink","description":"spoon","unicode_version":"9.0"},{"emoji":"🔪","aliases":["hocho","knife"],"tags":["cut","chop"],"category":"Food & Drink","description":"kitchen knife","unicode_version":"6.0"},{"emoji":"🏺","aliases":["amphora"],"tags":[],"category":"Food & Drink","description":"amphora","unicode_version":"8.0"},{"emoji":"🌍","aliases":["earth_africa"],"tags":["globe","world","international"],"category":"Travel & Places","description":"globe showing Europe-Africa","unicode_version":"6.0"},{"emoji":"🌎","aliases":["earth_americas"],"tags":["globe","world","international"],"category":"Travel & Places","description":"globe showing Americas","unicode_version":"6.0"},{"emoji":"🌏","aliases":["earth_asia"],"tags":["globe","world","international"],"category":"Travel & Places","description":"globe showing Asia-Australia","unicode_version":"6.0"},{"emoji":"🌐","aliases":["globe_with_meridians"],"tags":["world","global","international"],"category":"Travel & Places","description":"globe with meridians","unicode_version":"6.0"},{"emoji":"🗺️","aliases":["world_map"],"tags":["travel"],"category":"Travel & Places","description":"world map","unicode_version":"7.0"},{"emoji":"🗾","aliases":["japan"],"tags":[],"category":"Travel & Places","description":"map of Japan","unicode_version":"6.0"},{"emoji":"🧭","aliases":["compass"],"tags":[],"category":"Travel & Places","description":"compass","unicode_version":"11.0"},{"emoji":"🏔️","aliases":["mountain_snow"],"tags":[],"category":"Travel & Places","description":"snow-capped mountain","unicode_version":"7.0"},{"emoji":"⛰️","aliases":["mountain"],"tags":[],"category":"Travel & Places","description":"mountain","unicode_version":"5.2"},{"emoji":"🌋","aliases":["volcano"],"tags":[],"category":"Travel & Places","description":"volcano","unicode_version":"6.0"},{"emoji":"🗻","aliases":["mount_fuji"],"tags":[],"category":"Travel & Places","description":"mount fuji","unicode_version":"6.0"},{"emoji":"🏕️","aliases":["camping"],"tags":[],"category":"Travel & Places","description":"camping","unicode_version":"7.0"},{"emoji":"🏖️","aliases":["beach_umbrella"],"tags":[],"category":"Travel & Places","description":"beach with umbrella","unicode_version":"7.0"},{"emoji":"🏜️","aliases":["desert"],"tags":[],"category":"Travel & Places","description":"desert","unicode_version":"7.0"},{"emoji":"🏝️","aliases":["desert_island"],"tags":[],"category":"Travel & Places","description":"desert island","unicode_version":"7.0"},{"emoji":"🏞️","aliases":["national_park"],"tags":[],"category":"Travel & Places","description":"national park","unicode_version":"7.0"},{"emoji":"🏟️","aliases":["stadium"],"tags":[],"category":"Travel & Places","description":"stadium","unicode_version":"7.0"},{"emoji":"🏛️","aliases":["classical_building"],"tags":[],"category":"Travel & Places","description":"classical building","unicode_version":"7.0"},{"emoji":"🏗️","aliases":["building_construction"],"tags":[],"category":"Travel & Places","description":"building construction","unicode_version":"7.0"},{"emoji":"🧱","aliases":["bricks"],"tags":[],"category":"Travel & Places","description":"brick","unicode_version":"11.0"},{"emoji":"🪨","aliases":["rock"],"tags":[],"category":"Travel & Places","description":"rock","unicode_version":"13.0"},{"emoji":"🪵","aliases":["wood"],"tags":[],"category":"Travel & Places","description":"wood","unicode_version":"13.0"},{"emoji":"🛖","aliases":["hut"],"tags":[],"category":"Travel & Places","description":"hut","unicode_version":"13.0"},{"emoji":"🏘️","aliases":["houses"],"tags":[],"category":"Travel & Places","description":"houses","unicode_version":"7.0"},{"emoji":"🏚️","aliases":["derelict_house"],"tags":[],"category":"Travel & Places","description":"derelict house","unicode_version":"7.0"},{"emoji":"🏠","aliases":["house"],"tags":[],"category":"Travel & Places","description":"house","unicode_version":"6.0"},{"emoji":"🏡","aliases":["house_with_garden"],"tags":[],"category":"Travel & Places","description":"house with garden","unicode_version":"6.0"},{"emoji":"🏢","aliases":["office"],"tags":[],"category":"Travel & Places","description":"office building","unicode_version":"6.0"},{"emoji":"🏣","aliases":["post_office"],"tags":[],"category":"Travel & Places","description":"Japanese post office","unicode_version":"6.0"},{"emoji":"🏤","aliases":["european_post_office"],"tags":[],"category":"Travel & Places","description":"post office","unicode_version":"6.0"},{"emoji":"🏥","aliases":["hospital"],"tags":[],"category":"Travel & Places","description":"hospital","unicode_version":"6.0"},{"emoji":"🏦","aliases":["bank"],"tags":[],"category":"Travel & Places","description":"bank","unicode_version":"6.0"},{"emoji":"🏨","aliases":["hotel"],"tags":[],"category":"Travel & Places","description":"hotel","unicode_version":"6.0"},{"emoji":"🏩","aliases":["love_hotel"],"tags":[],"category":"Travel & Places","description":"love hotel","unicode_version":"6.0"},{"emoji":"🏪","aliases":["convenience_store"],"tags":[],"category":"Travel & Places","description":"convenience store","unicode_version":"6.0"},{"emoji":"🏫","aliases":["school"],"tags":[],"category":"Travel & Places","description":"school","unicode_version":"6.0"},{"emoji":"🏬","aliases":["department_store"],"tags":[],"category":"Travel & Places","description":"department store","unicode_version":"6.0"},{"emoji":"🏭","aliases":["factory"],"tags":[],"category":"Travel & Places","description":"factory","unicode_version":"6.0"},{"emoji":"🏯","aliases":["japanese_castle"],"tags":[],"category":"Travel & Places","description":"Japanese castle","unicode_version":"6.0"},{"emoji":"🏰","aliases":["european_castle"],"tags":[],"category":"Travel & Places","description":"castle","unicode_version":"6.0"},{"emoji":"💒","aliases":["wedding"],"tags":["marriage"],"category":"Travel & Places","description":"wedding","unicode_version":"6.0"},{"emoji":"🗼","aliases":["tokyo_tower"],"tags":[],"category":"Travel & Places","description":"Tokyo tower","unicode_version":"6.0"},{"emoji":"🗽","aliases":["statue_of_liberty"],"tags":[],"category":"Travel & Places","description":"Statue of Liberty","unicode_version":"6.0"},{"emoji":"⛪","aliases":["church"],"tags":[],"category":"Travel & Places","description":"church","unicode_version":"5.2"},{"emoji":"🕌","aliases":["mosque"],"tags":[],"category":"Travel & Places","description":"mosque","unicode_version":"8.0"},{"emoji":"🛕","aliases":["hindu_temple"],"tags":[],"category":"Travel & Places","description":"hindu temple","unicode_version":"12.0"},{"emoji":"🕍","aliases":["synagogue"],"tags":[],"category":"Travel & Places","description":"synagogue","unicode_version":"8.0"},{"emoji":"⛩️","aliases":["shinto_shrine"],"tags":[],"category":"Travel & Places","description":"shinto shrine","unicode_version":"5.2"},{"emoji":"🕋","aliases":["kaaba"],"tags":[],"category":"Travel & Places","description":"kaaba","unicode_version":"8.0"},{"emoji":"⛲","aliases":["fountain"],"tags":[],"category":"Travel & Places","description":"fountain","unicode_version":"5.2"},{"emoji":"⛺","aliases":["tent"],"tags":["camping"],"category":"Travel & Places","description":"tent","unicode_version":"5.2"},{"emoji":"🌁","aliases":["foggy"],"tags":["karl"],"category":"Travel & Places","description":"foggy","unicode_version":"6.0"},{"emoji":"🌃","aliases":["night_with_stars"],"tags":[],"category":"Travel & Places","description":"night with stars","unicode_version":"6.0"},{"emoji":"🏙️","aliases":["cityscape"],"tags":["skyline"],"category":"Travel & Places","description":"cityscape","unicode_version":"7.0"},{"emoji":"🌄","aliases":["sunrise_over_mountains"],"tags":[],"category":"Travel & Places","description":"sunrise over mountains","unicode_version":"6.0"},{"emoji":"🌅","aliases":["sunrise"],"tags":[],"category":"Travel & Places","description":"sunrise","unicode_version":"6.0"},{"emoji":"🌆","aliases":["city_sunset"],"tags":[],"category":"Travel & Places","description":"cityscape at dusk","unicode_version":"6.0"},{"emoji":"🌇","aliases":["city_sunrise"],"tags":[],"category":"Travel & Places","description":"sunset","unicode_version":"6.0"},{"emoji":"🌉","aliases":["bridge_at_night"],"tags":[],"category":"Travel & Places","description":"bridge at night","unicode_version":"6.0"},{"emoji":"♨️","aliases":["hotsprings"],"tags":[],"category":"Travel & Places","description":"hot springs","unicode_version":""},{"emoji":"🎠","aliases":["carousel_horse"],"tags":[],"category":"Travel & Places","description":"carousel horse","unicode_version":"6.0"},{"emoji":"🎡","aliases":["ferris_wheel"],"tags":[],"category":"Travel & Places","description":"ferris wheel","unicode_version":"6.0"},{"emoji":"🎢","aliases":["roller_coaster"],"tags":[],"category":"Travel & Places","description":"roller coaster","unicode_version":"6.0"},{"emoji":"💈","aliases":["barber"],"tags":[],"category":"Travel & Places","description":"barber pole","unicode_version":"6.0"},{"emoji":"🎪","aliases":["circus_tent"],"tags":[],"category":"Travel & Places","description":"circus tent","unicode_version":"6.0"},{"emoji":"🚂","aliases":["steam_locomotive"],"tags":["train"],"category":"Travel & Places","description":"locomotive","unicode_version":"6.0"},{"emoji":"🚃","aliases":["railway_car"],"tags":[],"category":"Travel & Places","description":"railway car","unicode_version":"6.0"},{"emoji":"🚄","aliases":["bullettrain_side"],"tags":["train"],"category":"Travel & Places","description":"high-speed train","unicode_version":"6.0"},{"emoji":"🚅","aliases":["bullettrain_front"],"tags":["train"],"category":"Travel & Places","description":"bullet train","unicode_version":"6.0"},{"emoji":"🚆","aliases":["train2"],"tags":[],"category":"Travel & Places","description":"train","unicode_version":"6.0"},{"emoji":"🚇","aliases":["metro"],"tags":[],"category":"Travel & Places","description":"metro","unicode_version":"6.0"},{"emoji":"🚈","aliases":["light_rail"],"tags":[],"category":"Travel & Places","description":"light rail","unicode_version":"6.0"},{"emoji":"🚉","aliases":["station"],"tags":[],"category":"Travel & Places","description":"station","unicode_version":"6.0"},{"emoji":"🚊","aliases":["tram"],"tags":[],"category":"Travel & Places","description":"tram","unicode_version":"6.0"},{"emoji":"🚝","aliases":["monorail"],"tags":[],"category":"Travel & Places","description":"monorail","unicode_version":"6.0"},{"emoji":"🚞","aliases":["mountain_railway"],"tags":[],"category":"Travel & Places","description":"mountain railway","unicode_version":"6.0"},{"emoji":"🚋","aliases":["train"],"tags":[],"category":"Travel & Places","description":"tram car","unicode_version":"6.0"},{"emoji":"🚌","aliases":["bus"],"tags":[],"category":"Travel & Places","description":"bus","unicode_version":"6.0"},{"emoji":"🚍","aliases":["oncoming_bus"],"tags":[],"category":"Travel & Places","description":"oncoming bus","unicode_version":"6.0"},{"emoji":"🚎","aliases":["trolleybus"],"tags":[],"category":"Travel & Places","description":"trolleybus","unicode_version":"6.0"},{"emoji":"🚐","aliases":["minibus"],"tags":[],"category":"Travel & Places","description":"minibus","unicode_version":"6.0"},{"emoji":"🚑","aliases":["ambulance"],"tags":[],"category":"Travel & Places","description":"ambulance","unicode_version":"6.0"},{"emoji":"🚒","aliases":["fire_engine"],"tags":[],"category":"Travel & Places","description":"fire engine","unicode_version":"6.0"},{"emoji":"🚓","aliases":["police_car"],"tags":[],"category":"Travel & Places","description":"police car","unicode_version":"6.0"},{"emoji":"🚔","aliases":["oncoming_police_car"],"tags":[],"category":"Travel & Places","description":"oncoming police car","unicode_version":"6.0"},{"emoji":"🚕","aliases":["taxi"],"tags":[],"category":"Travel & Places","description":"taxi","unicode_version":"6.0"},{"emoji":"🚖","aliases":["oncoming_taxi"],"tags":[],"category":"Travel & Places","description":"oncoming taxi","unicode_version":"6.0"},{"emoji":"🚗","aliases":["car","red_car"],"tags":[],"category":"Travel & Places","description":"automobile","unicode_version":"6.0"},{"emoji":"🚘","aliases":["oncoming_automobile"],"tags":[],"category":"Travel & Places","description":"oncoming automobile","unicode_version":"6.0"},{"emoji":"🚙","aliases":["blue_car"],"tags":[],"category":"Travel & Places","description":"sport utility vehicle","unicode_version":"6.0"},{"emoji":"🛻","aliases":["pickup_truck"],"tags":[],"category":"Travel & Places","description":"pickup truck","unicode_version":"13.0"},{"emoji":"🚚","aliases":["truck"],"tags":[],"category":"Travel & Places","description":"delivery truck","unicode_version":"6.0"},{"emoji":"🚛","aliases":["articulated_lorry"],"tags":[],"category":"Travel & Places","description":"articulated lorry","unicode_version":"6.0"},{"emoji":"🚜","aliases":["tractor"],"tags":[],"category":"Travel & Places","description":"tractor","unicode_version":"6.0"},{"emoji":"🏎️","aliases":["racing_car"],"tags":[],"category":"Travel & Places","description":"racing car","unicode_version":"7.0"},{"emoji":"🏍️","aliases":["motorcycle"],"tags":[],"category":"Travel & Places","description":"motorcycle","unicode_version":"7.0"},{"emoji":"🛵","aliases":["motor_scooter"],"tags":[],"category":"Travel & Places","description":"motor scooter","unicode_version":"9.0"},{"emoji":"🦽","aliases":["manual_wheelchair"],"tags":[],"category":"Travel & Places","description":"manual wheelchair","unicode_version":"12.0"},{"emoji":"🦼","aliases":["motorized_wheelchair"],"tags":[],"category":"Travel & Places","description":"motorized wheelchair","unicode_version":"12.0"},{"emoji":"🛺","aliases":["auto_rickshaw"],"tags":[],"category":"Travel & Places","description":"auto rickshaw","unicode_version":"12.0"},{"emoji":"🚲","aliases":["bike"],"tags":["bicycle"],"category":"Travel & Places","description":"bicycle","unicode_version":"6.0"},{"emoji":"🛴","aliases":["kick_scooter"],"tags":[],"category":"Travel & Places","description":"kick scooter","unicode_version":"9.0"},{"emoji":"🛹","aliases":["skateboard"],"tags":[],"category":"Travel & Places","description":"skateboard","unicode_version":"11.0"},{"emoji":"🛼","aliases":["roller_skate"],"tags":[],"category":"Travel & Places","description":"roller skate","unicode_version":"13.0"},{"emoji":"🚏","aliases":["busstop"],"tags":[],"category":"Travel & Places","description":"bus stop","unicode_version":"6.0"},{"emoji":"🛣️","aliases":["motorway"],"tags":[],"category":"Travel & Places","description":"motorway","unicode_version":"7.0"},{"emoji":"🛤️","aliases":["railway_track"],"tags":[],"category":"Travel & Places","description":"railway track","unicode_version":"7.0"},{"emoji":"🛢️","aliases":["oil_drum"],"tags":[],"category":"Travel & Places","description":"oil drum","unicode_version":"7.0"},{"emoji":"⛽","aliases":["fuelpump"],"tags":[],"category":"Travel & Places","description":"fuel pump","unicode_version":"5.2"},{"emoji":"🚨","aliases":["rotating_light"],"tags":["911","emergency"],"category":"Travel & Places","description":"police car light","unicode_version":"6.0"},{"emoji":"🚥","aliases":["traffic_light"],"tags":[],"category":"Travel & Places","description":"horizontal traffic light","unicode_version":"6.0"},{"emoji":"🚦","aliases":["vertical_traffic_light"],"tags":["semaphore"],"category":"Travel & Places","description":"vertical traffic light","unicode_version":"6.0"},{"emoji":"🛑","aliases":["stop_sign"],"tags":[],"category":"Travel & Places","description":"stop sign","unicode_version":"9.0"},{"emoji":"🚧","aliases":["construction"],"tags":["wip"],"category":"Travel & Places","description":"construction","unicode_version":"6.0"},{"emoji":"⚓","aliases":["anchor"],"tags":["ship"],"category":"Travel & Places","description":"anchor","unicode_version":"4.1"},{"emoji":"⛵","aliases":["boat","sailboat"],"tags":[],"category":"Travel & Places","description":"sailboat","unicode_version":"5.2"},{"emoji":"🛶","aliases":["canoe"],"tags":[],"category":"Travel & Places","description":"canoe","unicode_version":"9.0"},{"emoji":"🚤","aliases":["speedboat"],"tags":["ship"],"category":"Travel & Places","description":"speedboat","unicode_version":"6.0"},{"emoji":"🛳️","aliases":["passenger_ship"],"tags":["cruise"],"category":"Travel & Places","description":"passenger ship","unicode_version":"7.0"},{"emoji":"⛴️","aliases":["ferry"],"tags":[],"category":"Travel & Places","description":"ferry","unicode_version":"5.2"},{"emoji":"🛥️","aliases":["motor_boat"],"tags":[],"category":"Travel & Places","description":"motor boat","unicode_version":"7.0"},{"emoji":"🚢","aliases":["ship"],"tags":[],"category":"Travel & Places","description":"ship","unicode_version":"6.0"},{"emoji":"✈️","aliases":["airplane"],"tags":["flight"],"category":"Travel & Places","description":"airplane","unicode_version":""},{"emoji":"🛩️","aliases":["small_airplane"],"tags":["flight"],"category":"Travel & Places","description":"small airplane","unicode_version":"7.0"},{"emoji":"🛫","aliases":["flight_departure"],"tags":[],"category":"Travel & Places","description":"airplane departure","unicode_version":"7.0"},{"emoji":"🛬","aliases":["flight_arrival"],"tags":[],"category":"Travel & Places","description":"airplane arrival","unicode_version":"7.0"},{"emoji":"🪂","aliases":["parachute"],"tags":[],"category":"Travel & Places","description":"parachute","unicode_version":"12.0"},{"emoji":"💺","aliases":["seat"],"tags":[],"category":"Travel & Places","description":"seat","unicode_version":"6.0"},{"emoji":"🚁","aliases":["helicopter"],"tags":[],"category":"Travel & Places","description":"helicopter","unicode_version":"6.0"},{"emoji":"🚟","aliases":["suspension_railway"],"tags":[],"category":"Travel & Places","description":"suspension railway","unicode_version":"6.0"},{"emoji":"🚠","aliases":["mountain_cableway"],"tags":[],"category":"Travel & Places","description":"mountain cableway","unicode_version":"6.0"},{"emoji":"🚡","aliases":["aerial_tramway"],"tags":[],"category":"Travel & Places","description":"aerial tramway","unicode_version":"6.0"},{"emoji":"🛰️","aliases":["artificial_satellite"],"tags":["orbit","space"],"category":"Travel & Places","description":"satellite","unicode_version":"7.0"},{"emoji":"🚀","aliases":["rocket"],"tags":["ship","launch"],"category":"Travel & Places","description":"rocket","unicode_version":"6.0"},{"emoji":"🛸","aliases":["flying_saucer"],"tags":["ufo"],"category":"Travel & Places","description":"flying saucer","unicode_version":"11.0"},{"emoji":"🛎️","aliases":["bellhop_bell"],"tags":[],"category":"Travel & Places","description":"bellhop bell","unicode_version":"7.0"},{"emoji":"🧳","aliases":["luggage"],"tags":[],"category":"Travel & Places","description":"luggage","unicode_version":"11.0"},{"emoji":"⌛","aliases":["hourglass"],"tags":["time"],"category":"Travel & Places","description":"hourglass done","unicode_version":""},{"emoji":"⏳","aliases":["hourglass_flowing_sand"],"tags":["time"],"category":"Travel & Places","description":"hourglass not done","unicode_version":"6.0"},{"emoji":"⌚","aliases":["watch"],"tags":["time"],"category":"Travel & Places","description":"watch","unicode_version":""},{"emoji":"⏰","aliases":["alarm_clock"],"tags":["morning"],"category":"Travel & Places","description":"alarm clock","unicode_version":"6.0"},{"emoji":"⏱️","aliases":["stopwatch"],"tags":[],"category":"Travel & Places","description":"stopwatch","unicode_version":"6.0"},{"emoji":"⏲️","aliases":["timer_clock"],"tags":[],"category":"Travel & Places","description":"timer clock","unicode_version":"6.0"},{"emoji":"🕰️","aliases":["mantelpiece_clock"],"tags":[],"category":"Travel & Places","description":"mantelpiece clock","unicode_version":"7.0"},{"emoji":"🕛","aliases":["clock12"],"tags":[],"category":"Travel & Places","description":"twelve o’clock","unicode_version":"6.0"},{"emoji":"🕧","aliases":["clock1230"],"tags":[],"category":"Travel & Places","description":"twelve-thirty","unicode_version":"6.0"},{"emoji":"🕐","aliases":["clock1"],"tags":[],"category":"Travel & Places","description":"one o’clock","unicode_version":"6.0"},{"emoji":"🕜","aliases":["clock130"],"tags":[],"category":"Travel & Places","description":"one-thirty","unicode_version":"6.0"},{"emoji":"🕑","aliases":["clock2"],"tags":[],"category":"Travel & Places","description":"two o’clock","unicode_version":"6.0"},{"emoji":"🕝","aliases":["clock230"],"tags":[],"category":"Travel & Places","description":"two-thirty","unicode_version":"6.0"},{"emoji":"🕒","aliases":["clock3"],"tags":[],"category":"Travel & Places","description":"three o’clock","unicode_version":"6.0"},{"emoji":"🕞","aliases":["clock330"],"tags":[],"category":"Travel & Places","description":"three-thirty","unicode_version":"6.0"},{"emoji":"🕓","aliases":["clock4"],"tags":[],"category":"Travel & Places","description":"four o’clock","unicode_version":"6.0"},{"emoji":"🕟","aliases":["clock430"],"tags":[],"category":"Travel & Places","description":"four-thirty","unicode_version":"6.0"},{"emoji":"🕔","aliases":["clock5"],"tags":[],"category":"Travel & Places","description":"five o’clock","unicode_version":"6.0"},{"emoji":"🕠","aliases":["clock530"],"tags":[],"category":"Travel & Places","description":"five-thirty","unicode_version":"6.0"},{"emoji":"🕕","aliases":["clock6"],"tags":[],"category":"Travel & Places","description":"six o’clock","unicode_version":"6.0"},{"emoji":"🕡","aliases":["clock630"],"tags":[],"category":"Travel & Places","description":"six-thirty","unicode_version":"6.0"},{"emoji":"🕖","aliases":["clock7"],"tags":[],"category":"Travel & Places","description":"seven o’clock","unicode_version":"6.0"},{"emoji":"🕢","aliases":["clock730"],"tags":[],"category":"Travel & Places","description":"seven-thirty","unicode_version":"6.0"},{"emoji":"🕗","aliases":["clock8"],"tags":[],"category":"Travel & Places","description":"eight o’clock","unicode_version":"6.0"},{"emoji":"🕣","aliases":["clock830"],"tags":[],"category":"Travel & Places","description":"eight-thirty","unicode_version":"6.0"},{"emoji":"🕘","aliases":["clock9"],"tags":[],"category":"Travel & Places","description":"nine o’clock","unicode_version":"6.0"},{"emoji":"🕤","aliases":["clock930"],"tags":[],"category":"Travel & Places","description":"nine-thirty","unicode_version":"6.0"},{"emoji":"🕙","aliases":["clock10"],"tags":[],"category":"Travel & Places","description":"ten o’clock","unicode_version":"6.0"},{"emoji":"🕥","aliases":["clock1030"],"tags":[],"category":"Travel & Places","description":"ten-thirty","unicode_version":"6.0"},{"emoji":"🕚","aliases":["clock11"],"tags":[],"category":"Travel & Places","description":"eleven o’clock","unicode_version":"6.0"},{"emoji":"🕦","aliases":["clock1130"],"tags":[],"category":"Travel & Places","description":"eleven-thirty","unicode_version":"6.0"},{"emoji":"🌑","aliases":["new_moon"],"tags":[],"category":"Travel & Places","description":"new moon","unicode_version":"6.0"},{"emoji":"🌒","aliases":["waxing_crescent_moon"],"tags":[],"category":"Travel & Places","description":"waxing crescent moon","unicode_version":"6.0"},{"emoji":"🌓","aliases":["first_quarter_moon"],"tags":[],"category":"Travel & Places","description":"first quarter moon","unicode_version":"6.0"},{"emoji":"🌔","aliases":["moon","waxing_gibbous_moon"],"tags":[],"category":"Travel & Places","description":"waxing gibbous moon","unicode_version":"6.0"},{"emoji":"🌕","aliases":["full_moon"],"tags":[],"category":"Travel & Places","description":"full moon","unicode_version":"6.0"},{"emoji":"🌖","aliases":["waning_gibbous_moon"],"tags":[],"category":"Travel & Places","description":"waning gibbous moon","unicode_version":"6.0"},{"emoji":"🌗","aliases":["last_quarter_moon"],"tags":[],"category":"Travel & Places","description":"last quarter moon","unicode_version":"6.0"},{"emoji":"🌘","aliases":["waning_crescent_moon"],"tags":[],"category":"Travel & Places","description":"waning crescent moon","unicode_version":"6.0"},{"emoji":"🌙","aliases":["crescent_moon"],"tags":["night"],"category":"Travel & Places","description":"crescent moon","unicode_version":"6.0"},{"emoji":"🌚","aliases":["new_moon_with_face"],"tags":[],"category":"Travel & Places","description":"new moon face","unicode_version":"6.0"},{"emoji":"🌛","aliases":["first_quarter_moon_with_face"],"tags":[],"category":"Travel & Places","description":"first quarter moon face","unicode_version":"6.0"},{"emoji":"🌜","aliases":["last_quarter_moon_with_face"],"tags":[],"category":"Travel & Places","description":"last quarter moon face","unicode_version":"6.0"},{"emoji":"🌡️","aliases":["thermometer"],"tags":[],"category":"Travel & Places","description":"thermometer","unicode_version":"7.0"},{"emoji":"☀️","aliases":["sunny"],"tags":["weather"],"category":"Travel & Places","description":"sun","unicode_version":""},{"emoji":"🌝","aliases":["full_moon_with_face"],"tags":[],"category":"Travel & Places","description":"full moon face","unicode_version":"6.0"},{"emoji":"🌞","aliases":["sun_with_face"],"tags":["summer"],"category":"Travel & Places","description":"sun with face","unicode_version":"6.0"},{"emoji":"🪐","aliases":["ringed_planet"],"tags":[],"category":"Travel & Places","description":"ringed planet","unicode_version":"12.0"},{"emoji":"⭐","aliases":["star"],"tags":[],"category":"Travel & Places","description":"star","unicode_version":"5.1"},{"emoji":"🌟","aliases":["star2"],"tags":[],"category":"Travel & Places","description":"glowing star","unicode_version":"6.0"},{"emoji":"🌠","aliases":["stars"],"tags":[],"category":"Travel & Places","description":"shooting star","unicode_version":"6.0"},{"emoji":"🌌","aliases":["milky_way"],"tags":[],"category":"Travel & Places","description":"milky way","unicode_version":"6.0"},{"emoji":"☁️","aliases":["cloud"],"tags":[],"category":"Travel & Places","description":"cloud","unicode_version":""},{"emoji":"⛅","aliases":["partly_sunny"],"tags":["weather","cloud"],"category":"Travel & Places","description":"sun behind cloud","unicode_version":"5.2"},{"emoji":"⛈️","aliases":["cloud_with_lightning_and_rain"],"tags":[],"category":"Travel & Places","description":"cloud with lightning and rain","unicode_version":"5.2"},{"emoji":"🌤️","aliases":["sun_behind_small_cloud"],"tags":[],"category":"Travel & Places","description":"sun behind small cloud","unicode_version":"7.0"},{"emoji":"🌥️","aliases":["sun_behind_large_cloud"],"tags":[],"category":"Travel & Places","description":"sun behind large cloud","unicode_version":"7.0"},{"emoji":"🌦️","aliases":["sun_behind_rain_cloud"],"tags":[],"category":"Travel & Places","description":"sun behind rain cloud","unicode_version":"7.0"},{"emoji":"🌧️","aliases":["cloud_with_rain"],"tags":[],"category":"Travel & Places","description":"cloud with rain","unicode_version":"7.0"},{"emoji":"🌨️","aliases":["cloud_with_snow"],"tags":[],"category":"Travel & Places","description":"cloud with snow","unicode_version":"7.0"},{"emoji":"🌩️","aliases":["cloud_with_lightning"],"tags":[],"category":"Travel & Places","description":"cloud with lightning","unicode_version":"7.0"},{"emoji":"🌪️","aliases":["tornado"],"tags":[],"category":"Travel & Places","description":"tornado","unicode_version":"7.0"},{"emoji":"🌫️","aliases":["fog"],"tags":[],"category":"Travel & Places","description":"fog","unicode_version":"7.0"},{"emoji":"🌬️","aliases":["wind_face"],"tags":[],"category":"Travel & Places","description":"wind face","unicode_version":"7.0"},{"emoji":"🌀","aliases":["cyclone"],"tags":["swirl"],"category":"Travel & Places","description":"cyclone","unicode_version":"6.0"},{"emoji":"🌈","aliases":["rainbow"],"tags":[],"category":"Travel & Places","description":"rainbow","unicode_version":"6.0"},{"emoji":"🌂","aliases":["closed_umbrella"],"tags":["weather","rain"],"category":"Travel & Places","description":"closed umbrella","unicode_version":"6.0"},{"emoji":"☂️","aliases":["open_umbrella"],"tags":[],"category":"Travel & Places","description":"umbrella","unicode_version":""},{"emoji":"☔","aliases":["umbrella"],"tags":["rain","weather"],"category":"Travel & Places","description":"umbrella with rain drops","unicode_version":"4.0"},{"emoji":"⛱️","aliases":["parasol_on_ground"],"tags":["beach_umbrella"],"category":"Travel & Places","description":"umbrella on ground","unicode_version":"5.2"},{"emoji":"⚡","aliases":["zap"],"tags":["lightning","thunder"],"category":"Travel & Places","description":"high voltage","unicode_version":"4.0"},{"emoji":"❄️","aliases":["snowflake"],"tags":["winter","cold","weather"],"category":"Travel & Places","description":"snowflake","unicode_version":""},{"emoji":"☃️","aliases":["snowman_with_snow"],"tags":["winter","christmas"],"category":"Travel & Places","description":"snowman","unicode_version":""},{"emoji":"⛄","aliases":["snowman"],"tags":["winter"],"category":"Travel & Places","description":"snowman without snow","unicode_version":"5.2"},{"emoji":"☄️","aliases":["comet"],"tags":[],"category":"Travel & Places","description":"comet","unicode_version":""},{"emoji":"🔥","aliases":["fire"],"tags":["burn"],"category":"Travel & Places","description":"fire","unicode_version":"6.0"},{"emoji":"💧","aliases":["droplet"],"tags":["water"],"category":"Travel & Places","description":"droplet","unicode_version":"6.0"},{"emoji":"🌊","aliases":["ocean"],"tags":["sea"],"category":"Travel & Places","description":"water wave","unicode_version":"6.0"},{"emoji":"🎃","aliases":["jack_o_lantern"],"tags":["halloween"],"category":"Activities","description":"jack-o-lantern","unicode_version":"6.0"},{"emoji":"🎄","aliases":["christmas_tree"],"tags":[],"category":"Activities","description":"Christmas tree","unicode_version":"6.0"},{"emoji":"🎆","aliases":["fireworks"],"tags":["festival","celebration"],"category":"Activities","description":"fireworks","unicode_version":"6.0"},{"emoji":"🎇","aliases":["sparkler"],"tags":[],"category":"Activities","description":"sparkler","unicode_version":"6.0"},{"emoji":"🧨","aliases":["firecracker"],"tags":[],"category":"Activities","description":"firecracker","unicode_version":"11.0"},{"emoji":"✨","aliases":["sparkles"],"tags":["shiny"],"category":"Activities","description":"sparkles","unicode_version":"6.0"},{"emoji":"🎈","aliases":["balloon"],"tags":["party","birthday"],"category":"Activities","description":"balloon","unicode_version":"6.0"},{"emoji":"🎉","aliases":["tada"],"tags":["hooray","party"],"category":"Activities","description":"party popper","unicode_version":"6.0"},{"emoji":"🎊","aliases":["confetti_ball"],"tags":[],"category":"Activities","description":"confetti ball","unicode_version":"6.0"},{"emoji":"🎋","aliases":["tanabata_tree"],"tags":[],"category":"Activities","description":"tanabata tree","unicode_version":"6.0"},{"emoji":"🎍","aliases":["bamboo"],"tags":[],"category":"Activities","description":"pine decoration","unicode_version":"6.0"},{"emoji":"🎎","aliases":["dolls"],"tags":[],"category":"Activities","description":"Japanese dolls","unicode_version":"6.0"},{"emoji":"🎏","aliases":["flags"],"tags":[],"category":"Activities","description":"carp streamer","unicode_version":"6.0"},{"emoji":"🎐","aliases":["wind_chime"],"tags":[],"category":"Activities","description":"wind chime","unicode_version":"6.0"},{"emoji":"🎑","aliases":["rice_scene"],"tags":[],"category":"Activities","description":"moon viewing ceremony","unicode_version":"6.0"},{"emoji":"🧧","aliases":["red_envelope"],"tags":[],"category":"Activities","description":"red envelope","unicode_version":"11.0"},{"emoji":"🎀","aliases":["ribbon"],"tags":[],"category":"Activities","description":"ribbon","unicode_version":"6.0"},{"emoji":"🎁","aliases":["gift"],"tags":["present","birthday","christmas"],"category":"Activities","description":"wrapped gift","unicode_version":"6.0"},{"emoji":"🎗️","aliases":["reminder_ribbon"],"tags":[],"category":"Activities","description":"reminder ribbon","unicode_version":"7.0"},{"emoji":"🎟️","aliases":["tickets"],"tags":[],"category":"Activities","description":"admission tickets","unicode_version":"7.0"},{"emoji":"🎫","aliases":["ticket"],"tags":[],"category":"Activities","description":"ticket","unicode_version":"6.0"},{"emoji":"🎖️","aliases":["medal_military"],"tags":[],"category":"Activities","description":"military medal","unicode_version":"7.0"},{"emoji":"🏆","aliases":["trophy"],"tags":["award","contest","winner"],"category":"Activities","description":"trophy","unicode_version":"6.0"},{"emoji":"🏅","aliases":["medal_sports"],"tags":["gold","winner"],"category":"Activities","description":"sports medal","unicode_version":"7.0"},{"emoji":"🥇","aliases":["1st_place_medal"],"tags":["gold"],"category":"Activities","description":"1st place medal","unicode_version":"9.0"},{"emoji":"🥈","aliases":["2nd_place_medal"],"tags":["silver"],"category":"Activities","description":"2nd place medal","unicode_version":"9.0"},{"emoji":"🥉","aliases":["3rd_place_medal"],"tags":["bronze"],"category":"Activities","description":"3rd place medal","unicode_version":"9.0"},{"emoji":"⚽","aliases":["soccer"],"tags":["sports"],"category":"Activities","description":"soccer ball","unicode_version":"5.2"},{"emoji":"⚾","aliases":["baseball"],"tags":["sports"],"category":"Activities","description":"baseball","unicode_version":"5.2"},{"emoji":"🥎","aliases":["softball"],"tags":[],"category":"Activities","description":"softball","unicode_version":"11.0"},{"emoji":"🏀","aliases":["basketball"],"tags":["sports"],"category":"Activities","description":"basketball","unicode_version":"6.0"},{"emoji":"🏐","aliases":["volleyball"],"tags":[],"category":"Activities","description":"volleyball","unicode_version":"8.0"},{"emoji":"🏈","aliases":["football"],"tags":["sports"],"category":"Activities","description":"american football","unicode_version":"6.0"},{"emoji":"🏉","aliases":["rugby_football"],"tags":[],"category":"Activities","description":"rugby football","unicode_version":"6.0"},{"emoji":"🎾","aliases":["tennis"],"tags":["sports"],"category":"Activities","description":"tennis","unicode_version":"6.0"},{"emoji":"🥏","aliases":["flying_disc"],"tags":[],"category":"Activities","description":"flying disc","unicode_version":"11.0"},{"emoji":"🎳","aliases":["bowling"],"tags":[],"category":"Activities","description":"bowling","unicode_version":"6.0"},{"emoji":"🏏","aliases":["cricket_game"],"tags":[],"category":"Activities","description":"cricket game","unicode_version":"8.0"},{"emoji":"🏑","aliases":["field_hockey"],"tags":[],"category":"Activities","description":"field hockey","unicode_version":"8.0"},{"emoji":"🏒","aliases":["ice_hockey"],"tags":[],"category":"Activities","description":"ice hockey","unicode_version":"8.0"},{"emoji":"🥍","aliases":["lacrosse"],"tags":[],"category":"Activities","description":"lacrosse","unicode_version":"11.0"},{"emoji":"🏓","aliases":["ping_pong"],"tags":[],"category":"Activities","description":"ping pong","unicode_version":"8.0"},{"emoji":"🏸","aliases":["badminton"],"tags":[],"category":"Activities","description":"badminton","unicode_version":"8.0"},{"emoji":"🥊","aliases":["boxing_glove"],"tags":[],"category":"Activities","description":"boxing glove","unicode_version":"9.0"},{"emoji":"🥋","aliases":["martial_arts_uniform"],"tags":[],"category":"Activities","description":"martial arts uniform","unicode_version":"9.0"},{"emoji":"🥅","aliases":["goal_net"],"tags":[],"category":"Activities","description":"goal net","unicode_version":"9.0"},{"emoji":"⛳","aliases":["golf"],"tags":[],"category":"Activities","description":"flag in hole","unicode_version":"5.2"},{"emoji":"⛸️","aliases":["ice_skate"],"tags":["skating"],"category":"Activities","description":"ice skate","unicode_version":"5.2"},{"emoji":"🎣","aliases":["fishing_pole_and_fish"],"tags":[],"category":"Activities","description":"fishing pole","unicode_version":"6.0"},{"emoji":"🤿","aliases":["diving_mask"],"tags":[],"category":"Activities","description":"diving mask","unicode_version":"12.0"},{"emoji":"🎽","aliases":["running_shirt_with_sash"],"tags":["marathon"],"category":"Activities","description":"running shirt","unicode_version":"6.0"},{"emoji":"🎿","aliases":["ski"],"tags":[],"category":"Activities","description":"skis","unicode_version":"6.0"},{"emoji":"🛷","aliases":["sled"],"tags":[],"category":"Activities","description":"sled","unicode_version":"11.0"},{"emoji":"🥌","aliases":["curling_stone"],"tags":[],"category":"Activities","description":"curling stone","unicode_version":"11.0"},{"emoji":"🎯","aliases":["dart"],"tags":["target"],"category":"Activities","description":"bullseye","unicode_version":"6.0"},{"emoji":"🪀","aliases":["yo_yo"],"tags":[],"category":"Activities","description":"yo-yo","unicode_version":"12.0"},{"emoji":"🪁","aliases":["kite"],"tags":[],"category":"Activities","description":"kite","unicode_version":"12.0"},{"emoji":"🎱","aliases":["8ball"],"tags":["pool","billiards"],"category":"Activities","description":"pool 8 ball","unicode_version":"6.0"},{"emoji":"🔮","aliases":["crystal_ball"],"tags":["fortune"],"category":"Activities","description":"crystal ball","unicode_version":"6.0"},{"emoji":"🪄","aliases":["magic_wand"],"tags":[],"category":"Activities","description":"magic wand","unicode_version":"13.0"},{"emoji":"🧿","aliases":["nazar_amulet"],"tags":[],"category":"Activities","description":"nazar amulet","unicode_version":"11.0"},{"emoji":"🎮","aliases":["video_game"],"tags":["play","controller","console"],"category":"Activities","description":"video game","unicode_version":"6.0"},{"emoji":"🕹️","aliases":["joystick"],"tags":[],"category":"Activities","description":"joystick","unicode_version":"7.0"},{"emoji":"🎰","aliases":["slot_machine"],"tags":[],"category":"Activities","description":"slot machine","unicode_version":"6.0"},{"emoji":"🎲","aliases":["game_die"],"tags":["dice","gambling"],"category":"Activities","description":"game die","unicode_version":"6.0"},{"emoji":"🧩","aliases":["jigsaw"],"tags":[],"category":"Activities","description":"puzzle piece","unicode_version":"11.0"},{"emoji":"🧸","aliases":["teddy_bear"],"tags":[],"category":"Activities","description":"teddy bear","unicode_version":"11.0"},{"emoji":"🪅","aliases":["pinata"],"tags":[],"category":"Activities","description":"piñata","unicode_version":"13.0"},{"emoji":"🪆","aliases":["nesting_dolls"],"tags":[],"category":"Activities","description":"nesting dolls","unicode_version":"13.0"},{"emoji":"♠️","aliases":["spades"],"tags":[],"category":"Activities","description":"spade suit","unicode_version":""},{"emoji":"♥️","aliases":["hearts"],"tags":[],"category":"Activities","description":"heart suit","unicode_version":""},{"emoji":"♦️","aliases":["diamonds"],"tags":[],"category":"Activities","description":"diamond suit","unicode_version":""},{"emoji":"♣️","aliases":["clubs"],"tags":[],"category":"Activities","description":"club suit","unicode_version":""},{"emoji":"♟️","aliases":["chess_pawn"],"tags":[],"category":"Activities","description":"chess pawn","unicode_version":"11.0"},{"emoji":"🃏","aliases":["black_joker"],"tags":[],"category":"Activities","description":"joker","unicode_version":"6.0"},{"emoji":"🀄","aliases":["mahjong"],"tags":[],"category":"Activities","description":"mahjong red dragon","unicode_version":""},{"emoji":"🎴","aliases":["flower_playing_cards"],"tags":[],"category":"Activities","description":"flower playing cards","unicode_version":"6.0"},{"emoji":"🎭","aliases":["performing_arts"],"tags":["theater","drama"],"category":"Activities","description":"performing arts","unicode_version":"6.0"},{"emoji":"🖼️","aliases":["framed_picture"],"tags":[],"category":"Activities","description":"framed picture","unicode_version":"7.0"},{"emoji":"🎨","aliases":["art"],"tags":["design","paint"],"category":"Activities","description":"artist palette","unicode_version":"6.0"},{"emoji":"🧵","aliases":["thread"],"tags":[],"category":"Activities","description":"thread","unicode_version":"11.0"},{"emoji":"🪡","aliases":["sewing_needle"],"tags":[],"category":"Activities","description":"sewing needle","unicode_version":"13.0"},{"emoji":"🧶","aliases":["yarn"],"tags":[],"category":"Activities","description":"yarn","unicode_version":"11.0"},{"emoji":"🪢","aliases":["knot"],"tags":[],"category":"Activities","description":"knot","unicode_version":"13.0"},{"emoji":"👓","aliases":["eyeglasses"],"tags":["glasses"],"category":"Objects","description":"glasses","unicode_version":"6.0"},{"emoji":"🕶️","aliases":["dark_sunglasses"],"tags":[],"category":"Objects","description":"sunglasses","unicode_version":"7.0"},{"emoji":"🥽","aliases":["goggles"],"tags":[],"category":"Objects","description":"goggles","unicode_version":"11.0"},{"emoji":"🥼","aliases":["lab_coat"],"tags":[],"category":"Objects","description":"lab coat","unicode_version":"11.0"},{"emoji":"🦺","aliases":["safety_vest"],"tags":[],"category":"Objects","description":"safety vest","unicode_version":"12.0"},{"emoji":"👔","aliases":["necktie"],"tags":["shirt","formal"],"category":"Objects","description":"necktie","unicode_version":"6.0"},{"emoji":"👕","aliases":["shirt","tshirt"],"tags":[],"category":"Objects","description":"t-shirt","unicode_version":"6.0"},{"emoji":"👖","aliases":["jeans"],"tags":["pants"],"category":"Objects","description":"jeans","unicode_version":"6.0"},{"emoji":"🧣","aliases":["scarf"],"tags":[],"category":"Objects","description":"scarf","unicode_version":"11.0"},{"emoji":"🧤","aliases":["gloves"],"tags":[],"category":"Objects","description":"gloves","unicode_version":"11.0"},{"emoji":"🧥","aliases":["coat"],"tags":[],"category":"Objects","description":"coat","unicode_version":"11.0"},{"emoji":"🧦","aliases":["socks"],"tags":[],"category":"Objects","description":"socks","unicode_version":"11.0"},{"emoji":"👗","aliases":["dress"],"tags":[],"category":"Objects","description":"dress","unicode_version":"6.0"},{"emoji":"👘","aliases":["kimono"],"tags":[],"category":"Objects","description":"kimono","unicode_version":"6.0"},{"emoji":"🥻","aliases":["sari"],"tags":[],"category":"Objects","description":"sari","unicode_version":"12.0"},{"emoji":"🩱","aliases":["one_piece_swimsuit"],"tags":[],"category":"Objects","description":"one-piece swimsuit","unicode_version":"12.0"},{"emoji":"🩲","aliases":["swim_brief"],"tags":[],"category":"Objects","description":"briefs","unicode_version":"12.0"},{"emoji":"🩳","aliases":["shorts"],"tags":[],"category":"Objects","description":"shorts","unicode_version":"12.0"},{"emoji":"👙","aliases":["bikini"],"tags":["beach"],"category":"Objects","description":"bikini","unicode_version":"6.0"},{"emoji":"👚","aliases":["womans_clothes"],"tags":[],"category":"Objects","description":"woman’s clothes","unicode_version":"6.0"},{"emoji":"👛","aliases":["purse"],"tags":[],"category":"Objects","description":"purse","unicode_version":"6.0"},{"emoji":"👜","aliases":["handbag"],"tags":["bag"],"category":"Objects","description":"handbag","unicode_version":"6.0"},{"emoji":"👝","aliases":["pouch"],"tags":["bag"],"category":"Objects","description":"clutch bag","unicode_version":"6.0"},{"emoji":"🛍️","aliases":["shopping"],"tags":["bags"],"category":"Objects","description":"shopping bags","unicode_version":"7.0"},{"emoji":"🎒","aliases":["school_satchel"],"tags":[],"category":"Objects","description":"backpack","unicode_version":"6.0"},{"emoji":"🩴","aliases":["thong_sandal"],"tags":[],"category":"Objects","description":"thong sandal","unicode_version":"13.0"},{"emoji":"👞","aliases":["mans_shoe","shoe"],"tags":[],"category":"Objects","description":"man’s shoe","unicode_version":"6.0"},{"emoji":"👟","aliases":["athletic_shoe"],"tags":["sneaker","sport","running"],"category":"Objects","description":"running shoe","unicode_version":"6.0"},{"emoji":"🥾","aliases":["hiking_boot"],"tags":[],"category":"Objects","description":"hiking boot","unicode_version":"11.0"},{"emoji":"🥿","aliases":["flat_shoe"],"tags":[],"category":"Objects","description":"flat shoe","unicode_version":"11.0"},{"emoji":"👠","aliases":["high_heel"],"tags":["shoe"],"category":"Objects","description":"high-heeled shoe","unicode_version":"6.0"},{"emoji":"👡","aliases":["sandal"],"tags":["shoe"],"category":"Objects","description":"woman’s sandal","unicode_version":"6.0"},{"emoji":"🩰","aliases":["ballet_shoes"],"tags":[],"category":"Objects","description":"ballet shoes","unicode_version":"12.0"},{"emoji":"👢","aliases":["boot"],"tags":[],"category":"Objects","description":"woman’s boot","unicode_version":"6.0"},{"emoji":"👑","aliases":["crown"],"tags":["king","queen","royal"],"category":"Objects","description":"crown","unicode_version":"6.0"},{"emoji":"👒","aliases":["womans_hat"],"tags":[],"category":"Objects","description":"woman’s hat","unicode_version":"6.0"},{"emoji":"🎩","aliases":["tophat"],"tags":["hat","classy"],"category":"Objects","description":"top hat","unicode_version":"6.0"},{"emoji":"🎓","aliases":["mortar_board"],"tags":["education","college","university","graduation"],"category":"Objects","description":"graduation cap","unicode_version":"6.0"},{"emoji":"🧢","aliases":["billed_cap"],"tags":[],"category":"Objects","description":"billed cap","unicode_version":"11.0"},{"emoji":"🪖","aliases":["military_helmet"],"tags":[],"category":"Objects","description":"military helmet","unicode_version":"13.0"},{"emoji":"⛑️","aliases":["rescue_worker_helmet"],"tags":[],"category":"Objects","description":"rescue worker’s helmet","unicode_version":"5.2"},{"emoji":"📿","aliases":["prayer_beads"],"tags":[],"category":"Objects","description":"prayer beads","unicode_version":"8.0"},{"emoji":"💄","aliases":["lipstick"],"tags":["makeup"],"category":"Objects","description":"lipstick","unicode_version":"6.0"},{"emoji":"💍","aliases":["ring"],"tags":["wedding","marriage","engaged"],"category":"Objects","description":"ring","unicode_version":"6.0"},{"emoji":"💎","aliases":["gem"],"tags":["diamond"],"category":"Objects","description":"gem stone","unicode_version":"6.0"},{"emoji":"🔇","aliases":["mute"],"tags":["sound","volume"],"category":"Objects","description":"muted speaker","unicode_version":"6.0"},{"emoji":"🔈","aliases":["speaker"],"tags":[],"category":"Objects","description":"speaker low volume","unicode_version":"6.0"},{"emoji":"🔉","aliases":["sound"],"tags":["volume"],"category":"Objects","description":"speaker medium volume","unicode_version":"6.0"},{"emoji":"🔊","aliases":["loud_sound"],"tags":["volume"],"category":"Objects","description":"speaker high volume","unicode_version":"6.0"},{"emoji":"📢","aliases":["loudspeaker"],"tags":["announcement"],"category":"Objects","description":"loudspeaker","unicode_version":"6.0"},{"emoji":"📣","aliases":["mega"],"tags":[],"category":"Objects","description":"megaphone","unicode_version":"6.0"},{"emoji":"📯","aliases":["postal_horn"],"tags":[],"category":"Objects","description":"postal horn","unicode_version":"6.0"},{"emoji":"🔔","aliases":["bell"],"tags":["sound","notification"],"category":"Objects","description":"bell","unicode_version":"6.0"},{"emoji":"🔕","aliases":["no_bell"],"tags":["volume","off"],"category":"Objects","description":"bell with slash","unicode_version":"6.0"},{"emoji":"🎼","aliases":["musical_score"],"tags":[],"category":"Objects","description":"musical score","unicode_version":"6.0"},{"emoji":"🎵","aliases":["musical_note"],"tags":[],"category":"Objects","description":"musical note","unicode_version":"6.0"},{"emoji":"🎶","aliases":["notes"],"tags":["music"],"category":"Objects","description":"musical notes","unicode_version":"6.0"},{"emoji":"🎙️","aliases":["studio_microphone"],"tags":["podcast"],"category":"Objects","description":"studio microphone","unicode_version":"7.0"},{"emoji":"🎚️","aliases":["level_slider"],"tags":[],"category":"Objects","description":"level slider","unicode_version":"7.0"},{"emoji":"🎛️","aliases":["control_knobs"],"tags":[],"category":"Objects","description":"control knobs","unicode_version":"7.0"},{"emoji":"🎤","aliases":["microphone"],"tags":["sing"],"category":"Objects","description":"microphone","unicode_version":"6.0"},{"emoji":"🎧","aliases":["headphones"],"tags":["music","earphones"],"category":"Objects","description":"headphone","unicode_version":"6.0"},{"emoji":"📻","aliases":["radio"],"tags":["podcast"],"category":"Objects","description":"radio","unicode_version":"6.0"},{"emoji":"🎷","aliases":["saxophone"],"tags":[],"category":"Objects","description":"saxophone","unicode_version":"6.0"},{"emoji":"🪗","aliases":["accordion"],"tags":[],"category":"Objects","description":"accordion","unicode_version":"13.0"},{"emoji":"🎸","aliases":["guitar"],"tags":["rock"],"category":"Objects","description":"guitar","unicode_version":"6.0"},{"emoji":"🎹","aliases":["musical_keyboard"],"tags":["piano"],"category":"Objects","description":"musical keyboard","unicode_version":"6.0"},{"emoji":"🎺","aliases":["trumpet"],"tags":[],"category":"Objects","description":"trumpet","unicode_version":"6.0"},{"emoji":"🎻","aliases":["violin"],"tags":[],"category":"Objects","description":"violin","unicode_version":"6.0"},{"emoji":"🪕","aliases":["banjo"],"tags":[],"category":"Objects","description":"banjo","unicode_version":"12.0"},{"emoji":"🥁","aliases":["drum"],"tags":[],"category":"Objects","description":"drum","unicode_version":""},{"emoji":"🪘","aliases":["long_drum"],"tags":[],"category":"Objects","description":"long drum","unicode_version":"13.0"},{"emoji":"📱","aliases":["iphone"],"tags":["smartphone","mobile"],"category":"Objects","description":"mobile phone","unicode_version":"6.0"},{"emoji":"📲","aliases":["calling"],"tags":["call","incoming"],"category":"Objects","description":"mobile phone with arrow","unicode_version":"6.0"},{"emoji":"☎️","aliases":["phone","telephone"],"tags":[],"category":"Objects","description":"telephone","unicode_version":""},{"emoji":"📞","aliases":["telephone_receiver"],"tags":["phone","call"],"category":"Objects","description":"telephone receiver","unicode_version":"6.0"},{"emoji":"📟","aliases":["pager"],"tags":[],"category":"Objects","description":"pager","unicode_version":"6.0"},{"emoji":"📠","aliases":["fax"],"tags":[],"category":"Objects","description":"fax machine","unicode_version":"6.0"},{"emoji":"🔋","aliases":["battery"],"tags":["power"],"category":"Objects","description":"battery","unicode_version":"6.0"},{"emoji":"🔌","aliases":["electric_plug"],"tags":[],"category":"Objects","description":"electric plug","unicode_version":"6.0"},{"emoji":"💻","aliases":["computer"],"tags":["desktop","screen"],"category":"Objects","description":"laptop","unicode_version":"6.0"},{"emoji":"🖥️","aliases":["desktop_computer"],"tags":[],"category":"Objects","description":"desktop computer","unicode_version":"7.0"},{"emoji":"🖨️","aliases":["printer"],"tags":[],"category":"Objects","description":"printer","unicode_version":"7.0"},{"emoji":"⌨️","aliases":["keyboard"],"tags":[],"category":"Objects","description":"keyboard","unicode_version":""},{"emoji":"🖱️","aliases":["computer_mouse"],"tags":[],"category":"Objects","description":"computer mouse","unicode_version":"7.0"},{"emoji":"🖲️","aliases":["trackball"],"tags":[],"category":"Objects","description":"trackball","unicode_version":"7.0"},{"emoji":"💽","aliases":["minidisc"],"tags":[],"category":"Objects","description":"computer disk","unicode_version":"6.0"},{"emoji":"💾","aliases":["floppy_disk"],"tags":["save"],"category":"Objects","description":"floppy disk","unicode_version":"6.0"},{"emoji":"💿","aliases":["cd"],"tags":[],"category":"Objects","description":"optical disk","unicode_version":"6.0"},{"emoji":"📀","aliases":["dvd"],"tags":[],"category":"Objects","description":"dvd","unicode_version":"6.0"},{"emoji":"🧮","aliases":["abacus"],"tags":[],"category":"Objects","description":"abacus","unicode_version":"11.0"},{"emoji":"🎥","aliases":["movie_camera"],"tags":["film","video"],"category":"Objects","description":"movie camera","unicode_version":"6.0"},{"emoji":"🎞️","aliases":["film_strip"],"tags":[],"category":"Objects","description":"film frames","unicode_version":"7.0"},{"emoji":"📽️","aliases":["film_projector"],"tags":[],"category":"Objects","description":"film projector","unicode_version":"7.0"},{"emoji":"🎬","aliases":["clapper"],"tags":["film"],"category":"Objects","description":"clapper board","unicode_version":"6.0"},{"emoji":"📺","aliases":["tv"],"tags":[],"category":"Objects","description":"television","unicode_version":"6.0"},{"emoji":"📷","aliases":["camera"],"tags":["photo"],"category":"Objects","description":"camera","unicode_version":"6.0"},{"emoji":"📸","aliases":["camera_flash"],"tags":["photo"],"category":"Objects","description":"camera with flash","unicode_version":"7.0"},{"emoji":"📹","aliases":["video_camera"],"tags":[],"category":"Objects","description":"video camera","unicode_version":"6.0"},{"emoji":"📼","aliases":["vhs"],"tags":[],"category":"Objects","description":"videocassette","unicode_version":"6.0"},{"emoji":"🔍","aliases":["mag"],"tags":["search","zoom"],"category":"Objects","description":"magnifying glass tilted left","unicode_version":"6.0"},{"emoji":"🔎","aliases":["mag_right"],"tags":[],"category":"Objects","description":"magnifying glass tilted right","unicode_version":"6.0"},{"emoji":"🕯️","aliases":["candle"],"tags":[],"category":"Objects","description":"candle","unicode_version":"7.0"},{"emoji":"💡","aliases":["bulb"],"tags":["idea","light"],"category":"Objects","description":"light bulb","unicode_version":"6.0"},{"emoji":"🔦","aliases":["flashlight"],"tags":[],"category":"Objects","description":"flashlight","unicode_version":"6.0"},{"emoji":"🏮","aliases":["izakaya_lantern","lantern"],"tags":[],"category":"Objects","description":"red paper lantern","unicode_version":"6.0"},{"emoji":"🪔","aliases":["diya_lamp"],"tags":[],"category":"Objects","description":"diya lamp","unicode_version":"12.0"},{"emoji":"📔","aliases":["notebook_with_decorative_cover"],"tags":[],"category":"Objects","description":"notebook with decorative cover","unicode_version":"6.0"},{"emoji":"📕","aliases":["closed_book"],"tags":[],"category":"Objects","description":"closed book","unicode_version":"6.0"},{"emoji":"📖","aliases":["book","open_book"],"tags":[],"category":"Objects","description":"open book","unicode_version":"6.0"},{"emoji":"📗","aliases":["green_book"],"tags":[],"category":"Objects","description":"green book","unicode_version":"6.0"},{"emoji":"📘","aliases":["blue_book"],"tags":[],"category":"Objects","description":"blue book","unicode_version":"6.0"},{"emoji":"📙","aliases":["orange_book"],"tags":[],"category":"Objects","description":"orange book","unicode_version":"6.0"},{"emoji":"📚","aliases":["books"],"tags":["library"],"category":"Objects","description":"books","unicode_version":"6.0"},{"emoji":"📓","aliases":["notebook"],"tags":[],"category":"Objects","description":"notebook","unicode_version":"6.0"},{"emoji":"📒","aliases":["ledger"],"tags":[],"category":"Objects","description":"ledger","unicode_version":"6.0"},{"emoji":"📃","aliases":["page_with_curl"],"tags":[],"category":"Objects","description":"page with curl","unicode_version":"6.0"},{"emoji":"📜","aliases":["scroll"],"tags":["document"],"category":"Objects","description":"scroll","unicode_version":"6.0"},{"emoji":"📄","aliases":["page_facing_up"],"tags":["document"],"category":"Objects","description":"page facing up","unicode_version":"6.0"},{"emoji":"📰","aliases":["newspaper"],"tags":["press"],"category":"Objects","description":"newspaper","unicode_version":"6.0"},{"emoji":"🗞️","aliases":["newspaper_roll"],"tags":["press"],"category":"Objects","description":"rolled-up newspaper","unicode_version":"7.0"},{"emoji":"📑","aliases":["bookmark_tabs"],"tags":[],"category":"Objects","description":"bookmark tabs","unicode_version":"6.0"},{"emoji":"🔖","aliases":["bookmark"],"tags":[],"category":"Objects","description":"bookmark","unicode_version":"6.0"},{"emoji":"🏷️","aliases":["label"],"tags":["tag"],"category":"Objects","description":"label","unicode_version":"7.0"},{"emoji":"💰","aliases":["moneybag"],"tags":["dollar","cream"],"category":"Objects","description":"money bag","unicode_version":"6.0"},{"emoji":"🪙","aliases":["coin"],"tags":[],"category":"Objects","description":"coin","unicode_version":"13.0"},{"emoji":"💴","aliases":["yen"],"tags":[],"category":"Objects","description":"yen banknote","unicode_version":"6.0"},{"emoji":"💵","aliases":["dollar"],"tags":["money"],"category":"Objects","description":"dollar banknote","unicode_version":"6.0"},{"emoji":"💶","aliases":["euro"],"tags":[],"category":"Objects","description":"euro banknote","unicode_version":"6.0"},{"emoji":"💷","aliases":["pound"],"tags":[],"category":"Objects","description":"pound banknote","unicode_version":"6.0"},{"emoji":"💸","aliases":["money_with_wings"],"tags":["dollar"],"category":"Objects","description":"money with wings","unicode_version":"6.0"},{"emoji":"💳","aliases":["credit_card"],"tags":["subscription"],"category":"Objects","description":"credit card","unicode_version":"6.0"},{"emoji":"🧾","aliases":["receipt"],"tags":[],"category":"Objects","description":"receipt","unicode_version":"11.0"},{"emoji":"💹","aliases":["chart"],"tags":[],"category":"Objects","description":"chart increasing with yen","unicode_version":"6.0"},{"emoji":"✉️","aliases":["envelope"],"tags":["letter","email"],"category":"Objects","description":"envelope","unicode_version":""},{"emoji":"📧","aliases":["email","e-mail"],"tags":[],"category":"Objects","description":"e-mail","unicode_version":"6.0"},{"emoji":"📨","aliases":["incoming_envelope"],"tags":[],"category":"Objects","description":"incoming envelope","unicode_version":"6.0"},{"emoji":"📩","aliases":["envelope_with_arrow"],"tags":[],"category":"Objects","description":"envelope with arrow","unicode_version":"6.0"},{"emoji":"📤","aliases":["outbox_tray"],"tags":[],"category":"Objects","description":"outbox tray","unicode_version":"6.0"},{"emoji":"📥","aliases":["inbox_tray"],"tags":[],"category":"Objects","description":"inbox tray","unicode_version":"6.0"},{"emoji":"📦","aliases":["package"],"tags":["shipping"],"category":"Objects","description":"package","unicode_version":"6.0"},{"emoji":"📫","aliases":["mailbox"],"tags":[],"category":"Objects","description":"closed mailbox with raised flag","unicode_version":"6.0"},{"emoji":"📪","aliases":["mailbox_closed"],"tags":[],"category":"Objects","description":"closed mailbox with lowered flag","unicode_version":"6.0"},{"emoji":"📬","aliases":["mailbox_with_mail"],"tags":[],"category":"Objects","description":"open mailbox with raised flag","unicode_version":"6.0"},{"emoji":"📭","aliases":["mailbox_with_no_mail"],"tags":[],"category":"Objects","description":"open mailbox with lowered flag","unicode_version":"6.0"},{"emoji":"📮","aliases":["postbox"],"tags":[],"category":"Objects","description":"postbox","unicode_version":"6.0"},{"emoji":"🗳️","aliases":["ballot_box"],"tags":[],"category":"Objects","description":"ballot box with ballot","unicode_version":"7.0"},{"emoji":"✏️","aliases":["pencil2"],"tags":[],"category":"Objects","description":"pencil","unicode_version":""},{"emoji":"✒️","aliases":["black_nib"],"tags":[],"category":"Objects","description":"black nib","unicode_version":""},{"emoji":"🖋️","aliases":["fountain_pen"],"tags":[],"category":"Objects","description":"fountain pen","unicode_version":"7.0"},{"emoji":"🖊️","aliases":["pen"],"tags":[],"category":"Objects","description":"pen","unicode_version":"7.0"},{"emoji":"🖌️","aliases":["paintbrush"],"tags":[],"category":"Objects","description":"paintbrush","unicode_version":"7.0"},{"emoji":"🖍️","aliases":["crayon"],"tags":[],"category":"Objects","description":"crayon","unicode_version":"7.0"},{"emoji":"📝","aliases":["memo","pencil"],"tags":["document","note"],"category":"Objects","description":"memo","unicode_version":"6.0"},{"emoji":"💼","aliases":["briefcase"],"tags":["business"],"category":"Objects","description":"briefcase","unicode_version":"6.0"},{"emoji":"📁","aliases":["file_folder"],"tags":["directory"],"category":"Objects","description":"file folder","unicode_version":"6.0"},{"emoji":"📂","aliases":["open_file_folder"],"tags":[],"category":"Objects","description":"open file folder","unicode_version":"6.0"},{"emoji":"🗂️","aliases":["card_index_dividers"],"tags":[],"category":"Objects","description":"card index dividers","unicode_version":"7.0"},{"emoji":"📅","aliases":["date"],"tags":["calendar","schedule"],"category":"Objects","description":"calendar","unicode_version":"6.0"},{"emoji":"📆","aliases":["calendar"],"tags":["schedule"],"category":"Objects","description":"tear-off calendar","unicode_version":"6.0"},{"emoji":"🗒️","aliases":["spiral_notepad"],"tags":[],"category":"Objects","description":"spiral notepad","unicode_version":"7.0"},{"emoji":"🗓️","aliases":["spiral_calendar"],"tags":[],"category":"Objects","description":"spiral calendar","unicode_version":"7.0"},{"emoji":"📇","aliases":["card_index"],"tags":[],"category":"Objects","description":"card index","unicode_version":"6.0"},{"emoji":"📈","aliases":["chart_with_upwards_trend"],"tags":["graph","metrics"],"category":"Objects","description":"chart increasing","unicode_version":"6.0"},{"emoji":"📉","aliases":["chart_with_downwards_trend"],"tags":["graph","metrics"],"category":"Objects","description":"chart decreasing","unicode_version":"6.0"},{"emoji":"📊","aliases":["bar_chart"],"tags":["stats","metrics"],"category":"Objects","description":"bar chart","unicode_version":"6.0"},{"emoji":"📋","aliases":["clipboard"],"tags":[],"category":"Objects","description":"clipboard","unicode_version":"6.0"},{"emoji":"📌","aliases":["pushpin"],"tags":["location"],"category":"Objects","description":"pushpin","unicode_version":"6.0"},{"emoji":"📍","aliases":["round_pushpin"],"tags":["location"],"category":"Objects","description":"round pushpin","unicode_version":"6.0"},{"emoji":"📎","aliases":["paperclip"],"tags":[],"category":"Objects","description":"paperclip","unicode_version":"6.0"},{"emoji":"🖇️","aliases":["paperclips"],"tags":[],"category":"Objects","description":"linked paperclips","unicode_version":"7.0"},{"emoji":"📏","aliases":["straight_ruler"],"tags":[],"category":"Objects","description":"straight ruler","unicode_version":"6.0"},{"emoji":"📐","aliases":["triangular_ruler"],"tags":[],"category":"Objects","description":"triangular ruler","unicode_version":"6.0"},{"emoji":"✂️","aliases":["scissors"],"tags":["cut"],"category":"Objects","description":"scissors","unicode_version":""},{"emoji":"🗃️","aliases":["card_file_box"],"tags":[],"category":"Objects","description":"card file box","unicode_version":"7.0"},{"emoji":"🗄️","aliases":["file_cabinet"],"tags":[],"category":"Objects","description":"file cabinet","unicode_version":"7.0"},{"emoji":"🗑️","aliases":["wastebasket"],"tags":["trash"],"category":"Objects","description":"wastebasket","unicode_version":"7.0"},{"emoji":"🔒","aliases":["lock"],"tags":["security","private"],"category":"Objects","description":"locked","unicode_version":"6.0"},{"emoji":"🔓","aliases":["unlock"],"tags":["security"],"category":"Objects","description":"unlocked","unicode_version":"6.0"},{"emoji":"🔏","aliases":["lock_with_ink_pen"],"tags":[],"category":"Objects","description":"locked with pen","unicode_version":"6.0"},{"emoji":"🔐","aliases":["closed_lock_with_key"],"tags":["security"],"category":"Objects","description":"locked with key","unicode_version":"6.0"},{"emoji":"🔑","aliases":["key"],"tags":["lock","password"],"category":"Objects","description":"key","unicode_version":"6.0"},{"emoji":"🗝️","aliases":["old_key"],"tags":[],"category":"Objects","description":"old key","unicode_version":"7.0"},{"emoji":"🔨","aliases":["hammer"],"tags":["tool"],"category":"Objects","description":"hammer","unicode_version":"6.0"},{"emoji":"🪓","aliases":["axe"],"tags":[],"category":"Objects","description":"axe","unicode_version":"12.0"},{"emoji":"⛏️","aliases":["pick"],"tags":[],"category":"Objects","description":"pick","unicode_version":"5.2"},{"emoji":"⚒️","aliases":["hammer_and_pick"],"tags":[],"category":"Objects","description":"hammer and pick","unicode_version":"4.1"},{"emoji":"🛠️","aliases":["hammer_and_wrench"],"tags":[],"category":"Objects","description":"hammer and wrench","unicode_version":"7.0"},{"emoji":"🗡️","aliases":["dagger"],"tags":[],"category":"Objects","description":"dagger","unicode_version":"7.0"},{"emoji":"⚔️","aliases":["crossed_swords"],"tags":[],"category":"Objects","description":"crossed swords","unicode_version":"4.1"},{"emoji":"🔫","aliases":["gun"],"tags":["shoot","weapon"],"category":"Objects","description":"water pistol","unicode_version":"6.0"},{"emoji":"🪃","aliases":["boomerang"],"tags":[],"category":"Objects","description":"boomerang","unicode_version":"13.0"},{"emoji":"🏹","aliases":["bow_and_arrow"],"tags":["archery"],"category":"Objects","description":"bow and arrow","unicode_version":"8.0"},{"emoji":"🛡️","aliases":["shield"],"tags":[],"category":"Objects","description":"shield","unicode_version":"7.0"},{"emoji":"🪚","aliases":["carpentry_saw"],"tags":[],"category":"Objects","description":"carpentry saw","unicode_version":"13.0"},{"emoji":"🔧","aliases":["wrench"],"tags":["tool"],"category":"Objects","description":"wrench","unicode_version":"6.0"},{"emoji":"🪛","aliases":["screwdriver"],"tags":[],"category":"Objects","description":"screwdriver","unicode_version":"13.0"},{"emoji":"🔩","aliases":["nut_and_bolt"],"tags":[],"category":"Objects","description":"nut and bolt","unicode_version":"6.0"},{"emoji":"⚙️","aliases":["gear"],"tags":[],"category":"Objects","description":"gear","unicode_version":"4.1"},{"emoji":"🗜️","aliases":["clamp"],"tags":[],"category":"Objects","description":"clamp","unicode_version":"7.0"},{"emoji":"⚖️","aliases":["balance_scale"],"tags":[],"category":"Objects","description":"balance scale","unicode_version":"4.1"},{"emoji":"🦯","aliases":["probing_cane"],"tags":[],"category":"Objects","description":"white cane","unicode_version":"12.0"},{"emoji":"🔗","aliases":["link"],"tags":[],"category":"Objects","description":"link","unicode_version":"6.0"},{"emoji":"⛓️","aliases":["chains"],"tags":[],"category":"Objects","description":"chains","unicode_version":"5.2"},{"emoji":"🪝","aliases":["hook"],"tags":[],"category":"Objects","description":"hook","unicode_version":"13.0"},{"emoji":"🧰","aliases":["toolbox"],"tags":[],"category":"Objects","description":"toolbox","unicode_version":"11.0"},{"emoji":"🧲","aliases":["magnet"],"tags":[],"category":"Objects","description":"magnet","unicode_version":"11.0"},{"emoji":"🪜","aliases":["ladder"],"tags":[],"category":"Objects","description":"ladder","unicode_version":"13.0"},{"emoji":"⚗️","aliases":["alembic"],"tags":[],"category":"Objects","description":"alembic","unicode_version":"4.1"},{"emoji":"🧪","aliases":["test_tube"],"tags":[],"category":"Objects","description":"test tube","unicode_version":"11.0"},{"emoji":"🧫","aliases":["petri_dish"],"tags":[],"category":"Objects","description":"petri dish","unicode_version":"11.0"},{"emoji":"🧬","aliases":["dna"],"tags":[],"category":"Objects","description":"dna","unicode_version":"11.0"},{"emoji":"🔬","aliases":["microscope"],"tags":["science","laboratory","investigate"],"category":"Objects","description":"microscope","unicode_version":"6.0"},{"emoji":"🔭","aliases":["telescope"],"tags":[],"category":"Objects","description":"telescope","unicode_version":"6.0"},{"emoji":"📡","aliases":["satellite"],"tags":["signal"],"category":"Objects","description":"satellite antenna","unicode_version":"6.0"},{"emoji":"💉","aliases":["syringe"],"tags":["health","hospital","needle"],"category":"Objects","description":"syringe","unicode_version":"6.0"},{"emoji":"🩸","aliases":["drop_of_blood"],"tags":[],"category":"Objects","description":"drop of blood","unicode_version":"12.0"},{"emoji":"💊","aliases":["pill"],"tags":["health","medicine"],"category":"Objects","description":"pill","unicode_version":"6.0"},{"emoji":"🩹","aliases":["adhesive_bandage"],"tags":[],"category":"Objects","description":"adhesive bandage","unicode_version":"12.0"},{"emoji":"🩺","aliases":["stethoscope"],"tags":[],"category":"Objects","description":"stethoscope","unicode_version":"12.0"},{"emoji":"🚪","aliases":["door"],"tags":[],"category":"Objects","description":"door","unicode_version":"6.0"},{"emoji":"🛗","aliases":["elevator"],"tags":[],"category":"Objects","description":"elevator","unicode_version":"13.0"},{"emoji":"🪞","aliases":["mirror"],"tags":[],"category":"Objects","description":"mirror","unicode_version":"13.0"},{"emoji":"🪟","aliases":["window"],"tags":[],"category":"Objects","description":"window","unicode_version":"13.0"},{"emoji":"🛏️","aliases":["bed"],"tags":[],"category":"Objects","description":"bed","unicode_version":"7.0"},{"emoji":"🛋️","aliases":["couch_and_lamp"],"tags":[],"category":"Objects","description":"couch and lamp","unicode_version":"7.0"},{"emoji":"🪑","aliases":["chair"],"tags":[],"category":"Objects","description":"chair","unicode_version":"12.0"},{"emoji":"🚽","aliases":["toilet"],"tags":["wc"],"category":"Objects","description":"toilet","unicode_version":"6.0"},{"emoji":"🪠","aliases":["plunger"],"tags":[],"category":"Objects","description":"plunger","unicode_version":"13.0"},{"emoji":"🚿","aliases":["shower"],"tags":["bath"],"category":"Objects","description":"shower","unicode_version":"6.0"},{"emoji":"🛁","aliases":["bathtub"],"tags":[],"category":"Objects","description":"bathtub","unicode_version":"6.0"},{"emoji":"🪤","aliases":["mouse_trap"],"tags":[],"category":"Objects","description":"mouse trap","unicode_version":"13.0"},{"emoji":"🪒","aliases":["razor"],"tags":[],"category":"Objects","description":"razor","unicode_version":"12.0"},{"emoji":"🧴","aliases":["lotion_bottle"],"tags":[],"category":"Objects","description":"lotion bottle","unicode_version":"11.0"},{"emoji":"🧷","aliases":["safety_pin"],"tags":[],"category":"Objects","description":"safety pin","unicode_version":"11.0"},{"emoji":"🧹","aliases":["broom"],"tags":[],"category":"Objects","description":"broom","unicode_version":"11.0"},{"emoji":"🧺","aliases":["basket"],"tags":[],"category":"Objects","description":"basket","unicode_version":"11.0"},{"emoji":"🧻","aliases":["roll_of_paper"],"tags":["toilet"],"category":"Objects","description":"roll of paper","unicode_version":"11.0"},{"emoji":"🪣","aliases":["bucket"],"tags":[],"category":"Objects","description":"bucket","unicode_version":"13.0"},{"emoji":"🧼","aliases":["soap"],"tags":[],"category":"Objects","description":"soap","unicode_version":"11.0"},{"emoji":"🪥","aliases":["toothbrush"],"tags":[],"category":"Objects","description":"toothbrush","unicode_version":"13.0"},{"emoji":"🧽","aliases":["sponge"],"tags":[],"category":"Objects","description":"sponge","unicode_version":"11.0"},{"emoji":"🧯","aliases":["fire_extinguisher"],"tags":[],"category":"Objects","description":"fire extinguisher","unicode_version":"11.0"},{"emoji":"🛒","aliases":["shopping_cart"],"tags":[],"category":"Objects","description":"shopping cart","unicode_version":"9.0"},{"emoji":"🚬","aliases":["smoking"],"tags":["cigarette"],"category":"Objects","description":"cigarette","unicode_version":"6.0"},{"emoji":"⚰️","aliases":["coffin"],"tags":["funeral"],"category":"Objects","description":"coffin","unicode_version":"4.1"},{"emoji":"🪦","aliases":["headstone"],"tags":[],"category":"Objects","description":"headstone","unicode_version":"13.0"},{"emoji":"⚱️","aliases":["funeral_urn"],"tags":[],"category":"Objects","description":"funeral urn","unicode_version":"4.1"},{"emoji":"🗿","aliases":["moyai"],"tags":["stone"],"category":"Objects","description":"moai","unicode_version":"6.0"},{"emoji":"🪧","aliases":["placard"],"tags":[],"category":"Objects","description":"placard","unicode_version":"13.0"},{"emoji":"🏧","aliases":["atm"],"tags":[],"category":"Symbols","description":"ATM sign","unicode_version":"6.0"},{"emoji":"🚮","aliases":["put_litter_in_its_place"],"tags":[],"category":"Symbols","description":"litter in bin sign","unicode_version":"6.0"},{"emoji":"🚰","aliases":["potable_water"],"tags":[],"category":"Symbols","description":"potable water","unicode_version":"6.0"},{"emoji":"♿","aliases":["wheelchair"],"tags":["accessibility"],"category":"Symbols","description":"wheelchair symbol","unicode_version":"4.1"},{"emoji":"🚹","aliases":["mens"],"tags":[],"category":"Symbols","description":"men’s room","unicode_version":"6.0"},{"emoji":"🚺","aliases":["womens"],"tags":[],"category":"Symbols","description":"women’s room","unicode_version":"6.0"},{"emoji":"🚻","aliases":["restroom"],"tags":["toilet"],"category":"Symbols","description":"restroom","unicode_version":"6.0"},{"emoji":"🚼","aliases":["baby_symbol"],"tags":[],"category":"Symbols","description":"baby symbol","unicode_version":"6.0"},{"emoji":"🚾","aliases":["wc"],"tags":["toilet","restroom"],"category":"Symbols","description":"water closet","unicode_version":"6.0"},{"emoji":"🛂","aliases":["passport_control"],"tags":[],"category":"Symbols","description":"passport control","unicode_version":"6.0"},{"emoji":"🛃","aliases":["customs"],"tags":[],"category":"Symbols","description":"customs","unicode_version":"6.0"},{"emoji":"🛄","aliases":["baggage_claim"],"tags":["airport"],"category":"Symbols","description":"baggage claim","unicode_version":"6.0"},{"emoji":"🛅","aliases":["left_luggage"],"tags":[],"category":"Symbols","description":"left luggage","unicode_version":"6.0"},{"emoji":"⚠️","aliases":["warning"],"tags":["wip"],"category":"Symbols","description":"warning","unicode_version":"4.0"},{"emoji":"🚸","aliases":["children_crossing"],"tags":[],"category":"Symbols","description":"children crossing","unicode_version":"6.0"},{"emoji":"⛔","aliases":["no_entry"],"tags":["limit"],"category":"Symbols","description":"no entry","unicode_version":"5.2"},{"emoji":"🚫","aliases":["no_entry_sign"],"tags":["block","forbidden"],"category":"Symbols","description":"prohibited","unicode_version":"6.0"},{"emoji":"🚳","aliases":["no_bicycles"],"tags":[],"category":"Symbols","description":"no bicycles","unicode_version":"6.0"},{"emoji":"🚭","aliases":["no_smoking"],"tags":[],"category":"Symbols","description":"no smoking","unicode_version":"6.0"},{"emoji":"🚯","aliases":["do_not_litter"],"tags":[],"category":"Symbols","description":"no littering","unicode_version":"6.0"},{"emoji":"🚱","aliases":["non-potable_water"],"tags":[],"category":"Symbols","description":"non-potable water","unicode_version":"6.0"},{"emoji":"🚷","aliases":["no_pedestrians"],"tags":[],"category":"Symbols","description":"no pedestrians","unicode_version":"6.0"},{"emoji":"📵","aliases":["no_mobile_phones"],"tags":[],"category":"Symbols","description":"no mobile phones","unicode_version":"6.0"},{"emoji":"🔞","aliases":["underage"],"tags":[],"category":"Symbols","description":"no one under eighteen","unicode_version":"6.0"},{"emoji":"☢️","aliases":["radioactive"],"tags":[],"category":"Symbols","description":"radioactive","unicode_version":""},{"emoji":"☣️","aliases":["biohazard"],"tags":[],"category":"Symbols","description":"biohazard","unicode_version":""},{"emoji":"⬆️","aliases":["arrow_up"],"tags":[],"category":"Symbols","description":"up arrow","unicode_version":"4.0"},{"emoji":"↗️","aliases":["arrow_upper_right"],"tags":[],"category":"Symbols","description":"up-right arrow","unicode_version":""},{"emoji":"➡️","aliases":["arrow_right"],"tags":[],"category":"Symbols","description":"right arrow","unicode_version":""},{"emoji":"↘️","aliases":["arrow_lower_right"],"tags":[],"category":"Symbols","description":"down-right arrow","unicode_version":""},{"emoji":"⬇️","aliases":["arrow_down"],"tags":[],"category":"Symbols","description":"down arrow","unicode_version":"4.0"},{"emoji":"↙️","aliases":["arrow_lower_left"],"tags":[],"category":"Symbols","description":"down-left arrow","unicode_version":""},{"emoji":"⬅️","aliases":["arrow_left"],"tags":[],"category":"Symbols","description":"left arrow","unicode_version":"4.0"},{"emoji":"↖️","aliases":["arrow_upper_left"],"tags":[],"category":"Symbols","description":"up-left arrow","unicode_version":""},{"emoji":"↕️","aliases":["arrow_up_down"],"tags":[],"category":"Symbols","description":"up-down arrow","unicode_version":""},{"emoji":"↔️","aliases":["left_right_arrow"],"tags":[],"category":"Symbols","description":"left-right arrow","unicode_version":""},{"emoji":"↩️","aliases":["leftwards_arrow_with_hook"],"tags":["return"],"category":"Symbols","description":"right arrow curving left","unicode_version":""},{"emoji":"↪️","aliases":["arrow_right_hook"],"tags":[],"category":"Symbols","description":"left arrow curving right","unicode_version":""},{"emoji":"⤴️","aliases":["arrow_heading_up"],"tags":[],"category":"Symbols","description":"right arrow curving up","unicode_version":""},{"emoji":"⤵️","aliases":["arrow_heading_down"],"tags":[],"category":"Symbols","description":"right arrow curving down","unicode_version":""},{"emoji":"🔃","aliases":["arrows_clockwise"],"tags":[],"category":"Symbols","description":"clockwise vertical arrows","unicode_version":"6.0"},{"emoji":"🔄","aliases":["arrows_counterclockwise"],"tags":["sync"],"category":"Symbols","description":"counterclockwise arrows button","unicode_version":"6.0"},{"emoji":"🔙","aliases":["back"],"tags":[],"category":"Symbols","description":"BACK arrow","unicode_version":"6.0"},{"emoji":"🔚","aliases":["end"],"tags":[],"category":"Symbols","description":"END arrow","unicode_version":"6.0"},{"emoji":"🔛","aliases":["on"],"tags":[],"category":"Symbols","description":"ON! arrow","unicode_version":"6.0"},{"emoji":"🔜","aliases":["soon"],"tags":[],"category":"Symbols","description":"SOON arrow","unicode_version":"6.0"},{"emoji":"🔝","aliases":["top"],"tags":[],"category":"Symbols","description":"TOP arrow","unicode_version":"6.0"},{"emoji":"🛐","aliases":["place_of_worship"],"tags":[],"category":"Symbols","description":"place of worship","unicode_version":"8.0"},{"emoji":"⚛️","aliases":["atom_symbol"],"tags":[],"category":"Symbols","description":"atom symbol","unicode_version":"4.1"},{"emoji":"🕉️","aliases":["om"],"tags":[],"category":"Symbols","description":"om","unicode_version":"7.0"},{"emoji":"✡️","aliases":["star_of_david"],"tags":[],"category":"Symbols","description":"star of David","unicode_version":""},{"emoji":"☸️","aliases":["wheel_of_dharma"],"tags":[],"category":"Symbols","description":"wheel of dharma","unicode_version":""},{"emoji":"☯️","aliases":["yin_yang"],"tags":[],"category":"Symbols","description":"yin yang","unicode_version":""},{"emoji":"✝️","aliases":["latin_cross"],"tags":[],"category":"Symbols","description":"latin cross","unicode_version":""},{"emoji":"☦️","aliases":["orthodox_cross"],"tags":[],"category":"Symbols","description":"orthodox cross","unicode_version":""},{"emoji":"☪️","aliases":["star_and_crescent"],"tags":[],"category":"Symbols","description":"star and crescent","unicode_version":""},{"emoji":"☮️","aliases":["peace_symbol"],"tags":[],"category":"Symbols","description":"peace symbol","unicode_version":""},{"emoji":"🕎","aliases":["menorah"],"tags":[],"category":"Symbols","description":"menorah","unicode_version":"8.0"},{"emoji":"🔯","aliases":["six_pointed_star"],"tags":[],"category":"Symbols","description":"dotted six-pointed star","unicode_version":"6.0"},{"emoji":"♈","aliases":["aries"],"tags":[],"category":"Symbols","description":"Aries","unicode_version":""},{"emoji":"♉","aliases":["taurus"],"tags":[],"category":"Symbols","description":"Taurus","unicode_version":""},{"emoji":"♊","aliases":["gemini"],"tags":[],"category":"Symbols","description":"Gemini","unicode_version":""},{"emoji":"♋","aliases":["cancer"],"tags":[],"category":"Symbols","description":"Cancer","unicode_version":""},{"emoji":"♌","aliases":["leo"],"tags":[],"category":"Symbols","description":"Leo","unicode_version":""},{"emoji":"♍","aliases":["virgo"],"tags":[],"category":"Symbols","description":"Virgo","unicode_version":""},{"emoji":"♎","aliases":["libra"],"tags":[],"category":"Symbols","description":"Libra","unicode_version":""},{"emoji":"♏","aliases":["scorpius"],"tags":[],"category":"Symbols","description":"Scorpio","unicode_version":""},{"emoji":"♐","aliases":["sagittarius"],"tags":[],"category":"Symbols","description":"Sagittarius","unicode_version":""},{"emoji":"♑","aliases":["capricorn"],"tags":[],"category":"Symbols","description":"Capricorn","unicode_version":""},{"emoji":"♒","aliases":["aquarius"],"tags":[],"category":"Symbols","description":"Aquarius","unicode_version":""},{"emoji":"♓","aliases":["pisces"],"tags":[],"category":"Symbols","description":"Pisces","unicode_version":""},{"emoji":"⛎","aliases":["ophiuchus"],"tags":[],"category":"Symbols","description":"Ophiuchus","unicode_version":"6.0"},{"emoji":"🔀","aliases":["twisted_rightwards_arrows"],"tags":["shuffle"],"category":"Symbols","description":"shuffle tracks button","unicode_version":"6.0"},{"emoji":"🔁","aliases":["repeat"],"tags":["loop"],"category":"Symbols","description":"repeat button","unicode_version":"6.0"},{"emoji":"🔂","aliases":["repeat_one"],"tags":[],"category":"Symbols","description":"repeat single button","unicode_version":"6.0"},{"emoji":"▶️","aliases":["arrow_forward"],"tags":[],"category":"Symbols","description":"play button","unicode_version":""},{"emoji":"⏩","aliases":["fast_forward"],"tags":[],"category":"Symbols","description":"fast-forward button","unicode_version":"6.0"},{"emoji":"⏭️","aliases":["next_track_button"],"tags":[],"category":"Symbols","description":"next track button","unicode_version":"6.0"},{"emoji":"⏯️","aliases":["play_or_pause_button"],"tags":[],"category":"Symbols","description":"play or pause button","unicode_version":"6.0"},{"emoji":"◀️","aliases":["arrow_backward"],"tags":[],"category":"Symbols","description":"reverse button","unicode_version":""},{"emoji":"⏪","aliases":["rewind"],"tags":[],"category":"Symbols","description":"fast reverse button","unicode_version":"6.0"},{"emoji":"⏮️","aliases":["previous_track_button"],"tags":[],"category":"Symbols","description":"last track button","unicode_version":"6.0"},{"emoji":"🔼","aliases":["arrow_up_small"],"tags":[],"category":"Symbols","description":"upwards button","unicode_version":"6.0"},{"emoji":"⏫","aliases":["arrow_double_up"],"tags":[],"category":"Symbols","description":"fast up button","unicode_version":"6.0"},{"emoji":"🔽","aliases":["arrow_down_small"],"tags":[],"category":"Symbols","description":"downwards button","unicode_version":"6.0"},{"emoji":"⏬","aliases":["arrow_double_down"],"tags":[],"category":"Symbols","description":"fast down button","unicode_version":"6.0"},{"emoji":"⏸️","aliases":["pause_button"],"tags":[],"category":"Symbols","description":"pause button","unicode_version":"7.0"},{"emoji":"⏹️","aliases":["stop_button"],"tags":[],"category":"Symbols","description":"stop button","unicode_version":"7.0"},{"emoji":"⏺️","aliases":["record_button"],"tags":[],"category":"Symbols","description":"record button","unicode_version":"7.0"},{"emoji":"⏏️","aliases":["eject_button"],"tags":[],"category":"Symbols","description":"eject button","unicode_version":"11.0"},{"emoji":"🎦","aliases":["cinema"],"tags":["film","movie"],"category":"Symbols","description":"cinema","unicode_version":"6.0"},{"emoji":"🔅","aliases":["low_brightness"],"tags":[],"category":"Symbols","description":"dim button","unicode_version":"6.0"},{"emoji":"🔆","aliases":["high_brightness"],"tags":[],"category":"Symbols","description":"bright button","unicode_version":"6.0"},{"emoji":"📶","aliases":["signal_strength"],"tags":["wifi"],"category":"Symbols","description":"antenna bars","unicode_version":"6.0"},{"emoji":"📳","aliases":["vibration_mode"],"tags":[],"category":"Symbols","description":"vibration mode","unicode_version":"6.0"},{"emoji":"📴","aliases":["mobile_phone_off"],"tags":["mute","off"],"category":"Symbols","description":"mobile phone off","unicode_version":"6.0"},{"emoji":"♀️","aliases":["female_sign"],"tags":[],"category":"Symbols","description":"female sign","unicode_version":"11.0"},{"emoji":"♂️","aliases":["male_sign"],"tags":[],"category":"Symbols","description":"male sign","unicode_version":"11.0"},{"emoji":"⚧️","aliases":["transgender_symbol"],"tags":[],"category":"Symbols","description":"transgender symbol","unicode_version":"13.0"},{"emoji":"✖️","aliases":["heavy_multiplication_x"],"tags":[],"category":"Symbols","description":"multiply","unicode_version":""},{"emoji":"➕","aliases":["heavy_plus_sign"],"tags":[],"category":"Symbols","description":"plus","unicode_version":"6.0"},{"emoji":"➖","aliases":["heavy_minus_sign"],"tags":[],"category":"Symbols","description":"minus","unicode_version":"6.0"},{"emoji":"➗","aliases":["heavy_division_sign"],"tags":[],"category":"Symbols","description":"divide","unicode_version":"6.0"},{"emoji":"♾️","aliases":["infinity"],"tags":[],"category":"Symbols","description":"infinity","unicode_version":"11.0"},{"emoji":"‼️","aliases":["bangbang"],"tags":[],"category":"Symbols","description":"double exclamation mark","unicode_version":""},{"emoji":"⁉️","aliases":["interrobang"],"tags":[],"category":"Symbols","description":"exclamation question mark","unicode_version":"3.0"},{"emoji":"❓","aliases":["question"],"tags":["confused"],"category":"Symbols","description":"red question mark","unicode_version":"6.0"},{"emoji":"❔","aliases":["grey_question"],"tags":[],"category":"Symbols","description":"white question mark","unicode_version":"6.0"},{"emoji":"❕","aliases":["grey_exclamation"],"tags":[],"category":"Symbols","description":"white exclamation mark","unicode_version":"6.0"},{"emoji":"❗","aliases":["exclamation","heavy_exclamation_mark"],"tags":["bang"],"category":"Symbols","description":"red exclamation mark","unicode_version":"5.2"},{"emoji":"〰️","aliases":["wavy_dash"],"tags":[],"category":"Symbols","description":"wavy dash","unicode_version":""},{"emoji":"💱","aliases":["currency_exchange"],"tags":[],"category":"Symbols","description":"currency exchange","unicode_version":"6.0"},{"emoji":"💲","aliases":["heavy_dollar_sign"],"tags":[],"category":"Symbols","description":"heavy dollar sign","unicode_version":"6.0"},{"emoji":"⚕️","aliases":["medical_symbol"],"tags":[],"category":"Symbols","description":"medical symbol","unicode_version":"11.0"},{"emoji":"♻️","aliases":["recycle"],"tags":["environment","green"],"category":"Symbols","description":"recycling symbol","unicode_version":"3.2"},{"emoji":"⚜️","aliases":["fleur_de_lis"],"tags":[],"category":"Symbols","description":"fleur-de-lis","unicode_version":"4.1"},{"emoji":"🔱","aliases":["trident"],"tags":[],"category":"Symbols","description":"trident emblem","unicode_version":"6.0"},{"emoji":"📛","aliases":["name_badge"],"tags":[],"category":"Symbols","description":"name badge","unicode_version":"6.0"},{"emoji":"🔰","aliases":["beginner"],"tags":[],"category":"Symbols","description":"Japanese symbol for beginner","unicode_version":"6.0"},{"emoji":"⭕","aliases":["o"],"tags":[],"category":"Symbols","description":"hollow red circle","unicode_version":"5.2"},{"emoji":"✅","aliases":["white_check_mark"],"tags":[],"category":"Symbols","description":"check mark button","unicode_version":"6.0"},{"emoji":"☑️","aliases":["ballot_box_with_check"],"tags":[],"category":"Symbols","description":"check box with check","unicode_version":""},{"emoji":"✔️","aliases":["heavy_check_mark"],"tags":[],"category":"Symbols","description":"check mark","unicode_version":""},{"emoji":"❌","aliases":["x"],"tags":[],"category":"Symbols","description":"cross mark","unicode_version":"6.0"},{"emoji":"❎","aliases":["negative_squared_cross_mark"],"tags":[],"category":"Symbols","description":"cross mark button","unicode_version":"6.0"},{"emoji":"➰","aliases":["curly_loop"],"tags":[],"category":"Symbols","description":"curly loop","unicode_version":"6.0"},{"emoji":"➿","aliases":["loop"],"tags":[],"category":"Symbols","description":"double curly loop","unicode_version":"6.0"},{"emoji":"〽️","aliases":["part_alternation_mark"],"tags":[],"category":"Symbols","description":"part alternation mark","unicode_version":"3.2"},{"emoji":"✳️","aliases":["eight_spoked_asterisk"],"tags":[],"category":"Symbols","description":"eight-spoked asterisk","unicode_version":""},{"emoji":"✴️","aliases":["eight_pointed_black_star"],"tags":[],"category":"Symbols","description":"eight-pointed star","unicode_version":""},{"emoji":"❇️","aliases":["sparkle"],"tags":[],"category":"Symbols","description":"sparkle","unicode_version":""},{"emoji":"©️","aliases":["copyright"],"tags":[],"category":"Symbols","description":"copyright","unicode_version":""},{"emoji":"®️","aliases":["registered"],"tags":[],"category":"Symbols","description":"registered","unicode_version":""},{"emoji":"™️","aliases":["tm"],"tags":["trademark"],"category":"Symbols","description":"trade mark","unicode_version":""},{"emoji":"#️⃣","aliases":["hash"],"tags":["number"],"category":"Symbols","description":"keycap: #","unicode_version":""},{"emoji":"*️⃣","aliases":["asterisk"],"tags":[],"category":"Symbols","description":"keycap: *","unicode_version":""},{"emoji":"0️⃣","aliases":["zero"],"tags":[],"category":"Symbols","description":"keycap: 0","unicode_version":""},{"emoji":"1️⃣","aliases":["one"],"tags":[],"category":"Symbols","description":"keycap: 1","unicode_version":""},{"emoji":"2️⃣","aliases":["two"],"tags":[],"category":"Symbols","description":"keycap: 2","unicode_version":""},{"emoji":"3️⃣","aliases":["three"],"tags":[],"category":"Symbols","description":"keycap: 3","unicode_version":""},{"emoji":"4️⃣","aliases":["four"],"tags":[],"category":"Symbols","description":"keycap: 4","unicode_version":""},{"emoji":"5️⃣","aliases":["five"],"tags":[],"category":"Symbols","description":"keycap: 5","unicode_version":""},{"emoji":"6️⃣","aliases":["six"],"tags":[],"category":"Symbols","description":"keycap: 6","unicode_version":""},{"emoji":"7️⃣","aliases":["seven"],"tags":[],"category":"Symbols","description":"keycap: 7","unicode_version":""},{"emoji":"8️⃣","aliases":["eight"],"tags":[],"category":"Symbols","description":"keycap: 8","unicode_version":""},{"emoji":"9️⃣","aliases":["nine"],"tags":[],"category":"Symbols","description":"keycap: 9","unicode_version":""},{"emoji":"🔟","aliases":["keycap_ten"],"tags":[],"category":"Symbols","description":"keycap: 10","unicode_version":"6.0"},{"emoji":"🔠","aliases":["capital_abcd"],"tags":["letters"],"category":"Symbols","description":"input latin uppercase","unicode_version":"6.0"},{"emoji":"🔡","aliases":["abcd"],"tags":[],"category":"Symbols","description":"input latin lowercase","unicode_version":"6.0"},{"emoji":"🔢","aliases":["1234"],"tags":["numbers"],"category":"Symbols","description":"input numbers","unicode_version":"6.0"},{"emoji":"🔣","aliases":["symbols"],"tags":[],"category":"Symbols","description":"input symbols","unicode_version":"6.0"},{"emoji":"🔤","aliases":["abc"],"tags":["alphabet"],"category":"Symbols","description":"input latin letters","unicode_version":"6.0"},{"emoji":"🅰️","aliases":["a"],"tags":[],"category":"Symbols","description":"A button (blood type)","unicode_version":"6.0"},{"emoji":"🆎","aliases":["ab"],"tags":[],"category":"Symbols","description":"AB button (blood type)","unicode_version":"6.0"},{"emoji":"🅱️","aliases":["b"],"tags":[],"category":"Symbols","description":"B button (blood type)","unicode_version":"6.0"},{"emoji":"🆑","aliases":["cl"],"tags":[],"category":"Symbols","description":"CL button","unicode_version":"6.0"},{"emoji":"🆒","aliases":["cool"],"tags":[],"category":"Symbols","description":"COOL button","unicode_version":"6.0"},{"emoji":"🆓","aliases":["free"],"tags":[],"category":"Symbols","description":"FREE button","unicode_version":"6.0"},{"emoji":"ℹ️","aliases":["information_source"],"tags":[],"category":"Symbols","description":"information","unicode_version":"3.0"},{"emoji":"🆔","aliases":["id"],"tags":[],"category":"Symbols","description":"ID button","unicode_version":"6.0"},{"emoji":"Ⓜ️","aliases":["m"],"tags":[],"category":"Symbols","description":"circled M","unicode_version":""},{"emoji":"🆕","aliases":["new"],"tags":["fresh"],"category":"Symbols","description":"NEW button","unicode_version":"6.0"},{"emoji":"🆖","aliases":["ng"],"tags":[],"category":"Symbols","description":"NG button","unicode_version":"6.0"},{"emoji":"🅾️","aliases":["o2"],"tags":[],"category":"Symbols","description":"O button (blood type)","unicode_version":"6.0"},{"emoji":"🆗","aliases":["ok"],"tags":["yes"],"category":"Symbols","description":"OK button","unicode_version":"6.0"},{"emoji":"🅿️","aliases":["parking"],"tags":[],"category":"Symbols","description":"P button","unicode_version":"5.2"},{"emoji":"🆘","aliases":["sos"],"tags":["help","emergency"],"category":"Symbols","description":"SOS button","unicode_version":"6.0"},{"emoji":"🆙","aliases":["up"],"tags":[],"category":"Symbols","description":"UP! button","unicode_version":"6.0"},{"emoji":"🆚","aliases":["vs"],"tags":[],"category":"Symbols","description":"VS button","unicode_version":"6.0"},{"emoji":"🈁","aliases":["koko"],"tags":[],"category":"Symbols","description":"Japanese “here” button","unicode_version":"6.0"},{"emoji":"🈂️","aliases":["sa"],"tags":[],"category":"Symbols","description":"Japanese “service charge” button","unicode_version":"6.0"},{"emoji":"🈷️","aliases":["u6708"],"tags":[],"category":"Symbols","description":"Japanese “monthly amount” button","unicode_version":"6.0"},{"emoji":"🈶","aliases":["u6709"],"tags":[],"category":"Symbols","description":"Japanese “not free of charge” button","unicode_version":"6.0"},{"emoji":"🈯","aliases":["u6307"],"tags":[],"category":"Symbols","description":"Japanese “reserved” button","unicode_version":""},{"emoji":"🉐","aliases":["ideograph_advantage"],"tags":[],"category":"Symbols","description":"Japanese “bargain” button","unicode_version":"6.0"},{"emoji":"🈹","aliases":["u5272"],"tags":[],"category":"Symbols","description":"Japanese “discount” button","unicode_version":"6.0"},{"emoji":"🈚","aliases":["u7121"],"tags":[],"category":"Symbols","description":"Japanese “free of charge” button","unicode_version":""},{"emoji":"🈲","aliases":["u7981"],"tags":[],"category":"Symbols","description":"Japanese “prohibited” button","unicode_version":"6.0"},{"emoji":"🉑","aliases":["accept"],"tags":[],"category":"Symbols","description":"Japanese “acceptable” button","unicode_version":"6.0"},{"emoji":"🈸","aliases":["u7533"],"tags":[],"category":"Symbols","description":"Japanese “application” button","unicode_version":"6.0"},{"emoji":"🈴","aliases":["u5408"],"tags":[],"category":"Symbols","description":"Japanese “passing grade” button","unicode_version":"6.0"},{"emoji":"🈳","aliases":["u7a7a"],"tags":[],"category":"Symbols","description":"Japanese “vacancy” button","unicode_version":"6.0"},{"emoji":"㊗️","aliases":["congratulations"],"tags":[],"category":"Symbols","description":"Japanese “congratulations” button","unicode_version":""},{"emoji":"㊙️","aliases":["secret"],"tags":[],"category":"Symbols","description":"Japanese “secret” button","unicode_version":""},{"emoji":"🈺","aliases":["u55b6"],"tags":[],"category":"Symbols","description":"Japanese “open for business” button","unicode_version":"6.0"},{"emoji":"🈵","aliases":["u6e80"],"tags":[],"category":"Symbols","description":"Japanese “no vacancy” button","unicode_version":"6.0"},{"emoji":"🔴","aliases":["red_circle"],"tags":[],"category":"Symbols","description":"red circle","unicode_version":"6.0"},{"emoji":"🟠","aliases":["orange_circle"],"tags":[],"category":"Symbols","description":"orange circle","unicode_version":"12.0"},{"emoji":"🟡","aliases":["yellow_circle"],"tags":[],"category":"Symbols","description":"yellow circle","unicode_version":"12.0"},{"emoji":"🟢","aliases":["green_circle"],"tags":[],"category":"Symbols","description":"green circle","unicode_version":"12.0"},{"emoji":"🔵","aliases":["large_blue_circle"],"tags":[],"category":"Symbols","description":"blue circle","unicode_version":"6.0"},{"emoji":"🟣","aliases":["purple_circle"],"tags":[],"category":"Symbols","description":"purple circle","unicode_version":"12.0"},{"emoji":"🟤","aliases":["brown_circle"],"tags":[],"category":"Symbols","description":"brown circle","unicode_version":"12.0"},{"emoji":"⚫","aliases":["black_circle"],"tags":[],"category":"Symbols","description":"black circle","unicode_version":"4.1"},{"emoji":"⚪","aliases":["white_circle"],"tags":[],"category":"Symbols","description":"white circle","unicode_version":"4.1"},{"emoji":"🟥","aliases":["red_square"],"tags":[],"category":"Symbols","description":"red square","unicode_version":"12.0"},{"emoji":"🟧","aliases":["orange_square"],"tags":[],"category":"Symbols","description":"orange square","unicode_version":"12.0"},{"emoji":"🟨","aliases":["yellow_square"],"tags":[],"category":"Symbols","description":"yellow square","unicode_version":"12.0"},{"emoji":"🟩","aliases":["green_square"],"tags":[],"category":"Symbols","description":"green square","unicode_version":"12.0"},{"emoji":"🟦","aliases":["blue_square"],"tags":[],"category":"Symbols","description":"blue square","unicode_version":"12.0"},{"emoji":"🟪","aliases":["purple_square"],"tags":[],"category":"Symbols","description":"purple square","unicode_version":"12.0"},{"emoji":"🟫","aliases":["brown_square"],"tags":[],"category":"Symbols","description":"brown square","unicode_version":"12.0"},{"emoji":"⬛","aliases":["black_large_square"],"tags":[],"category":"Symbols","description":"black large square","unicode_version":"5.1"},{"emoji":"⬜","aliases":["white_large_square"],"tags":[],"category":"Symbols","description":"white large square","unicode_version":"5.1"},{"emoji":"◼️","aliases":["black_medium_square"],"tags":[],"category":"Symbols","description":"black medium square","unicode_version":"3.2"},{"emoji":"◻️","aliases":["white_medium_square"],"tags":[],"category":"Symbols","description":"white medium square","unicode_version":"3.2"},{"emoji":"◾","aliases":["black_medium_small_square"],"tags":[],"category":"Symbols","description":"black medium-small square","unicode_version":"3.2"},{"emoji":"◽","aliases":["white_medium_small_square"],"tags":[],"category":"Symbols","description":"white medium-small square","unicode_version":"3.2"},{"emoji":"▪️","aliases":["black_small_square"],"tags":[],"category":"Symbols","description":"black small square","unicode_version":""},{"emoji":"▫️","aliases":["white_small_square"],"tags":[],"category":"Symbols","description":"white small square","unicode_version":""},{"emoji":"🔶","aliases":["large_orange_diamond"],"tags":[],"category":"Symbols","description":"large orange diamond","unicode_version":"6.0"},{"emoji":"🔷","aliases":["large_blue_diamond"],"tags":[],"category":"Symbols","description":"large blue diamond","unicode_version":"6.0"},{"emoji":"🔸","aliases":["small_orange_diamond"],"tags":[],"category":"Symbols","description":"small orange diamond","unicode_version":"6.0"},{"emoji":"🔹","aliases":["small_blue_diamond"],"tags":[],"category":"Symbols","description":"small blue diamond","unicode_version":"6.0"},{"emoji":"🔺","aliases":["small_red_triangle"],"tags":[],"category":"Symbols","description":"red triangle pointed up","unicode_version":"6.0"},{"emoji":"🔻","aliases":["small_red_triangle_down"],"tags":[],"category":"Symbols","description":"red triangle pointed down","unicode_version":"6.0"},{"emoji":"💠","aliases":["diamond_shape_with_a_dot_inside"],"tags":[],"category":"Symbols","description":"diamond with a dot","unicode_version":"6.0"},{"emoji":"🔘","aliases":["radio_button"],"tags":[],"category":"Symbols","description":"radio button","unicode_version":"6.0"},{"emoji":"🔳","aliases":["white_square_button"],"tags":[],"category":"Symbols","description":"white square button","unicode_version":"6.0"},{"emoji":"🔲","aliases":["black_square_button"],"tags":[],"category":"Symbols","description":"black square button","unicode_version":"6.0"},{"emoji":"🏁","aliases":["checkered_flag"],"tags":["milestone","finish"],"category":"Flags","description":"chequered flag","unicode_version":"6.0"},{"emoji":"🚩","aliases":["triangular_flag_on_post"],"tags":[],"category":"Flags","description":"triangular flag","unicode_version":"6.0"},{"emoji":"🎌","aliases":["crossed_flags"],"tags":[],"category":"Flags","description":"crossed flags","unicode_version":"6.0"},{"emoji":"🏴","aliases":["black_flag"],"tags":[],"category":"Flags","description":"black flag","unicode_version":"7.0"},{"emoji":"🏳️","aliases":["white_flag"],"tags":[],"category":"Flags","description":"white flag","unicode_version":"7.0"},{"emoji":"🏳️‍🌈","aliases":["rainbow_flag"],"tags":["pride"],"category":"Flags","description":"rainbow flag","unicode_version":"6.0"},{"emoji":"🏳️‍⚧️","aliases":["transgender_flag"],"tags":[],"category":"Flags","description":"transgender flag","unicode_version":"13.0"},{"emoji":"🏴‍☠️","aliases":["pirate_flag"],"tags":[],"category":"Flags","description":"pirate flag","unicode_version":"11.0"},{"emoji":"🇦🇨","aliases":["ascension_island"],"tags":[],"category":"Flags","description":"flag: Ascension Island","unicode_version":"11.0"},{"emoji":"🇦🇩","aliases":["andorra"],"tags":[],"category":"Flags","description":"flag: Andorra","unicode_version":"6.0"},{"emoji":"🇦🇪","aliases":["united_arab_emirates"],"tags":[],"category":"Flags","description":"flag: United Arab Emirates","unicode_version":"6.0"},{"emoji":"🇦🇫","aliases":["afghanistan"],"tags":[],"category":"Flags","description":"flag: Afghanistan","unicode_version":"6.0"},{"emoji":"🇦🇬","aliases":["antigua_barbuda"],"tags":[],"category":"Flags","description":"flag: Antigua & Barbuda","unicode_version":"6.0"},{"emoji":"🇦🇮","aliases":["anguilla"],"tags":[],"category":"Flags","description":"flag: Anguilla","unicode_version":"6.0"},{"emoji":"🇦🇱","aliases":["albania"],"tags":[],"category":"Flags","description":"flag: Albania","unicode_version":"6.0"},{"emoji":"🇦🇲","aliases":["armenia"],"tags":[],"category":"Flags","description":"flag: Armenia","unicode_version":"6.0"},{"emoji":"🇦🇴","aliases":["angola"],"tags":[],"category":"Flags","description":"flag: Angola","unicode_version":"6.0"},{"emoji":"🇦🇶","aliases":["antarctica"],"tags":[],"category":"Flags","description":"flag: Antarctica","unicode_version":"6.0"},{"emoji":"🇦🇷","aliases":["argentina"],"tags":[],"category":"Flags","description":"flag: Argentina","unicode_version":"6.0"},{"emoji":"🇦🇸","aliases":["american_samoa"],"tags":[],"category":"Flags","description":"flag: American Samoa","unicode_version":"6.0"},{"emoji":"🇦🇹","aliases":["austria"],"tags":[],"category":"Flags","description":"flag: Austria","unicode_version":"6.0"},{"emoji":"🇦🇺","aliases":["australia"],"tags":[],"category":"Flags","description":"flag: Australia","unicode_version":"6.0"},{"emoji":"🇦🇼","aliases":["aruba"],"tags":[],"category":"Flags","description":"flag: Aruba","unicode_version":"6.0"},{"emoji":"🇦🇽","aliases":["aland_islands"],"tags":[],"category":"Flags","description":"flag: Åland Islands","unicode_version":"6.0"},{"emoji":"🇦🇿","aliases":["azerbaijan"],"tags":[],"category":"Flags","description":"flag: Azerbaijan","unicode_version":"6.0"},{"emoji":"🇧🇦","aliases":["bosnia_herzegovina"],"tags":[],"category":"Flags","description":"flag: Bosnia & Herzegovina","unicode_version":"6.0"},{"emoji":"🇧🇧","aliases":["barbados"],"tags":[],"category":"Flags","description":"flag: Barbados","unicode_version":"6.0"},{"emoji":"🇧🇩","aliases":["bangladesh"],"tags":[],"category":"Flags","description":"flag: Bangladesh","unicode_version":"6.0"},{"emoji":"🇧🇪","aliases":["belgium"],"tags":[],"category":"Flags","description":"flag: Belgium","unicode_version":"6.0"},{"emoji":"🇧🇫","aliases":["burkina_faso"],"tags":[],"category":"Flags","description":"flag: Burkina Faso","unicode_version":"6.0"},{"emoji":"🇧🇬","aliases":["bulgaria"],"tags":[],"category":"Flags","description":"flag: Bulgaria","unicode_version":"6.0"},{"emoji":"🇧🇭","aliases":["bahrain"],"tags":[],"category":"Flags","description":"flag: Bahrain","unicode_version":"6.0"},{"emoji":"🇧🇮","aliases":["burundi"],"tags":[],"category":"Flags","description":"flag: Burundi","unicode_version":"6.0"},{"emoji":"🇧🇯","aliases":["benin"],"tags":[],"category":"Flags","description":"flag: Benin","unicode_version":"6.0"},{"emoji":"🇧🇱","aliases":["st_barthelemy"],"tags":[],"category":"Flags","description":"flag: St. Barthélemy","unicode_version":"6.0"},{"emoji":"🇧🇲","aliases":["bermuda"],"tags":[],"category":"Flags","description":"flag: Bermuda","unicode_version":"6.0"},{"emoji":"🇧🇳","aliases":["brunei"],"tags":[],"category":"Flags","description":"flag: Brunei","unicode_version":"6.0"},{"emoji":"🇧🇴","aliases":["bolivia"],"tags":[],"category":"Flags","description":"flag: Bolivia","unicode_version":"6.0"},{"emoji":"🇧🇶","aliases":["caribbean_netherlands"],"tags":[],"category":"Flags","description":"flag: Caribbean Netherlands","unicode_version":"6.0"},{"emoji":"🇧🇷","aliases":["brazil"],"tags":[],"category":"Flags","description":"flag: Brazil","unicode_version":"6.0"},{"emoji":"🇧🇸","aliases":["bahamas"],"tags":[],"category":"Flags","description":"flag: Bahamas","unicode_version":"6.0"},{"emoji":"🇧🇹","aliases":["bhutan"],"tags":[],"category":"Flags","description":"flag: Bhutan","unicode_version":"6.0"},{"emoji":"🇧🇻","aliases":["bouvet_island"],"tags":[],"category":"Flags","description":"flag: Bouvet Island","unicode_version":"11.0"},{"emoji":"🇧🇼","aliases":["botswana"],"tags":[],"category":"Flags","description":"flag: Botswana","unicode_version":"6.0"},{"emoji":"🇧🇾","aliases":["belarus"],"tags":[],"category":"Flags","description":"flag: Belarus","unicode_version":"6.0"},{"emoji":"🇧🇿","aliases":["belize"],"tags":[],"category":"Flags","description":"flag: Belize","unicode_version":"6.0"},{"emoji":"🇨🇦","aliases":["canada"],"tags":[],"category":"Flags","description":"flag: Canada","unicode_version":"6.0"},{"emoji":"🇨🇨","aliases":["cocos_islands"],"tags":["keeling"],"category":"Flags","description":"flag: Cocos (Keeling) Islands","unicode_version":"6.0"},{"emoji":"🇨🇩","aliases":["congo_kinshasa"],"tags":[],"category":"Flags","description":"flag: Congo - Kinshasa","unicode_version":"6.0"},{"emoji":"🇨🇫","aliases":["central_african_republic"],"tags":[],"category":"Flags","description":"flag: Central African Republic","unicode_version":"6.0"},{"emoji":"🇨🇬","aliases":["congo_brazzaville"],"tags":[],"category":"Flags","description":"flag: Congo - Brazzaville","unicode_version":"6.0"},{"emoji":"🇨🇭","aliases":["switzerland"],"tags":[],"category":"Flags","description":"flag: Switzerland","unicode_version":"6.0"},{"emoji":"🇨🇮","aliases":["cote_divoire"],"tags":["ivory"],"category":"Flags","description":"flag: Côte d’Ivoire","unicode_version":"6.0"},{"emoji":"🇨🇰","aliases":["cook_islands"],"tags":[],"category":"Flags","description":"flag: Cook Islands","unicode_version":"6.0"},{"emoji":"🇨🇱","aliases":["chile"],"tags":[],"category":"Flags","description":"flag: Chile","unicode_version":"6.0"},{"emoji":"🇨🇲","aliases":["cameroon"],"tags":[],"category":"Flags","description":"flag: Cameroon","unicode_version":"6.0"},{"emoji":"🇨🇳","aliases":["cn"],"tags":["china"],"category":"Flags","description":"flag: China","unicode_version":"6.0"},{"emoji":"🇨🇴","aliases":["colombia"],"tags":[],"category":"Flags","description":"flag: Colombia","unicode_version":"6.0"},{"emoji":"🇨🇵","aliases":["clipperton_island"],"tags":[],"category":"Flags","description":"flag: Clipperton Island","unicode_version":"11.0"},{"emoji":"🇨🇷","aliases":["costa_rica"],"tags":[],"category":"Flags","description":"flag: Costa Rica","unicode_version":"6.0"},{"emoji":"🇨🇺","aliases":["cuba"],"tags":[],"category":"Flags","description":"flag: Cuba","unicode_version":"6.0"},{"emoji":"🇨🇻","aliases":["cape_verde"],"tags":[],"category":"Flags","description":"flag: Cape Verde","unicode_version":"6.0"},{"emoji":"🇨🇼","aliases":["curacao"],"tags":[],"category":"Flags","description":"flag: Curaçao","unicode_version":"6.0"},{"emoji":"🇨🇽","aliases":["christmas_island"],"tags":[],"category":"Flags","description":"flag: Christmas Island","unicode_version":"6.0"},{"emoji":"🇨🇾","aliases":["cyprus"],"tags":[],"category":"Flags","description":"flag: Cyprus","unicode_version":"6.0"},{"emoji":"🇨🇿","aliases":["czech_republic"],"tags":[],"category":"Flags","description":"flag: Czechia","unicode_version":"6.0"},{"emoji":"🇩🇪","aliases":["de"],"tags":["flag","germany"],"category":"Flags","description":"flag: Germany","unicode_version":"6.0"},{"emoji":"🇩🇬","aliases":["diego_garcia"],"tags":[],"category":"Flags","description":"flag: Diego Garcia","unicode_version":"11.0"},{"emoji":"🇩🇯","aliases":["djibouti"],"tags":[],"category":"Flags","description":"flag: Djibouti","unicode_version":"6.0"},{"emoji":"🇩🇰","aliases":["denmark"],"tags":[],"category":"Flags","description":"flag: Denmark","unicode_version":"6.0"},{"emoji":"🇩🇲","aliases":["dominica"],"tags":[],"category":"Flags","description":"flag: Dominica","unicode_version":"6.0"},{"emoji":"🇩🇴","aliases":["dominican_republic"],"tags":[],"category":"Flags","description":"flag: Dominican Republic","unicode_version":"6.0"},{"emoji":"🇩🇿","aliases":["algeria"],"tags":[],"category":"Flags","description":"flag: Algeria","unicode_version":"6.0"},{"emoji":"🇪🇦","aliases":["ceuta_melilla"],"tags":[],"category":"Flags","description":"flag: Ceuta & Melilla","unicode_version":"11.0"},{"emoji":"🇪🇨","aliases":["ecuador"],"tags":[],"category":"Flags","description":"flag: Ecuador","unicode_version":"6.0"},{"emoji":"🇪🇪","aliases":["estonia"],"tags":[],"category":"Flags","description":"flag: Estonia","unicode_version":"6.0"},{"emoji":"🇪🇬","aliases":["egypt"],"tags":[],"category":"Flags","description":"flag: Egypt","unicode_version":"6.0"},{"emoji":"🇪🇭","aliases":["western_sahara"],"tags":[],"category":"Flags","description":"flag: Western Sahara","unicode_version":"6.0"},{"emoji":"🇪🇷","aliases":["eritrea"],"tags":[],"category":"Flags","description":"flag: Eritrea","unicode_version":"6.0"},{"emoji":"🇪🇸","aliases":["es"],"tags":["spain"],"category":"Flags","description":"flag: Spain","unicode_version":"6.0"},{"emoji":"🇪🇹","aliases":["ethiopia"],"tags":[],"category":"Flags","description":"flag: Ethiopia","unicode_version":"6.0"},{"emoji":"🇪🇺","aliases":["eu","european_union"],"tags":[],"category":"Flags","description":"flag: European Union","unicode_version":"6.0"},{"emoji":"🇫🇮","aliases":["finland"],"tags":[],"category":"Flags","description":"flag: Finland","unicode_version":"6.0"},{"emoji":"🇫🇯","aliases":["fiji"],"tags":[],"category":"Flags","description":"flag: Fiji","unicode_version":"6.0"},{"emoji":"🇫🇰","aliases":["falkland_islands"],"tags":[],"category":"Flags","description":"flag: Falkland Islands","unicode_version":"6.0"},{"emoji":"🇫🇲","aliases":["micronesia"],"tags":[],"category":"Flags","description":"flag: Micronesia","unicode_version":"6.0"},{"emoji":"🇫🇴","aliases":["faroe_islands"],"tags":[],"category":"Flags","description":"flag: Faroe Islands","unicode_version":"6.0"},{"emoji":"🇫🇷","aliases":["fr"],"tags":["france","french"],"category":"Flags","description":"flag: France","unicode_version":"6.0"},{"emoji":"🇬🇦","aliases":["gabon"],"tags":[],"category":"Flags","description":"flag: Gabon","unicode_version":"6.0"},{"emoji":"🇬🇧","aliases":["gb","uk"],"tags":["flag","british"],"category":"Flags","description":"flag: United Kingdom","unicode_version":"6.0"},{"emoji":"🇬🇩","aliases":["grenada"],"tags":[],"category":"Flags","description":"flag: Grenada","unicode_version":"6.0"},{"emoji":"🇬🇪","aliases":["georgia"],"tags":[],"category":"Flags","description":"flag: Georgia","unicode_version":"6.0"},{"emoji":"🇬🇫","aliases":["french_guiana"],"tags":[],"category":"Flags","description":"flag: French Guiana","unicode_version":"6.0"},{"emoji":"🇬🇬","aliases":["guernsey"],"tags":[],"category":"Flags","description":"flag: Guernsey","unicode_version":"6.0"},{"emoji":"🇬🇭","aliases":["ghana"],"tags":[],"category":"Flags","description":"flag: Ghana","unicode_version":"6.0"},{"emoji":"🇬🇮","aliases":["gibraltar"],"tags":[],"category":"Flags","description":"flag: Gibraltar","unicode_version":"6.0"},{"emoji":"🇬🇱","aliases":["greenland"],"tags":[],"category":"Flags","description":"flag: Greenland","unicode_version":"6.0"},{"emoji":"🇬🇲","aliases":["gambia"],"tags":[],"category":"Flags","description":"flag: Gambia","unicode_version":"6.0"},{"emoji":"🇬🇳","aliases":["guinea"],"tags":[],"category":"Flags","description":"flag: Guinea","unicode_version":"6.0"},{"emoji":"🇬🇵","aliases":["guadeloupe"],"tags":[],"category":"Flags","description":"flag: Guadeloupe","unicode_version":"6.0"},{"emoji":"🇬🇶","aliases":["equatorial_guinea"],"tags":[],"category":"Flags","description":"flag: Equatorial Guinea","unicode_version":"6.0"},{"emoji":"🇬🇷","aliases":["greece"],"tags":[],"category":"Flags","description":"flag: Greece","unicode_version":"6.0"},{"emoji":"🇬🇸","aliases":["south_georgia_south_sandwich_islands"],"tags":[],"category":"Flags","description":"flag: South Georgia & South Sandwich Islands","unicode_version":"6.0"},{"emoji":"🇬🇹","aliases":["guatemala"],"tags":[],"category":"Flags","description":"flag: Guatemala","unicode_version":"6.0"},{"emoji":"🇬🇺","aliases":["guam"],"tags":[],"category":"Flags","description":"flag: Guam","unicode_version":"6.0"},{"emoji":"🇬🇼","aliases":["guinea_bissau"],"tags":[],"category":"Flags","description":"flag: Guinea-Bissau","unicode_version":"6.0"},{"emoji":"🇬🇾","aliases":["guyana"],"tags":[],"category":"Flags","description":"flag: Guyana","unicode_version":"6.0"},{"emoji":"🇭🇰","aliases":["hong_kong"],"tags":[],"category":"Flags","description":"flag: Hong Kong SAR China","unicode_version":"6.0"},{"emoji":"🇭🇲","aliases":["heard_mcdonald_islands"],"tags":[],"category":"Flags","description":"flag: Heard & McDonald Islands","unicode_version":"11.0"},{"emoji":"🇭🇳","aliases":["honduras"],"tags":[],"category":"Flags","description":"flag: Honduras","unicode_version":"6.0"},{"emoji":"🇭🇷","aliases":["croatia"],"tags":[],"category":"Flags","description":"flag: Croatia","unicode_version":"6.0"},{"emoji":"🇭🇹","aliases":["haiti"],"tags":[],"category":"Flags","description":"flag: Haiti","unicode_version":"6.0"},{"emoji":"🇭🇺","aliases":["hungary"],"tags":[],"category":"Flags","description":"flag: Hungary","unicode_version":"6.0"},{"emoji":"🇮🇨","aliases":["canary_islands"],"tags":[],"category":"Flags","description":"flag: Canary Islands","unicode_version":"6.0"},{"emoji":"🇮🇩","aliases":["indonesia"],"tags":[],"category":"Flags","description":"flag: Indonesia","unicode_version":"6.0"},{"emoji":"🇮🇪","aliases":["ireland"],"tags":[],"category":"Flags","description":"flag: Ireland","unicode_version":"6.0"},{"emoji":"🇮🇱","aliases":["israel"],"tags":[],"category":"Flags","description":"flag: Israel","unicode_version":"6.0"},{"emoji":"🇮🇲","aliases":["isle_of_man"],"tags":[],"category":"Flags","description":"flag: Isle of Man","unicode_version":"6.0"},{"emoji":"🇮🇳","aliases":["india"],"tags":[],"category":"Flags","description":"flag: India","unicode_version":"6.0"},{"emoji":"🇮🇴","aliases":["british_indian_ocean_territory"],"tags":[],"category":"Flags","description":"flag: British Indian Ocean Territory","unicode_version":"6.0"},{"emoji":"🇮🇶","aliases":["iraq"],"tags":[],"category":"Flags","description":"flag: Iraq","unicode_version":"6.0"},{"emoji":"🇮🇷","aliases":["iran"],"tags":[],"category":"Flags","description":"flag: Iran","unicode_version":"6.0"},{"emoji":"🇮🇸","aliases":["iceland"],"tags":[],"category":"Flags","description":"flag: Iceland","unicode_version":"6.0"},{"emoji":"🇮🇹","aliases":["it"],"tags":["italy"],"category":"Flags","description":"flag: Italy","unicode_version":"6.0"},{"emoji":"🇯🇪","aliases":["jersey"],"tags":[],"category":"Flags","description":"flag: Jersey","unicode_version":"6.0"},{"emoji":"🇯🇲","aliases":["jamaica"],"tags":[],"category":"Flags","description":"flag: Jamaica","unicode_version":"6.0"},{"emoji":"🇯🇴","aliases":["jordan"],"tags":[],"category":"Flags","description":"flag: Jordan","unicode_version":"6.0"},{"emoji":"🇯🇵","aliases":["jp"],"tags":["japan"],"category":"Flags","description":"flag: Japan","unicode_version":"6.0"},{"emoji":"🇰🇪","aliases":["kenya"],"tags":[],"category":"Flags","description":"flag: Kenya","unicode_version":"6.0"},{"emoji":"🇰🇬","aliases":["kyrgyzstan"],"tags":[],"category":"Flags","description":"flag: Kyrgyzstan","unicode_version":"6.0"},{"emoji":"🇰🇭","aliases":["cambodia"],"tags":[],"category":"Flags","description":"flag: Cambodia","unicode_version":"6.0"},{"emoji":"🇰🇮","aliases":["kiribati"],"tags":[],"category":"Flags","description":"flag: Kiribati","unicode_version":"6.0"},{"emoji":"🇰🇲","aliases":["comoros"],"tags":[],"category":"Flags","description":"flag: Comoros","unicode_version":"6.0"},{"emoji":"🇰🇳","aliases":["st_kitts_nevis"],"tags":[],"category":"Flags","description":"flag: St. Kitts & Nevis","unicode_version":"6.0"},{"emoji":"🇰🇵","aliases":["north_korea"],"tags":[],"category":"Flags","description":"flag: North Korea","unicode_version":"6.0"},{"emoji":"🇰🇷","aliases":["kr"],"tags":["korea"],"category":"Flags","description":"flag: South Korea","unicode_version":"6.0"},{"emoji":"🇰🇼","aliases":["kuwait"],"tags":[],"category":"Flags","description":"flag: Kuwait","unicode_version":"6.0"},{"emoji":"🇰🇾","aliases":["cayman_islands"],"tags":[],"category":"Flags","description":"flag: Cayman Islands","unicode_version":"6.0"},{"emoji":"🇰🇿","aliases":["kazakhstan"],"tags":[],"category":"Flags","description":"flag: Kazakhstan","unicode_version":"6.0"},{"emoji":"🇱🇦","aliases":["laos"],"tags":[],"category":"Flags","description":"flag: Laos","unicode_version":"6.0"},{"emoji":"🇱🇧","aliases":["lebanon"],"tags":[],"category":"Flags","description":"flag: Lebanon","unicode_version":"6.0"},{"emoji":"🇱🇨","aliases":["st_lucia"],"tags":[],"category":"Flags","description":"flag: St. Lucia","unicode_version":"6.0"},{"emoji":"🇱🇮","aliases":["liechtenstein"],"tags":[],"category":"Flags","description":"flag: Liechtenstein","unicode_version":"6.0"},{"emoji":"🇱🇰","aliases":["sri_lanka"],"tags":[],"category":"Flags","description":"flag: Sri Lanka","unicode_version":"6.0"},{"emoji":"🇱🇷","aliases":["liberia"],"tags":[],"category":"Flags","description":"flag: Liberia","unicode_version":"6.0"},{"emoji":"🇱🇸","aliases":["lesotho"],"tags":[],"category":"Flags","description":"flag: Lesotho","unicode_version":"6.0"},{"emoji":"🇱🇹","aliases":["lithuania"],"tags":[],"category":"Flags","description":"flag: Lithuania","unicode_version":"6.0"},{"emoji":"🇱🇺","aliases":["luxembourg"],"tags":[],"category":"Flags","description":"flag: Luxembourg","unicode_version":"6.0"},{"emoji":"🇱🇻","aliases":["latvia"],"tags":[],"category":"Flags","description":"flag: Latvia","unicode_version":"6.0"},{"emoji":"🇱🇾","aliases":["libya"],"tags":[],"category":"Flags","description":"flag: Libya","unicode_version":"6.0"},{"emoji":"🇲🇦","aliases":["morocco"],"tags":[],"category":"Flags","description":"flag: Morocco","unicode_version":"6.0"},{"emoji":"🇲🇨","aliases":["monaco"],"tags":[],"category":"Flags","description":"flag: Monaco","unicode_version":"6.0"},{"emoji":"🇲🇩","aliases":["moldova"],"tags":[],"category":"Flags","description":"flag: Moldova","unicode_version":"6.0"},{"emoji":"🇲🇪","aliases":["montenegro"],"tags":[],"category":"Flags","description":"flag: Montenegro","unicode_version":"6.0"},{"emoji":"🇲🇫","aliases":["st_martin"],"tags":[],"category":"Flags","description":"flag: St. Martin","unicode_version":"11.0"},{"emoji":"🇲🇬","aliases":["madagascar"],"tags":[],"category":"Flags","description":"flag: Madagascar","unicode_version":"6.0"},{"emoji":"🇲🇭","aliases":["marshall_islands"],"tags":[],"category":"Flags","description":"flag: Marshall Islands","unicode_version":"6.0"},{"emoji":"🇲🇰","aliases":["macedonia"],"tags":[],"category":"Flags","description":"flag: North Macedonia","unicode_version":"6.0"},{"emoji":"🇲🇱","aliases":["mali"],"tags":[],"category":"Flags","description":"flag: Mali","unicode_version":"6.0"},{"emoji":"🇲🇲","aliases":["myanmar"],"tags":["burma"],"category":"Flags","description":"flag: Myanmar (Burma)","unicode_version":"6.0"},{"emoji":"🇲🇳","aliases":["mongolia"],"tags":[],"category":"Flags","description":"flag: Mongolia","unicode_version":"6.0"},{"emoji":"🇲🇴","aliases":["macau"],"tags":[],"category":"Flags","description":"flag: Macao SAR China","unicode_version":"6.0"},{"emoji":"🇲🇵","aliases":["northern_mariana_islands"],"tags":[],"category":"Flags","description":"flag: Northern Mariana Islands","unicode_version":"6.0"},{"emoji":"🇲🇶","aliases":["martinique"],"tags":[],"category":"Flags","description":"flag: Martinique","unicode_version":"6.0"},{"emoji":"🇲🇷","aliases":["mauritania"],"tags":[],"category":"Flags","description":"flag: Mauritania","unicode_version":"6.0"},{"emoji":"🇲🇸","aliases":["montserrat"],"tags":[],"category":"Flags","description":"flag: Montserrat","unicode_version":"6.0"},{"emoji":"🇲🇹","aliases":["malta"],"tags":[],"category":"Flags","description":"flag: Malta","unicode_version":"6.0"},{"emoji":"🇲🇺","aliases":["mauritius"],"tags":[],"category":"Flags","description":"flag: Mauritius","unicode_version":"6.0"},{"emoji":"🇲🇻","aliases":["maldives"],"tags":[],"category":"Flags","description":"flag: Maldives","unicode_version":"6.0"},{"emoji":"🇲🇼","aliases":["malawi"],"tags":[],"category":"Flags","description":"flag: Malawi","unicode_version":"6.0"},{"emoji":"🇲🇽","aliases":["mexico"],"tags":[],"category":"Flags","description":"flag: Mexico","unicode_version":"6.0"},{"emoji":"🇲🇾","aliases":["malaysia"],"tags":[],"category":"Flags","description":"flag: Malaysia","unicode_version":"6.0"},{"emoji":"🇲🇿","aliases":["mozambique"],"tags":[],"category":"Flags","description":"flag: Mozambique","unicode_version":"6.0"},{"emoji":"🇳🇦","aliases":["namibia"],"tags":[],"category":"Flags","description":"flag: Namibia","unicode_version":"6.0"},{"emoji":"🇳🇨","aliases":["new_caledonia"],"tags":[],"category":"Flags","description":"flag: New Caledonia","unicode_version":"6.0"},{"emoji":"🇳🇪","aliases":["niger"],"tags":[],"category":"Flags","description":"flag: Niger","unicode_version":"6.0"},{"emoji":"🇳🇫","aliases":["norfolk_island"],"tags":[],"category":"Flags","description":"flag: Norfolk Island","unicode_version":"6.0"},{"emoji":"🇳🇬","aliases":["nigeria"],"tags":[],"category":"Flags","description":"flag: Nigeria","unicode_version":"6.0"},{"emoji":"🇳🇮","aliases":["nicaragua"],"tags":[],"category":"Flags","description":"flag: Nicaragua","unicode_version":"6.0"},{"emoji":"🇳🇱","aliases":["netherlands"],"tags":[],"category":"Flags","description":"flag: Netherlands","unicode_version":"6.0"},{"emoji":"🇳🇴","aliases":["norway"],"tags":[],"category":"Flags","description":"flag: Norway","unicode_version":"6.0"},{"emoji":"🇳🇵","aliases":["nepal"],"tags":[],"category":"Flags","description":"flag: Nepal","unicode_version":"6.0"},{"emoji":"🇳🇷","aliases":["nauru"],"tags":[],"category":"Flags","description":"flag: Nauru","unicode_version":"6.0"},{"emoji":"🇳🇺","aliases":["niue"],"tags":[],"category":"Flags","description":"flag: Niue","unicode_version":"6.0"},{"emoji":"🇳🇿","aliases":["new_zealand"],"tags":[],"category":"Flags","description":"flag: New Zealand","unicode_version":"6.0"},{"emoji":"🇴🇲","aliases":["oman"],"tags":[],"category":"Flags","description":"flag: Oman","unicode_version":"6.0"},{"emoji":"🇵🇦","aliases":["panama"],"tags":[],"category":"Flags","description":"flag: Panama","unicode_version":"6.0"},{"emoji":"🇵🇪","aliases":["peru"],"tags":[],"category":"Flags","description":"flag: Peru","unicode_version":"6.0"},{"emoji":"🇵🇫","aliases":["french_polynesia"],"tags":[],"category":"Flags","description":"flag: French Polynesia","unicode_version":"6.0"},{"emoji":"🇵🇬","aliases":["papua_new_guinea"],"tags":[],"category":"Flags","description":"flag: Papua New Guinea","unicode_version":"6.0"},{"emoji":"🇵🇭","aliases":["philippines"],"tags":[],"category":"Flags","description":"flag: Philippines","unicode_version":"6.0"},{"emoji":"🇵🇰","aliases":["pakistan"],"tags":[],"category":"Flags","description":"flag: Pakistan","unicode_version":"6.0"},{"emoji":"🇵🇱","aliases":["poland"],"tags":[],"category":"Flags","description":"flag: Poland","unicode_version":"6.0"},{"emoji":"🇵🇲","aliases":["st_pierre_miquelon"],"tags":[],"category":"Flags","description":"flag: St. Pierre & Miquelon","unicode_version":"6.0"},{"emoji":"🇵🇳","aliases":["pitcairn_islands"],"tags":[],"category":"Flags","description":"flag: Pitcairn Islands","unicode_version":"6.0"},{"emoji":"🇵🇷","aliases":["puerto_rico"],"tags":[],"category":"Flags","description":"flag: Puerto Rico","unicode_version":"6.0"},{"emoji":"🇵🇸","aliases":["palestinian_territories"],"tags":[],"category":"Flags","description":"flag: Palestinian Territories","unicode_version":"6.0"},{"emoji":"🇵🇹","aliases":["portugal"],"tags":[],"category":"Flags","description":"flag: Portugal","unicode_version":"6.0"},{"emoji":"🇵🇼","aliases":["palau"],"tags":[],"category":"Flags","description":"flag: Palau","unicode_version":"6.0"},{"emoji":"🇵🇾","aliases":["paraguay"],"tags":[],"category":"Flags","description":"flag: Paraguay","unicode_version":"6.0"},{"emoji":"🇶🇦","aliases":["qatar"],"tags":[],"category":"Flags","description":"flag: Qatar","unicode_version":"6.0"},{"emoji":"🇷🇪","aliases":["reunion"],"tags":[],"category":"Flags","description":"flag: Réunion","unicode_version":"6.0"},{"emoji":"🇷🇴","aliases":["romania"],"tags":[],"category":"Flags","description":"flag: Romania","unicode_version":"6.0"},{"emoji":"🇷🇸","aliases":["serbia"],"tags":[],"category":"Flags","description":"flag: Serbia","unicode_version":"6.0"},{"emoji":"🇷🇺","aliases":["ru"],"tags":["russia"],"category":"Flags","description":"flag: Russia","unicode_version":"6.0"},{"emoji":"🇷🇼","aliases":["rwanda"],"tags":[],"category":"Flags","description":"flag: Rwanda","unicode_version":"6.0"},{"emoji":"🇸🇦","aliases":["saudi_arabia"],"tags":[],"category":"Flags","description":"flag: Saudi Arabia","unicode_version":"6.0"},{"emoji":"🇸🇧","aliases":["solomon_islands"],"tags":[],"category":"Flags","description":"flag: Solomon Islands","unicode_version":"6.0"},{"emoji":"🇸🇨","aliases":["seychelles"],"tags":[],"category":"Flags","description":"flag: Seychelles","unicode_version":"6.0"},{"emoji":"🇸🇩","aliases":["sudan"],"tags":[],"category":"Flags","description":"flag: Sudan","unicode_version":"6.0"},{"emoji":"🇸🇪","aliases":["sweden"],"tags":[],"category":"Flags","description":"flag: Sweden","unicode_version":"6.0"},{"emoji":"🇸🇬","aliases":["singapore"],"tags":[],"category":"Flags","description":"flag: Singapore","unicode_version":"6.0"},{"emoji":"🇸🇭","aliases":["st_helena"],"tags":[],"category":"Flags","description":"flag: St. Helena","unicode_version":"6.0"},{"emoji":"🇸🇮","aliases":["slovenia"],"tags":[],"category":"Flags","description":"flag: Slovenia","unicode_version":"6.0"},{"emoji":"🇸🇯","aliases":["svalbard_jan_mayen"],"tags":[],"category":"Flags","description":"flag: Svalbard & Jan Mayen","unicode_version":"11.0"},{"emoji":"🇸🇰","aliases":["slovakia"],"tags":[],"category":"Flags","description":"flag: Slovakia","unicode_version":"6.0"},{"emoji":"🇸🇱","aliases":["sierra_leone"],"tags":[],"category":"Flags","description":"flag: Sierra Leone","unicode_version":"6.0"},{"emoji":"🇸🇲","aliases":["san_marino"],"tags":[],"category":"Flags","description":"flag: San Marino","unicode_version":"6.0"},{"emoji":"🇸🇳","aliases":["senegal"],"tags":[],"category":"Flags","description":"flag: Senegal","unicode_version":"6.0"},{"emoji":"🇸🇴","aliases":["somalia"],"tags":[],"category":"Flags","description":"flag: Somalia","unicode_version":"6.0"},{"emoji":"🇸🇷","aliases":["suriname"],"tags":[],"category":"Flags","description":"flag: Suriname","unicode_version":"6.0"},{"emoji":"🇸🇸","aliases":["south_sudan"],"tags":[],"category":"Flags","description":"flag: South Sudan","unicode_version":"6.0"},{"emoji":"🇸🇹","aliases":["sao_tome_principe"],"tags":[],"category":"Flags","description":"flag: São Tomé & Príncipe","unicode_version":"6.0"},{"emoji":"🇸🇻","aliases":["el_salvador"],"tags":[],"category":"Flags","description":"flag: El Salvador","unicode_version":"6.0"},{"emoji":"🇸🇽","aliases":["sint_maarten"],"tags":[],"category":"Flags","description":"flag: Sint Maarten","unicode_version":"6.0"},{"emoji":"🇸🇾","aliases":["syria"],"tags":[],"category":"Flags","description":"flag: Syria","unicode_version":"6.0"},{"emoji":"🇸🇿","aliases":["swaziland"],"tags":[],"category":"Flags","description":"flag: Eswatini","unicode_version":"6.0"},{"emoji":"🇹🇦","aliases":["tristan_da_cunha"],"tags":[],"category":"Flags","description":"flag: Tristan da Cunha","unicode_version":"11.0"},{"emoji":"🇹🇨","aliases":["turks_caicos_islands"],"tags":[],"category":"Flags","description":"flag: Turks & Caicos Islands","unicode_version":"6.0"},{"emoji":"🇹🇩","aliases":["chad"],"tags":[],"category":"Flags","description":"flag: Chad","unicode_version":"6.0"},{"emoji":"🇹🇫","aliases":["french_southern_territories"],"tags":[],"category":"Flags","description":"flag: French Southern Territories","unicode_version":"6.0"},{"emoji":"🇹🇬","aliases":["togo"],"tags":[],"category":"Flags","description":"flag: Togo","unicode_version":"6.0"},{"emoji":"🇹🇭","aliases":["thailand"],"tags":[],"category":"Flags","description":"flag: Thailand","unicode_version":"6.0"},{"emoji":"🇹🇯","aliases":["tajikistan"],"tags":[],"category":"Flags","description":"flag: Tajikistan","unicode_version":"6.0"},{"emoji":"🇹🇰","aliases":["tokelau"],"tags":[],"category":"Flags","description":"flag: Tokelau","unicode_version":"6.0"},{"emoji":"🇹🇱","aliases":["timor_leste"],"tags":[],"category":"Flags","description":"flag: Timor-Leste","unicode_version":"6.0"},{"emoji":"🇹🇲","aliases":["turkmenistan"],"tags":[],"category":"Flags","description":"flag: Turkmenistan","unicode_version":"6.0"},{"emoji":"🇹🇳","aliases":["tunisia"],"tags":[],"category":"Flags","description":"flag: Tunisia","unicode_version":"6.0"},{"emoji":"🇹🇴","aliases":["tonga"],"tags":[],"category":"Flags","description":"flag: Tonga","unicode_version":"6.0"},{"emoji":"🇹🇷","aliases":["tr"],"tags":["turkey"],"category":"Flags","description":"flag: Turkey","unicode_version":"8.0"},{"emoji":"🇹🇹","aliases":["trinidad_tobago"],"tags":[],"category":"Flags","description":"flag: Trinidad & Tobago","unicode_version":"6.0"},{"emoji":"🇹🇻","aliases":["tuvalu"],"tags":[],"category":"Flags","description":"flag: Tuvalu","unicode_version":"6.0"},{"emoji":"🇹🇼","aliases":["taiwan"],"tags":[],"category":"Flags","description":"flag: Taiwan","unicode_version":"6.0"},{"emoji":"🇹🇿","aliases":["tanzania"],"tags":[],"category":"Flags","description":"flag: Tanzania","unicode_version":"6.0"},{"emoji":"🇺🇦","aliases":["ukraine"],"tags":[],"category":"Flags","description":"flag: Ukraine","unicode_version":"6.0"},{"emoji":"🇺🇬","aliases":["uganda"],"tags":[],"category":"Flags","description":"flag: Uganda","unicode_version":"6.0"},{"emoji":"🇺🇲","aliases":["us_outlying_islands"],"tags":[],"category":"Flags","description":"flag: U.S. Outlying Islands","unicode_version":"11.0"},{"emoji":"🇺🇳","aliases":["united_nations"],"tags":[],"category":"Flags","description":"flag: United Nations","unicode_version":"11.0"},{"emoji":"🇺🇸","aliases":["us"],"tags":["flag","united","america"],"category":"Flags","description":"flag: United States","unicode_version":"6.0"},{"emoji":"🇺🇾","aliases":["uruguay"],"tags":[],"category":"Flags","description":"flag: Uruguay","unicode_version":"6.0"},{"emoji":"🇺🇿","aliases":["uzbekistan"],"tags":[],"category":"Flags","description":"flag: Uzbekistan","unicode_version":"6.0"},{"emoji":"🇻🇦","aliases":["vatican_city"],"tags":[],"category":"Flags","description":"flag: Vatican City","unicode_version":"6.0"},{"emoji":"🇻🇨","aliases":["st_vincent_grenadines"],"tags":[],"category":"Flags","description":"flag: St. Vincent & Grenadines","unicode_version":"6.0"},{"emoji":"🇻🇪","aliases":["venezuela"],"tags":[],"category":"Flags","description":"flag: Venezuela","unicode_version":"6.0"},{"emoji":"🇻🇬","aliases":["british_virgin_islands"],"tags":[],"category":"Flags","description":"flag: British Virgin Islands","unicode_version":"6.0"},{"emoji":"🇻🇮","aliases":["us_virgin_islands"],"tags":[],"category":"Flags","description":"flag: U.S. Virgin Islands","unicode_version":"6.0"},{"emoji":"🇻🇳","aliases":["vietnam"],"tags":[],"category":"Flags","description":"flag: Vietnam","unicode_version":"6.0"},{"emoji":"🇻🇺","aliases":["vanuatu"],"tags":[],"category":"Flags","description":"flag: Vanuatu","unicode_version":"6.0"},{"emoji":"🇼🇫","aliases":["wallis_futuna"],"tags":[],"category":"Flags","description":"flag: Wallis & Futuna","unicode_version":"6.0"},{"emoji":"🇼🇸","aliases":["samoa"],"tags":[],"category":"Flags","description":"flag: Samoa","unicode_version":"6.0"},{"emoji":"🇽🇰","aliases":["kosovo"],"tags":[],"category":"Flags","description":"flag: Kosovo","unicode_version":"6.0"},{"emoji":"🇾🇪","aliases":["yemen"],"tags":[],"category":"Flags","description":"flag: Yemen","unicode_version":"6.0"},{"emoji":"🇾🇹","aliases":["mayotte"],"tags":[],"category":"Flags","description":"flag: Mayotte","unicode_version":"6.0"},{"emoji":"🇿🇦","aliases":["south_africa"],"tags":[],"category":"Flags","description":"flag: South Africa","unicode_version":"6.0"},{"emoji":"🇿🇲","aliases":["zambia"],"tags":[],"category":"Flags","description":"flag: Zambia","unicode_version":"6.0"},{"emoji":"🇿🇼","aliases":["zimbabwe"],"tags":[],"category":"Flags","description":"flag: Zimbabwe","unicode_version":"6.0"},{"emoji":"🏴󠁧󠁢󠁥󠁮󠁧󠁿","aliases":["england"],"tags":[],"category":"Flags","description":"flag: England","unicode_version":"11.0"},{"emoji":"🏴󠁧󠁢󠁳󠁣󠁴󠁿","aliases":["scotland"],"tags":[],"category":"Flags","description":"flag: Scotland","unicode_version":"11.0"},{"emoji":"🏴󠁧󠁢󠁷󠁬󠁳󠁿","aliases":["wales"],"tags":[],"category":"Flags","description":"flag: Wales","unicode_version":"11.0"}] +export const rawEmojis = [ + { + emoji: "😀", + aliases: ["grinning"], + tags: ["smile", "happy"], + category: "Smileys & Emotion", + description: "grinning face", + unicode_version: "6.1", + }, + { + emoji: "😃", + aliases: ["smiley"], + tags: ["happy", "joy", "haha"], + category: "Smileys & Emotion", + description: "grinning face with big eyes", + unicode_version: "6.0", + }, + { + emoji: "😄", + aliases: ["smile"], + tags: ["happy", "joy", "laugh", "pleased"], + category: "Smileys & Emotion", + description: "grinning face with smiling eyes", + unicode_version: "6.0", + }, + { + emoji: "😁", + aliases: ["grin"], + tags: [], + category: "Smileys & Emotion", + description: "beaming face with smiling eyes", + unicode_version: "6.0", + }, + { + emoji: "😆", + aliases: ["laughing", "satisfied"], + tags: ["happy", "haha"], + category: "Smileys & Emotion", + description: "grinning squinting face", + unicode_version: "6.0", + }, + { + emoji: "😅", + aliases: ["sweat_smile"], + tags: ["hot"], + category: "Smileys & Emotion", + description: "grinning face with sweat", + unicode_version: "6.0", + }, + { + emoji: "🤣", + aliases: ["rofl"], + tags: ["lol", "laughing"], + category: "Smileys & Emotion", + description: "rolling on the floor laughing", + unicode_version: "9.0", + }, + { + emoji: "😂", + aliases: ["joy"], + tags: ["tears"], + category: "Smileys & Emotion", + description: "face with tears of joy", + unicode_version: "6.0", + }, + { + emoji: "🙂", + aliases: ["slightly_smiling_face"], + tags: [], + category: "Smileys & Emotion", + description: "slightly smiling face", + unicode_version: "7.0", + }, + { + emoji: "🙃", + aliases: ["upside_down_face"], + tags: [], + category: "Smileys & Emotion", + description: "upside-down face", + unicode_version: "8.0", + }, + { + emoji: "😉", + aliases: ["wink"], + tags: ["flirt"], + category: "Smileys & Emotion", + description: "winking face", + unicode_version: "6.0", + }, + { + emoji: "😊", + aliases: ["blush"], + tags: ["proud"], + category: "Smileys & Emotion", + description: "smiling face with smiling eyes", + unicode_version: "6.0", + }, + { + emoji: "😇", + aliases: ["innocent"], + tags: ["angel"], + category: "Smileys & Emotion", + description: "smiling face with halo", + unicode_version: "6.0", + }, + { + emoji: "🥰", + aliases: ["smiling_face_with_three_hearts"], + tags: ["love"], + category: "Smileys & Emotion", + description: "smiling face with hearts", + unicode_version: "11.0", + }, + { + emoji: "😍", + aliases: ["heart_eyes"], + tags: ["love", "crush"], + category: "Smileys & Emotion", + description: "smiling face with heart-eyes", + unicode_version: "6.0", + }, + { + emoji: "🤩", + aliases: ["star_struck"], + tags: ["eyes"], + category: "Smileys & Emotion", + description: "star-struck", + unicode_version: "11.0", + }, + { + emoji: "😘", + aliases: ["kissing_heart"], + tags: ["flirt"], + category: "Smileys & Emotion", + description: "face blowing a kiss", + unicode_version: "6.0", + }, + { + emoji: "😗", + aliases: ["kissing"], + tags: [], + category: "Smileys & Emotion", + description: "kissing face", + unicode_version: "6.1", + }, + { + emoji: "☺️", + aliases: ["relaxed"], + tags: ["blush", "pleased"], + category: "Smileys & Emotion", + description: "smiling face", + unicode_version: "", + }, + { + emoji: "😚", + aliases: ["kissing_closed_eyes"], + tags: [], + category: "Smileys & Emotion", + description: "kissing face with closed eyes", + unicode_version: "6.0", + }, + { + emoji: "😙", + aliases: ["kissing_smiling_eyes"], + tags: [], + category: "Smileys & Emotion", + description: "kissing face with smiling eyes", + unicode_version: "6.1", + }, + { + emoji: "🥲", + aliases: ["smiling_face_with_tear"], + tags: [], + category: "Smileys & Emotion", + description: "smiling face with tear", + unicode_version: "13.0", + }, + { + emoji: "😋", + aliases: ["yum"], + tags: ["tongue", "lick"], + category: "Smileys & Emotion", + description: "face savoring food", + unicode_version: "6.0", + }, + { + emoji: "😛", + aliases: ["stuck_out_tongue"], + tags: [], + category: "Smileys & Emotion", + description: "face with tongue", + unicode_version: "6.1", + }, + { + emoji: "😜", + aliases: ["stuck_out_tongue_winking_eye"], + tags: ["prank", "silly"], + category: "Smileys & Emotion", + description: "winking face with tongue", + unicode_version: "6.0", + }, + { + emoji: "🤪", + aliases: ["zany_face"], + tags: ["goofy", "wacky"], + category: "Smileys & Emotion", + description: "zany face", + unicode_version: "11.0", + }, + { + emoji: "😝", + aliases: ["stuck_out_tongue_closed_eyes"], + tags: ["prank"], + category: "Smileys & Emotion", + description: "squinting face with tongue", + unicode_version: "6.0", + }, + { + emoji: "🤑", + aliases: ["money_mouth_face"], + tags: ["rich"], + category: "Smileys & Emotion", + description: "money-mouth face", + unicode_version: "8.0", + }, + { + emoji: "🤗", + aliases: ["hugs"], + tags: [], + category: "Smileys & Emotion", + description: "hugging face", + unicode_version: "8.0", + }, + { + emoji: "🤭", + aliases: ["hand_over_mouth"], + tags: ["quiet", "whoops"], + category: "Smileys & Emotion", + description: "face with hand over mouth", + unicode_version: "11.0", + }, + { + emoji: "🤫", + aliases: ["shushing_face"], + tags: ["silence", "quiet"], + category: "Smileys & Emotion", + description: "shushing face", + unicode_version: "11.0", + }, + { + emoji: "🤔", + aliases: ["thinking"], + tags: [], + category: "Smileys & Emotion", + description: "thinking face", + unicode_version: "8.0", + }, + { + emoji: "🤐", + aliases: ["zipper_mouth_face"], + tags: ["silence", "hush"], + category: "Smileys & Emotion", + description: "zipper-mouth face", + unicode_version: "8.0", + }, + { + emoji: "🤨", + aliases: ["raised_eyebrow"], + tags: ["suspicious"], + category: "Smileys & Emotion", + description: "face with raised eyebrow", + unicode_version: "11.0", + }, + { + emoji: "😐", + aliases: ["neutral_face"], + tags: ["meh"], + category: "Smileys & Emotion", + description: "neutral face", + unicode_version: "6.0", + }, + { + emoji: "😑", + aliases: ["expressionless"], + tags: [], + category: "Smileys & Emotion", + description: "expressionless face", + unicode_version: "6.1", + }, + { + emoji: "😶", + aliases: ["no_mouth"], + tags: ["mute", "silence"], + category: "Smileys & Emotion", + description: "face without mouth", + unicode_version: "6.0", + }, + { + emoji: "😶‍🌫️", + aliases: ["face_in_clouds"], + tags: [], + category: "Smileys & Emotion", + description: "face in clouds", + unicode_version: "13.1", + }, + { + emoji: "😏", + aliases: ["smirk"], + tags: ["smug"], + category: "Smileys & Emotion", + description: "smirking face", + unicode_version: "6.0", + }, + { + emoji: "😒", + aliases: ["unamused"], + tags: ["meh"], + category: "Smileys & Emotion", + description: "unamused face", + unicode_version: "6.0", + }, + { + emoji: "🙄", + aliases: ["roll_eyes"], + tags: [], + category: "Smileys & Emotion", + description: "face with rolling eyes", + unicode_version: "8.0", + }, + { + emoji: "😬", + aliases: ["grimacing"], + tags: [], + category: "Smileys & Emotion", + description: "grimacing face", + unicode_version: "6.1", + }, + { + emoji: "😮‍💨", + aliases: ["face_exhaling"], + tags: [], + category: "Smileys & Emotion", + description: "face exhaling", + unicode_version: "13.1", + }, + { + emoji: "🤥", + aliases: ["lying_face"], + tags: ["liar"], + category: "Smileys & Emotion", + description: "lying face", + unicode_version: "9.0", + }, + { + emoji: "😌", + aliases: ["relieved"], + tags: ["whew"], + category: "Smileys & Emotion", + description: "relieved face", + unicode_version: "6.0", + }, + { + emoji: "😔", + aliases: ["pensive"], + tags: [], + category: "Smileys & Emotion", + description: "pensive face", + unicode_version: "6.0", + }, + { + emoji: "😪", + aliases: ["sleepy"], + tags: ["tired"], + category: "Smileys & Emotion", + description: "sleepy face", + unicode_version: "6.0", + }, + { + emoji: "🤤", + aliases: ["drooling_face"], + tags: [], + category: "Smileys & Emotion", + description: "drooling face", + unicode_version: "9.0", + }, + { + emoji: "😴", + aliases: ["sleeping"], + tags: ["zzz"], + category: "Smileys & Emotion", + description: "sleeping face", + unicode_version: "6.1", + }, + { + emoji: "😷", + aliases: ["mask"], + tags: ["sick", "ill"], + category: "Smileys & Emotion", + description: "face with medical mask", + unicode_version: "6.0", + }, + { + emoji: "🤒", + aliases: ["face_with_thermometer"], + tags: ["sick"], + category: "Smileys & Emotion", + description: "face with thermometer", + unicode_version: "8.0", + }, + { + emoji: "🤕", + aliases: ["face_with_head_bandage"], + tags: ["hurt"], + category: "Smileys & Emotion", + description: "face with head-bandage", + unicode_version: "8.0", + }, + { + emoji: "🤢", + aliases: ["nauseated_face"], + tags: ["sick", "barf", "disgusted"], + category: "Smileys & Emotion", + description: "nauseated face", + unicode_version: "9.0", + }, + { + emoji: "🤮", + aliases: ["vomiting_face"], + tags: ["barf", "sick"], + category: "Smileys & Emotion", + description: "face vomiting", + unicode_version: "11.0", + }, + { + emoji: "🤧", + aliases: ["sneezing_face"], + tags: ["achoo", "sick"], + category: "Smileys & Emotion", + description: "sneezing face", + unicode_version: "9.0", + }, + { + emoji: "🥵", + aliases: ["hot_face"], + tags: ["heat", "sweating"], + category: "Smileys & Emotion", + description: "hot face", + unicode_version: "11.0", + }, + { + emoji: "🥶", + aliases: ["cold_face"], + tags: ["freezing", "ice"], + category: "Smileys & Emotion", + description: "cold face", + unicode_version: "11.0", + }, + { + emoji: "🥴", + aliases: ["woozy_face"], + tags: ["groggy"], + category: "Smileys & Emotion", + description: "woozy face", + unicode_version: "11.0", + }, + { + emoji: "😵", + aliases: ["dizzy_face"], + tags: [], + category: "Smileys & Emotion", + description: "knocked-out face", + unicode_version: "6.0", + }, + { + emoji: "😵‍💫", + aliases: ["face_with_spiral_eyes"], + tags: [], + category: "Smileys & Emotion", + description: "face with spiral eyes", + unicode_version: "13.1", + }, + { + emoji: "🤯", + aliases: ["exploding_head"], + tags: ["mind", "blown"], + category: "Smileys & Emotion", + description: "exploding head", + unicode_version: "11.0", + }, + { + emoji: "🤠", + aliases: ["cowboy_hat_face"], + tags: [], + category: "Smileys & Emotion", + description: "cowboy hat face", + unicode_version: "9.0", + }, + { + emoji: "🥳", + aliases: ["partying_face"], + tags: ["celebration", "birthday"], + category: "Smileys & Emotion", + description: "partying face", + unicode_version: "11.0", + }, + { + emoji: "🥸", + aliases: ["disguised_face"], + tags: [], + category: "Smileys & Emotion", + description: "disguised face", + unicode_version: "13.0", + }, + { + emoji: "😎", + aliases: ["sunglasses"], + tags: ["cool"], + category: "Smileys & Emotion", + description: "smiling face with sunglasses", + unicode_version: "6.0", + }, + { + emoji: "🤓", + aliases: ["nerd_face"], + tags: ["geek", "glasses"], + category: "Smileys & Emotion", + description: "nerd face", + unicode_version: "8.0", + }, + { + emoji: "🧐", + aliases: ["monocle_face"], + tags: [], + category: "Smileys & Emotion", + description: "face with monocle", + unicode_version: "11.0", + }, + { + emoji: "😕", + aliases: ["confused"], + tags: [], + category: "Smileys & Emotion", + description: "confused face", + unicode_version: "6.1", + }, + { + emoji: "😟", + aliases: ["worried"], + tags: ["nervous"], + category: "Smileys & Emotion", + description: "worried face", + unicode_version: "6.1", + }, + { + emoji: "🙁", + aliases: ["slightly_frowning_face"], + tags: [], + category: "Smileys & Emotion", + description: "slightly frowning face", + unicode_version: "7.0", + }, + { + emoji: "☹️", + aliases: ["frowning_face"], + tags: [], + category: "Smileys & Emotion", + description: "frowning face", + unicode_version: "", + }, + { + emoji: "😮", + aliases: ["open_mouth"], + tags: ["surprise", "impressed", "wow"], + category: "Smileys & Emotion", + description: "face with open mouth", + unicode_version: "6.1", + }, + { + emoji: "😯", + aliases: ["hushed"], + tags: ["silence", "speechless"], + category: "Smileys & Emotion", + description: "hushed face", + unicode_version: "6.1", + }, + { + emoji: "😲", + aliases: ["astonished"], + tags: ["amazed", "gasp"], + category: "Smileys & Emotion", + description: "astonished face", + unicode_version: "6.0", + }, + { + emoji: "😳", + aliases: ["flushed"], + tags: [], + category: "Smileys & Emotion", + description: "flushed face", + unicode_version: "6.0", + }, + { + emoji: "🥺", + aliases: ["pleading_face"], + tags: ["puppy", "eyes"], + category: "Smileys & Emotion", + description: "pleading face", + unicode_version: "11.0", + }, + { + emoji: "😦", + aliases: ["frowning"], + tags: [], + category: "Smileys & Emotion", + description: "frowning face with open mouth", + unicode_version: "6.1", + }, + { + emoji: "😧", + aliases: ["anguished"], + tags: ["stunned"], + category: "Smileys & Emotion", + description: "anguished face", + unicode_version: "6.1", + }, + { + emoji: "😨", + aliases: ["fearful"], + tags: ["scared", "shocked", "oops"], + category: "Smileys & Emotion", + description: "fearful face", + unicode_version: "6.0", + }, + { + emoji: "😰", + aliases: ["cold_sweat"], + tags: ["nervous"], + category: "Smileys & Emotion", + description: "anxious face with sweat", + unicode_version: "6.0", + }, + { + emoji: "😥", + aliases: ["disappointed_relieved"], + tags: ["phew", "sweat", "nervous"], + category: "Smileys & Emotion", + description: "sad but relieved face", + unicode_version: "6.0", + }, + { + emoji: "😢", + aliases: ["cry"], + tags: ["sad", "tear"], + category: "Smileys & Emotion", + description: "crying face", + unicode_version: "6.0", + }, + { + emoji: "😭", + aliases: ["sob"], + tags: ["sad", "cry", "bawling"], + category: "Smileys & Emotion", + description: "loudly crying face", + unicode_version: "6.0", + }, + { + emoji: "😱", + aliases: ["scream"], + tags: ["horror", "shocked"], + category: "Smileys & Emotion", + description: "face screaming in fear", + unicode_version: "6.0", + }, + { + emoji: "😖", + aliases: ["confounded"], + tags: [], + category: "Smileys & Emotion", + description: "confounded face", + unicode_version: "6.0", + }, + { + emoji: "😣", + aliases: ["persevere"], + tags: ["struggling"], + category: "Smileys & Emotion", + description: "persevering face", + unicode_version: "6.0", + }, + { + emoji: "😞", + aliases: ["disappointed"], + tags: ["sad"], + category: "Smileys & Emotion", + description: "disappointed face", + unicode_version: "6.0", + }, + { + emoji: "😓", + aliases: ["sweat"], + tags: [], + category: "Smileys & Emotion", + description: "downcast face with sweat", + unicode_version: "6.0", + }, + { + emoji: "😩", + aliases: ["weary"], + tags: ["tired"], + category: "Smileys & Emotion", + description: "weary face", + unicode_version: "6.0", + }, + { + emoji: "😫", + aliases: ["tired_face"], + tags: ["upset", "whine"], + category: "Smileys & Emotion", + description: "tired face", + unicode_version: "6.0", + }, + { + emoji: "🥱", + aliases: ["yawning_face"], + tags: [], + category: "Smileys & Emotion", + description: "yawning face", + unicode_version: "12.0", + }, + { + emoji: "😤", + aliases: ["triumph"], + tags: ["smug"], + category: "Smileys & Emotion", + description: "face with steam from nose", + unicode_version: "6.0", + }, + { + emoji: "😡", + aliases: ["rage", "pout"], + tags: ["angry"], + category: "Smileys & Emotion", + description: "pouting face", + unicode_version: "6.0", + }, + { + emoji: "😠", + aliases: ["angry"], + tags: ["mad", "annoyed"], + category: "Smileys & Emotion", + description: "angry face", + unicode_version: "6.0", + }, + { + emoji: "🤬", + aliases: ["cursing_face"], + tags: ["foul"], + category: "Smileys & Emotion", + description: "face with symbols on mouth", + unicode_version: "11.0", + }, + { + emoji: "😈", + aliases: ["smiling_imp"], + tags: ["devil", "evil", "horns"], + category: "Smileys & Emotion", + description: "smiling face with horns", + unicode_version: "6.0", + }, + { + emoji: "👿", + aliases: ["imp"], + tags: ["angry", "devil", "evil", "horns"], + category: "Smileys & Emotion", + description: "angry face with horns", + unicode_version: "6.0", + }, + { + emoji: "💀", + aliases: ["skull"], + tags: ["dead", "danger", "poison"], + category: "Smileys & Emotion", + description: "skull", + unicode_version: "6.0", + }, + { + emoji: "☠️", + aliases: ["skull_and_crossbones"], + tags: ["danger", "pirate"], + category: "Smileys & Emotion", + description: "skull and crossbones", + unicode_version: "", + }, + { + emoji: "💩", + aliases: ["hankey", "poop", "shit"], + tags: ["crap"], + category: "Smileys & Emotion", + description: "pile of poo", + unicode_version: "6.0", + }, + { + emoji: "🤡", + aliases: ["clown_face"], + tags: [], + category: "Smileys & Emotion", + description: "clown face", + unicode_version: "9.0", + }, + { + emoji: "👹", + aliases: ["japanese_ogre"], + tags: ["monster"], + category: "Smileys & Emotion", + description: "ogre", + unicode_version: "6.0", + }, + { + emoji: "👺", + aliases: ["japanese_goblin"], + tags: [], + category: "Smileys & Emotion", + description: "goblin", + unicode_version: "6.0", + }, + { + emoji: "👻", + aliases: ["ghost"], + tags: ["halloween"], + category: "Smileys & Emotion", + description: "ghost", + unicode_version: "6.0", + }, + { + emoji: "👽", + aliases: ["alien"], + tags: ["ufo"], + category: "Smileys & Emotion", + description: "alien", + unicode_version: "6.0", + }, + { + emoji: "👾", + aliases: ["space_invader"], + tags: ["game", "retro"], + category: "Smileys & Emotion", + description: "alien monster", + unicode_version: "6.0", + }, + { + emoji: "🤖", + aliases: ["robot"], + tags: [], + category: "Smileys & Emotion", + description: "robot", + unicode_version: "8.0", + }, + { + emoji: "😺", + aliases: ["smiley_cat"], + tags: [], + category: "Smileys & Emotion", + description: "grinning cat", + unicode_version: "6.0", + }, + { + emoji: "😸", + aliases: ["smile_cat"], + tags: [], + category: "Smileys & Emotion", + description: "grinning cat with smiling eyes", + unicode_version: "6.0", + }, + { + emoji: "😹", + aliases: ["joy_cat"], + tags: [], + category: "Smileys & Emotion", + description: "cat with tears of joy", + unicode_version: "6.0", + }, + { + emoji: "😻", + aliases: ["heart_eyes_cat"], + tags: [], + category: "Smileys & Emotion", + description: "smiling cat with heart-eyes", + unicode_version: "6.0", + }, + { + emoji: "😼", + aliases: ["smirk_cat"], + tags: [], + category: "Smileys & Emotion", + description: "cat with wry smile", + unicode_version: "6.0", + }, + { + emoji: "😽", + aliases: ["kissing_cat"], + tags: [], + category: "Smileys & Emotion", + description: "kissing cat", + unicode_version: "6.0", + }, + { + emoji: "🙀", + aliases: ["scream_cat"], + tags: ["horror"], + category: "Smileys & Emotion", + description: "weary cat", + unicode_version: "6.0", + }, + { + emoji: "😿", + aliases: ["crying_cat_face"], + tags: ["sad", "tear"], + category: "Smileys & Emotion", + description: "crying cat", + unicode_version: "6.0", + }, + { + emoji: "😾", + aliases: ["pouting_cat"], + tags: [], + category: "Smileys & Emotion", + description: "pouting cat", + unicode_version: "6.0", + }, + { + emoji: "🙈", + aliases: ["see_no_evil"], + tags: ["monkey", "blind", "ignore"], + category: "Smileys & Emotion", + description: "see-no-evil monkey", + unicode_version: "6.0", + }, + { + emoji: "🙉", + aliases: ["hear_no_evil"], + tags: ["monkey", "deaf"], + category: "Smileys & Emotion", + description: "hear-no-evil monkey", + unicode_version: "6.0", + }, + { + emoji: "🙊", + aliases: ["speak_no_evil"], + tags: ["monkey", "mute", "hush"], + category: "Smileys & Emotion", + description: "speak-no-evil monkey", + unicode_version: "6.0", + }, + { + emoji: "💋", + aliases: ["kiss"], + tags: ["lipstick"], + category: "Smileys & Emotion", + description: "kiss mark", + unicode_version: "6.0", + }, + { + emoji: "💌", + aliases: ["love_letter"], + tags: ["email", "envelope"], + category: "Smileys & Emotion", + description: "love letter", + unicode_version: "6.0", + }, + { + emoji: "💘", + aliases: ["cupid"], + tags: ["love", "heart"], + category: "Smileys & Emotion", + description: "heart with arrow", + unicode_version: "6.0", + }, + { + emoji: "💝", + aliases: ["gift_heart"], + tags: ["chocolates"], + category: "Smileys & Emotion", + description: "heart with ribbon", + unicode_version: "6.0", + }, + { + emoji: "💖", + aliases: ["sparkling_heart"], + tags: [], + category: "Smileys & Emotion", + description: "sparkling heart", + unicode_version: "6.0", + }, + { + emoji: "💗", + aliases: ["heartpulse"], + tags: [], + category: "Smileys & Emotion", + description: "growing heart", + unicode_version: "6.0", + }, + { + emoji: "💓", + aliases: ["heartbeat"], + tags: [], + category: "Smileys & Emotion", + description: "beating heart", + unicode_version: "6.0", + }, + { + emoji: "💞", + aliases: ["revolving_hearts"], + tags: [], + category: "Smileys & Emotion", + description: "revolving hearts", + unicode_version: "6.0", + }, + { + emoji: "💕", + aliases: ["two_hearts"], + tags: [], + category: "Smileys & Emotion", + description: "two hearts", + unicode_version: "6.0", + }, + { + emoji: "💟", + aliases: ["heart_decoration"], + tags: [], + category: "Smileys & Emotion", + description: "heart decoration", + unicode_version: "6.0", + }, + { + emoji: "❣️", + aliases: ["heavy_heart_exclamation"], + tags: [], + category: "Smileys & Emotion", + description: "heart exclamation", + unicode_version: "", + }, + { + emoji: "💔", + aliases: ["broken_heart"], + tags: [], + category: "Smileys & Emotion", + description: "broken heart", + unicode_version: "6.0", + }, + { + emoji: "❤️‍🔥", + aliases: ["heart_on_fire"], + tags: [], + category: "Smileys & Emotion", + description: "heart on fire", + unicode_version: "13.1", + }, + { + emoji: "❤️‍🩹", + aliases: ["mending_heart"], + tags: [], + category: "Smileys & Emotion", + description: "mending heart", + unicode_version: "13.1", + }, + { + emoji: "❤️", + aliases: ["heart"], + tags: ["love"], + category: "Smileys & Emotion", + description: "red heart", + unicode_version: "", + }, + { + emoji: "🧡", + aliases: ["orange_heart"], + tags: [], + category: "Smileys & Emotion", + description: "orange heart", + unicode_version: "11.0", + }, + { + emoji: "💛", + aliases: ["yellow_heart"], + tags: [], + category: "Smileys & Emotion", + description: "yellow heart", + unicode_version: "6.0", + }, + { + emoji: "💚", + aliases: ["green_heart"], + tags: [], + category: "Smileys & Emotion", + description: "green heart", + unicode_version: "6.0", + }, + { + emoji: "💙", + aliases: ["blue_heart"], + tags: [], + category: "Smileys & Emotion", + description: "blue heart", + unicode_version: "6.0", + }, + { + emoji: "💜", + aliases: ["purple_heart"], + tags: [], + category: "Smileys & Emotion", + description: "purple heart", + unicode_version: "6.0", + }, + { + emoji: "🤎", + aliases: ["brown_heart"], + tags: [], + category: "Smileys & Emotion", + description: "brown heart", + unicode_version: "12.0", + }, + { + emoji: "🖤", + aliases: ["black_heart"], + tags: [], + category: "Smileys & Emotion", + description: "black heart", + unicode_version: "9.0", + }, + { + emoji: "🤍", + aliases: ["white_heart"], + tags: [], + category: "Smileys & Emotion", + description: "white heart", + unicode_version: "12.0", + }, + { + emoji: "💯", + aliases: ["100"], + tags: ["score", "perfect"], + category: "Smileys & Emotion", + description: "hundred points", + unicode_version: "6.0", + }, + { + emoji: "💢", + aliases: ["anger"], + tags: ["angry"], + category: "Smileys & Emotion", + description: "anger symbol", + unicode_version: "6.0", + }, + { + emoji: "💥", + aliases: ["boom", "collision"], + tags: ["explode"], + category: "Smileys & Emotion", + description: "collision", + unicode_version: "6.0", + }, + { + emoji: "💫", + aliases: ["dizzy"], + tags: ["star"], + category: "Smileys & Emotion", + description: "dizzy", + unicode_version: "6.0", + }, + { + emoji: "💦", + aliases: ["sweat_drops"], + tags: ["water", "workout"], + category: "Smileys & Emotion", + description: "sweat droplets", + unicode_version: "6.0", + }, + { + emoji: "💨", + aliases: ["dash"], + tags: ["wind", "blow", "fast"], + category: "Smileys & Emotion", + description: "dashing away", + unicode_version: "6.0", + }, + { + emoji: "🕳️", + aliases: ["hole"], + tags: [], + category: "Smileys & Emotion", + description: "hole", + unicode_version: "7.0", + }, + { + emoji: "💣", + aliases: ["bomb"], + tags: ["boom"], + category: "Smileys & Emotion", + description: "bomb", + unicode_version: "6.0", + }, + { + emoji: "💬", + aliases: ["speech_balloon"], + tags: ["comment"], + category: "Smileys & Emotion", + description: "speech balloon", + unicode_version: "6.0", + }, + { + emoji: "👁️‍🗨️", + aliases: ["eye_speech_bubble"], + tags: [], + category: "Smileys & Emotion", + description: "eye in speech bubble", + unicode_version: "11.0", + }, + { + emoji: "🗨️", + aliases: ["left_speech_bubble"], + tags: [], + category: "Smileys & Emotion", + description: "left speech bubble", + unicode_version: "11.0", + }, + { + emoji: "🗯️", + aliases: ["right_anger_bubble"], + tags: [], + category: "Smileys & Emotion", + description: "right anger bubble", + unicode_version: "7.0", + }, + { + emoji: "💭", + aliases: ["thought_balloon"], + tags: ["thinking"], + category: "Smileys & Emotion", + description: "thought balloon", + unicode_version: "6.0", + }, + { + emoji: "💤", + aliases: ["zzz"], + tags: ["sleeping"], + category: "Smileys & Emotion", + description: "zzz", + unicode_version: "6.0", + }, + { + emoji: "👋", + aliases: ["wave"], + tags: ["goodbye"], + category: "People & Body", + description: "waving hand", + unicode_version: "6.0", + }, + { + emoji: "🤚", + aliases: ["raised_back_of_hand"], + tags: [], + category: "People & Body", + description: "raised back of hand", + unicode_version: "9.0", + }, + { + emoji: "🖐️", + aliases: ["raised_hand_with_fingers_splayed"], + tags: [], + category: "People & Body", + description: "hand with fingers splayed", + unicode_version: "7.0", + }, + { + emoji: "✋", + aliases: ["hand", "raised_hand"], + tags: ["highfive", "stop"], + category: "People & Body", + description: "raised hand", + unicode_version: "6.0", + }, + { + emoji: "🖖", + aliases: ["vulcan_salute"], + tags: ["prosper", "spock"], + category: "People & Body", + description: "vulcan salute", + unicode_version: "7.0", + }, + { + emoji: "👌", + aliases: ["ok_hand"], + tags: [], + category: "People & Body", + description: "OK hand", + unicode_version: "6.0", + }, + { + emoji: "🤌", + aliases: ["pinched_fingers"], + tags: [], + category: "People & Body", + description: "pinched fingers", + unicode_version: "13.0", + }, + { + emoji: "🤏", + aliases: ["pinching_hand"], + tags: [], + category: "People & Body", + description: "pinching hand", + unicode_version: "12.0", + }, + { + emoji: "✌️", + aliases: ["v"], + tags: ["victory", "peace"], + category: "People & Body", + description: "victory hand", + unicode_version: "", + }, + { + emoji: "🤞", + aliases: ["crossed_fingers"], + tags: ["luck", "hopeful"], + category: "People & Body", + description: "crossed fingers", + unicode_version: "9.0", + }, + { + emoji: "🤟", + aliases: ["love_you_gesture"], + tags: [], + category: "People & Body", + description: "love-you gesture", + unicode_version: "11.0", + }, + { + emoji: "🤘", + aliases: ["metal"], + tags: [], + category: "People & Body", + description: "sign of the horns", + unicode_version: "8.0", + }, + { + emoji: "🤙", + aliases: ["call_me_hand"], + tags: [], + category: "People & Body", + description: "call me hand", + unicode_version: "9.0", + }, + { + emoji: "👈", + aliases: ["point_left"], + tags: [], + category: "People & Body", + description: "backhand index pointing left", + unicode_version: "6.0", + }, + { + emoji: "👉", + aliases: ["point_right"], + tags: [], + category: "People & Body", + description: "backhand index pointing right", + unicode_version: "6.0", + }, + { + emoji: "👆", + aliases: ["point_up_2"], + tags: [], + category: "People & Body", + description: "backhand index pointing up", + unicode_version: "6.0", + }, + { + emoji: "🖕", + aliases: ["middle_finger", "fu"], + tags: [], + category: "People & Body", + description: "middle finger", + unicode_version: "7.0", + }, + { + emoji: "👇", + aliases: ["point_down"], + tags: [], + category: "People & Body", + description: "backhand index pointing down", + unicode_version: "6.0", + }, + { + emoji: "☝️", + aliases: ["point_up"], + tags: [], + category: "People & Body", + description: "index pointing up", + unicode_version: "", + }, + { + emoji: "👍", + aliases: ["+1", "thumbsup"], + tags: ["approve", "ok"], + category: "People & Body", + description: "thumbs up", + unicode_version: "6.0", + }, + { + emoji: "👎", + aliases: ["-1", "thumbsdown"], + tags: ["disapprove", "bury"], + category: "People & Body", + description: "thumbs down", + unicode_version: "6.0", + }, + { + emoji: "✊", + aliases: ["fist_raised", "fist"], + tags: ["power"], + category: "People & Body", + description: "raised fist", + unicode_version: "6.0", + }, + { + emoji: "👊", + aliases: ["fist_oncoming", "facepunch", "punch"], + tags: ["attack"], + category: "People & Body", + description: "oncoming fist", + unicode_version: "6.0", + }, + { + emoji: "🤛", + aliases: ["fist_left"], + tags: [], + category: "People & Body", + description: "left-facing fist", + unicode_version: "9.0", + }, + { + emoji: "🤜", + aliases: ["fist_right"], + tags: [], + category: "People & Body", + description: "right-facing fist", + unicode_version: "9.0", + }, + { + emoji: "👏", + aliases: ["clap"], + tags: ["praise", "applause"], + category: "People & Body", + description: "clapping hands", + unicode_version: "6.0", + }, + { + emoji: "🙌", + aliases: ["raised_hands"], + tags: ["hooray"], + category: "People & Body", + description: "raising hands", + unicode_version: "6.0", + }, + { + emoji: "👐", + aliases: ["open_hands"], + tags: [], + category: "People & Body", + description: "open hands", + unicode_version: "6.0", + }, + { + emoji: "🤲", + aliases: ["palms_up_together"], + tags: [], + category: "People & Body", + description: "palms up together", + unicode_version: "11.0", + }, + { + emoji: "🤝", + aliases: ["handshake"], + tags: ["deal"], + category: "People & Body", + description: "handshake", + unicode_version: "9.0", + }, + { + emoji: "🙏", + aliases: ["pray"], + tags: ["please", "hope", "wish"], + category: "People & Body", + description: "folded hands", + unicode_version: "6.0", + }, + { + emoji: "✍️", + aliases: ["writing_hand"], + tags: [], + category: "People & Body", + description: "writing hand", + unicode_version: "", + }, + { + emoji: "💅", + aliases: ["nail_care"], + tags: ["beauty", "manicure"], + category: "People & Body", + description: "nail polish", + unicode_version: "6.0", + }, + { + emoji: "🤳", + aliases: ["selfie"], + tags: [], + category: "People & Body", + description: "selfie", + unicode_version: "9.0", + }, + { + emoji: "💪", + aliases: ["muscle"], + tags: ["flex", "bicep", "strong", "workout"], + category: "People & Body", + description: "flexed biceps", + unicode_version: "6.0", + }, + { + emoji: "🦾", + aliases: ["mechanical_arm"], + tags: [], + category: "People & Body", + description: "mechanical arm", + unicode_version: "12.0", + }, + { + emoji: "🦿", + aliases: ["mechanical_leg"], + tags: [], + category: "People & Body", + description: "mechanical leg", + unicode_version: "12.0", + }, + { + emoji: "🦵", + aliases: ["leg"], + tags: [], + category: "People & Body", + description: "leg", + unicode_version: "11.0", + }, + { + emoji: "🦶", + aliases: ["foot"], + tags: [], + category: "People & Body", + description: "foot", + unicode_version: "11.0", + }, + { + emoji: "👂", + aliases: ["ear"], + tags: ["hear", "sound", "listen"], + category: "People & Body", + description: "ear", + unicode_version: "6.0", + }, + { + emoji: "🦻", + aliases: ["ear_with_hearing_aid"], + tags: [], + category: "People & Body", + description: "ear with hearing aid", + unicode_version: "12.0", + }, + { + emoji: "👃", + aliases: ["nose"], + tags: ["smell"], + category: "People & Body", + description: "nose", + unicode_version: "6.0", + }, + { + emoji: "🧠", + aliases: ["brain"], + tags: [], + category: "People & Body", + description: "brain", + unicode_version: "11.0", + }, + { + emoji: "🫀", + aliases: ["anatomical_heart"], + tags: [], + category: "People & Body", + description: "anatomical heart", + unicode_version: "13.0", + }, + { + emoji: "🫁", + aliases: ["lungs"], + tags: [], + category: "People & Body", + description: "lungs", + unicode_version: "13.0", + }, + { + emoji: "🦷", + aliases: ["tooth"], + tags: [], + category: "People & Body", + description: "tooth", + unicode_version: "11.0", + }, + { + emoji: "🦴", + aliases: ["bone"], + tags: [], + category: "People & Body", + description: "bone", + unicode_version: "11.0", + }, + { + emoji: "👀", + aliases: ["eyes"], + tags: ["look", "see", "watch"], + category: "People & Body", + description: "eyes", + unicode_version: "6.0", + }, + { + emoji: "👁️", + aliases: ["eye"], + tags: [], + category: "People & Body", + description: "eye", + unicode_version: "7.0", + }, + { + emoji: "👅", + aliases: ["tongue"], + tags: ["taste"], + category: "People & Body", + description: "tongue", + unicode_version: "6.0", + }, + { + emoji: "👄", + aliases: ["lips"], + tags: ["kiss"], + category: "People & Body", + description: "mouth", + unicode_version: "6.0", + }, + { + emoji: "👶", + aliases: ["baby"], + tags: ["child", "newborn"], + category: "People & Body", + description: "baby", + unicode_version: "6.0", + }, + { + emoji: "🧒", + aliases: ["child"], + tags: [], + category: "People & Body", + description: "child", + unicode_version: "11.0", + }, + { + emoji: "👦", + aliases: ["boy"], + tags: ["child"], + category: "People & Body", + description: "boy", + unicode_version: "6.0", + }, + { + emoji: "👧", + aliases: ["girl"], + tags: ["child"], + category: "People & Body", + description: "girl", + unicode_version: "6.0", + }, + { + emoji: "🧑", + aliases: ["adult"], + tags: [], + category: "People & Body", + description: "person", + unicode_version: "11.0", + }, + { + emoji: "👱", + aliases: ["blond_haired_person"], + tags: [], + category: "People & Body", + description: "person: blond hair", + unicode_version: "6.0", + }, + { + emoji: "👨", + aliases: ["man"], + tags: ["mustache", "father", "dad"], + category: "People & Body", + description: "man", + unicode_version: "6.0", + }, + { + emoji: "🧔", + aliases: ["bearded_person"], + tags: [], + category: "People & Body", + description: "person: beard", + unicode_version: "11.0", + }, + { + emoji: "🧔‍♂️", + aliases: ["man_beard"], + tags: [], + category: "People & Body", + description: "man: beard", + unicode_version: "13.1", + }, + { + emoji: "🧔‍♀️", + aliases: ["woman_beard"], + tags: [], + category: "People & Body", + description: "woman: beard", + unicode_version: "13.1", + }, + { + emoji: "👨‍🦰", + aliases: ["red_haired_man"], + tags: [], + category: "People & Body", + description: "man: red hair", + unicode_version: "11.0", + }, + { + emoji: "👨‍🦱", + aliases: ["curly_haired_man"], + tags: [], + category: "People & Body", + description: "man: curly hair", + unicode_version: "11.0", + }, + { + emoji: "👨‍🦳", + aliases: ["white_haired_man"], + tags: [], + category: "People & Body", + description: "man: white hair", + unicode_version: "11.0", + }, + { + emoji: "👨‍🦲", + aliases: ["bald_man"], + tags: [], + category: "People & Body", + description: "man: bald", + unicode_version: "11.0", + }, + { + emoji: "👩", + aliases: ["woman"], + tags: ["girls"], + category: "People & Body", + description: "woman", + unicode_version: "6.0", + }, + { + emoji: "👩‍🦰", + aliases: ["red_haired_woman"], + tags: [], + category: "People & Body", + description: "woman: red hair", + unicode_version: "11.0", + }, + { + emoji: "🧑‍🦰", + aliases: ["person_red_hair"], + tags: [], + category: "People & Body", + description: "person: red hair", + unicode_version: "12.1", + }, + { + emoji: "👩‍🦱", + aliases: ["curly_haired_woman"], + tags: [], + category: "People & Body", + description: "woman: curly hair", + unicode_version: "11.0", + }, + { + emoji: "🧑‍🦱", + aliases: ["person_curly_hair"], + tags: [], + category: "People & Body", + description: "person: curly hair", + unicode_version: "12.1", + }, + { + emoji: "👩‍🦳", + aliases: ["white_haired_woman"], + tags: [], + category: "People & Body", + description: "woman: white hair", + unicode_version: "11.0", + }, + { + emoji: "🧑‍🦳", + aliases: ["person_white_hair"], + tags: [], + category: "People & Body", + description: "person: white hair", + unicode_version: "12.1", + }, + { + emoji: "👩‍🦲", + aliases: ["bald_woman"], + tags: [], + category: "People & Body", + description: "woman: bald", + unicode_version: "11.0", + }, + { + emoji: "🧑‍🦲", + aliases: ["person_bald"], + tags: [], + category: "People & Body", + description: "person: bald", + unicode_version: "12.1", + }, + { + emoji: "👱‍♀️", + aliases: ["blond_haired_woman", "blonde_woman"], + tags: [], + category: "People & Body", + description: "woman: blond hair", + unicode_version: "6.0", + }, + { + emoji: "👱‍♂️", + aliases: ["blond_haired_man"], + tags: [], + category: "People & Body", + description: "man: blond hair", + unicode_version: "11.0", + }, + { + emoji: "🧓", + aliases: ["older_adult"], + tags: [], + category: "People & Body", + description: "older person", + unicode_version: "11.0", + }, + { + emoji: "👴", + aliases: ["older_man"], + tags: [], + category: "People & Body", + description: "old man", + unicode_version: "6.0", + }, + { + emoji: "👵", + aliases: ["older_woman"], + tags: [], + category: "People & Body", + description: "old woman", + unicode_version: "6.0", + }, + { + emoji: "🙍", + aliases: ["frowning_person"], + tags: [], + category: "People & Body", + description: "person frowning", + unicode_version: "6.0", + }, + { + emoji: "🙍‍♂️", + aliases: ["frowning_man"], + tags: [], + category: "People & Body", + description: "man frowning", + unicode_version: "6.0", + }, + { + emoji: "🙍‍♀️", + aliases: ["frowning_woman"], + tags: [], + category: "People & Body", + description: "woman frowning", + unicode_version: "11.0", + }, + { + emoji: "🙎", + aliases: ["pouting_face"], + tags: [], + category: "People & Body", + description: "person pouting", + unicode_version: "6.0", + }, + { + emoji: "🙎‍♂️", + aliases: ["pouting_man"], + tags: [], + category: "People & Body", + description: "man pouting", + unicode_version: "6.0", + }, + { + emoji: "🙎‍♀️", + aliases: ["pouting_woman"], + tags: [], + category: "People & Body", + description: "woman pouting", + unicode_version: "11.0", + }, + { + emoji: "🙅", + aliases: ["no_good"], + tags: ["stop", "halt", "denied"], + category: "People & Body", + description: "person gesturing NO", + unicode_version: "6.0", + }, + { + emoji: "🙅‍♂️", + aliases: ["no_good_man", "ng_man"], + tags: ["stop", "halt", "denied"], + category: "People & Body", + description: "man gesturing NO", + unicode_version: "6.0", + }, + { + emoji: "🙅‍♀️", + aliases: ["no_good_woman", "ng_woman"], + tags: ["stop", "halt", "denied"], + category: "People & Body", + description: "woman gesturing NO", + unicode_version: "11.0", + }, + { + emoji: "🙆", + aliases: ["ok_person"], + tags: [], + category: "People & Body", + description: "person gesturing OK", + unicode_version: "6.0", + }, + { + emoji: "🙆‍♂️", + aliases: ["ok_man"], + tags: [], + category: "People & Body", + description: "man gesturing OK", + unicode_version: "6.0", + }, + { + emoji: "🙆‍♀️", + aliases: ["ok_woman"], + tags: [], + category: "People & Body", + description: "woman gesturing OK", + unicode_version: "11.0", + }, + { + emoji: "💁", + aliases: ["tipping_hand_person", "information_desk_person"], + tags: [], + category: "People & Body", + description: "person tipping hand", + unicode_version: "6.0", + }, + { + emoji: "💁‍♂️", + aliases: ["tipping_hand_man", "sassy_man"], + tags: ["information"], + category: "People & Body", + description: "man tipping hand", + unicode_version: "6.0", + }, + { + emoji: "💁‍♀️", + aliases: ["tipping_hand_woman", "sassy_woman"], + tags: ["information"], + category: "People & Body", + description: "woman tipping hand", + unicode_version: "11.0", + }, + { + emoji: "🙋", + aliases: ["raising_hand"], + tags: [], + category: "People & Body", + description: "person raising hand", + unicode_version: "6.0", + }, + { + emoji: "🙋‍♂️", + aliases: ["raising_hand_man"], + tags: [], + category: "People & Body", + description: "man raising hand", + unicode_version: "6.0", + }, + { + emoji: "🙋‍♀️", + aliases: ["raising_hand_woman"], + tags: [], + category: "People & Body", + description: "woman raising hand", + unicode_version: "11.0", + }, + { + emoji: "🧏", + aliases: ["deaf_person"], + tags: [], + category: "People & Body", + description: "deaf person", + unicode_version: "12.0", + }, + { + emoji: "🧏‍♂️", + aliases: ["deaf_man"], + tags: [], + category: "People & Body", + description: "deaf man", + unicode_version: "12.0", + }, + { + emoji: "🧏‍♀️", + aliases: ["deaf_woman"], + tags: [], + category: "People & Body", + description: "deaf woman", + unicode_version: "12.0", + }, + { + emoji: "🙇", + aliases: ["bow"], + tags: ["respect", "thanks"], + category: "People & Body", + description: "person bowing", + unicode_version: "6.0", + }, + { + emoji: "🙇‍♂️", + aliases: ["bowing_man"], + tags: ["respect", "thanks"], + category: "People & Body", + description: "man bowing", + unicode_version: "11.0", + }, + { + emoji: "🙇‍♀️", + aliases: ["bowing_woman"], + tags: ["respect", "thanks"], + category: "People & Body", + description: "woman bowing", + unicode_version: "6.0", + }, + { + emoji: "🤦", + aliases: ["facepalm"], + tags: [], + category: "People & Body", + description: "person facepalming", + unicode_version: "11.0", + }, + { + emoji: "🤦‍♂️", + aliases: ["man_facepalming"], + tags: [], + category: "People & Body", + description: "man facepalming", + unicode_version: "9.0", + }, + { + emoji: "🤦‍♀️", + aliases: ["woman_facepalming"], + tags: [], + category: "People & Body", + description: "woman facepalming", + unicode_version: "9.0", + }, + { + emoji: "🤷", + aliases: ["shrug"], + tags: [], + category: "People & Body", + description: "person shrugging", + unicode_version: "11.0", + }, + { + emoji: "🤷‍♂️", + aliases: ["man_shrugging"], + tags: [], + category: "People & Body", + description: "man shrugging", + unicode_version: "9.0", + }, + { + emoji: "🤷‍♀️", + aliases: ["woman_shrugging"], + tags: [], + category: "People & Body", + description: "woman shrugging", + unicode_version: "9.0", + }, + { + emoji: "🧑‍⚕️", + aliases: ["health_worker"], + tags: [], + category: "People & Body", + description: "health worker", + unicode_version: "12.1", + }, + { + emoji: "👨‍⚕️", + aliases: ["man_health_worker"], + tags: ["doctor", "nurse"], + category: "People & Body", + description: "man health worker", + unicode_version: "", + }, + { + emoji: "👩‍⚕️", + aliases: ["woman_health_worker"], + tags: ["doctor", "nurse"], + category: "People & Body", + description: "woman health worker", + unicode_version: "", + }, + { + emoji: "🧑‍🎓", + aliases: ["student"], + tags: [], + category: "People & Body", + description: "student", + unicode_version: "12.1", + }, + { + emoji: "👨‍🎓", + aliases: ["man_student"], + tags: ["graduation"], + category: "People & Body", + description: "man student", + unicode_version: "", + }, + { + emoji: "👩‍🎓", + aliases: ["woman_student"], + tags: ["graduation"], + category: "People & Body", + description: "woman student", + unicode_version: "", + }, + { + emoji: "🧑‍🏫", + aliases: ["teacher"], + tags: [], + category: "People & Body", + description: "teacher", + unicode_version: "12.1", + }, + { + emoji: "👨‍🏫", + aliases: ["man_teacher"], + tags: ["school", "professor"], + category: "People & Body", + description: "man teacher", + unicode_version: "", + }, + { + emoji: "👩‍🏫", + aliases: ["woman_teacher"], + tags: ["school", "professor"], + category: "People & Body", + description: "woman teacher", + unicode_version: "", + }, + { + emoji: "🧑‍⚖️", + aliases: ["judge"], + tags: [], + category: "People & Body", + description: "judge", + unicode_version: "12.1", + }, + { + emoji: "👨‍⚖️", + aliases: ["man_judge"], + tags: ["justice"], + category: "People & Body", + description: "man judge", + unicode_version: "", + }, + { + emoji: "👩‍⚖️", + aliases: ["woman_judge"], + tags: ["justice"], + category: "People & Body", + description: "woman judge", + unicode_version: "", + }, + { + emoji: "🧑‍🌾", + aliases: ["farmer"], + tags: [], + category: "People & Body", + description: "farmer", + unicode_version: "12.1", + }, + { + emoji: "👨‍🌾", + aliases: ["man_farmer"], + tags: [], + category: "People & Body", + description: "man farmer", + unicode_version: "", + }, + { + emoji: "👩‍🌾", + aliases: ["woman_farmer"], + tags: [], + category: "People & Body", + description: "woman farmer", + unicode_version: "", + }, + { + emoji: "🧑‍🍳", + aliases: ["cook"], + tags: [], + category: "People & Body", + description: "cook", + unicode_version: "12.1", + }, + { + emoji: "👨‍🍳", + aliases: ["man_cook"], + tags: ["chef"], + category: "People & Body", + description: "man cook", + unicode_version: "", + }, + { + emoji: "👩‍🍳", + aliases: ["woman_cook"], + tags: ["chef"], + category: "People & Body", + description: "woman cook", + unicode_version: "", + }, + { + emoji: "🧑‍🔧", + aliases: ["mechanic"], + tags: [], + category: "People & Body", + description: "mechanic", + unicode_version: "12.1", + }, + { + emoji: "👨‍🔧", + aliases: ["man_mechanic"], + tags: [], + category: "People & Body", + description: "man mechanic", + unicode_version: "", + }, + { + emoji: "👩‍🔧", + aliases: ["woman_mechanic"], + tags: [], + category: "People & Body", + description: "woman mechanic", + unicode_version: "", + }, + { + emoji: "🧑‍🏭", + aliases: ["factory_worker"], + tags: [], + category: "People & Body", + description: "factory worker", + unicode_version: "12.1", + }, + { + emoji: "👨‍🏭", + aliases: ["man_factory_worker"], + tags: [], + category: "People & Body", + description: "man factory worker", + unicode_version: "", + }, + { + emoji: "👩‍🏭", + aliases: ["woman_factory_worker"], + tags: [], + category: "People & Body", + description: "woman factory worker", + unicode_version: "", + }, + { + emoji: "🧑‍💼", + aliases: ["office_worker"], + tags: [], + category: "People & Body", + description: "office worker", + unicode_version: "12.1", + }, + { + emoji: "👨‍💼", + aliases: ["man_office_worker"], + tags: ["business"], + category: "People & Body", + description: "man office worker", + unicode_version: "", + }, + { + emoji: "👩‍💼", + aliases: ["woman_office_worker"], + tags: ["business"], + category: "People & Body", + description: "woman office worker", + unicode_version: "", + }, + { + emoji: "🧑‍🔬", + aliases: ["scientist"], + tags: [], + category: "People & Body", + description: "scientist", + unicode_version: "12.1", + }, + { + emoji: "👨‍🔬", + aliases: ["man_scientist"], + tags: ["research"], + category: "People & Body", + description: "man scientist", + unicode_version: "", + }, + { + emoji: "👩‍🔬", + aliases: ["woman_scientist"], + tags: ["research"], + category: "People & Body", + description: "woman scientist", + unicode_version: "", + }, + { + emoji: "🧑‍💻", + aliases: ["technologist"], + tags: [], + category: "People & Body", + description: "technologist", + unicode_version: "12.1", + }, + { + emoji: "👨‍💻", + aliases: ["man_technologist"], + tags: ["coder"], + category: "People & Body", + description: "man technologist", + unicode_version: "", + }, + { + emoji: "👩‍💻", + aliases: ["woman_technologist"], + tags: ["coder"], + category: "People & Body", + description: "woman technologist", + unicode_version: "", + }, + { + emoji: "🧑‍🎤", + aliases: ["singer"], + tags: [], + category: "People & Body", + description: "singer", + unicode_version: "12.1", + }, + { + emoji: "👨‍🎤", + aliases: ["man_singer"], + tags: ["rockstar"], + category: "People & Body", + description: "man singer", + unicode_version: "", + }, + { + emoji: "👩‍🎤", + aliases: ["woman_singer"], + tags: ["rockstar"], + category: "People & Body", + description: "woman singer", + unicode_version: "", + }, + { + emoji: "🧑‍🎨", + aliases: ["artist"], + tags: [], + category: "People & Body", + description: "artist", + unicode_version: "12.1", + }, + { + emoji: "👨‍🎨", + aliases: ["man_artist"], + tags: ["painter"], + category: "People & Body", + description: "man artist", + unicode_version: "", + }, + { + emoji: "👩‍🎨", + aliases: ["woman_artist"], + tags: ["painter"], + category: "People & Body", + description: "woman artist", + unicode_version: "", + }, + { + emoji: "🧑‍✈️", + aliases: ["pilot"], + tags: [], + category: "People & Body", + description: "pilot", + unicode_version: "12.1", + }, + { + emoji: "👨‍✈️", + aliases: ["man_pilot"], + tags: [], + category: "People & Body", + description: "man pilot", + unicode_version: "", + }, + { + emoji: "👩‍✈️", + aliases: ["woman_pilot"], + tags: [], + category: "People & Body", + description: "woman pilot", + unicode_version: "", + }, + { + emoji: "🧑‍🚀", + aliases: ["astronaut"], + tags: [], + category: "People & Body", + description: "astronaut", + unicode_version: "12.1", + }, + { + emoji: "👨‍🚀", + aliases: ["man_astronaut"], + tags: ["space"], + category: "People & Body", + description: "man astronaut", + unicode_version: "", + }, + { + emoji: "👩‍🚀", + aliases: ["woman_astronaut"], + tags: ["space"], + category: "People & Body", + description: "woman astronaut", + unicode_version: "", + }, + { + emoji: "🧑‍🚒", + aliases: ["firefighter"], + tags: [], + category: "People & Body", + description: "firefighter", + unicode_version: "12.1", + }, + { + emoji: "👨‍🚒", + aliases: ["man_firefighter"], + tags: [], + category: "People & Body", + description: "man firefighter", + unicode_version: "", + }, + { + emoji: "👩‍🚒", + aliases: ["woman_firefighter"], + tags: [], + category: "People & Body", + description: "woman firefighter", + unicode_version: "", + }, + { + emoji: "👮", + aliases: ["police_officer", "cop"], + tags: ["law"], + category: "People & Body", + description: "police officer", + unicode_version: "6.0", + }, + { + emoji: "👮‍♂️", + aliases: ["policeman"], + tags: ["law", "cop"], + category: "People & Body", + description: "man police officer", + unicode_version: "11.0", + }, + { + emoji: "👮‍♀️", + aliases: ["policewoman"], + tags: ["law", "cop"], + category: "People & Body", + description: "woman police officer", + unicode_version: "6.0", + }, + { + emoji: "🕵️", + aliases: ["detective"], + tags: ["sleuth"], + category: "People & Body", + description: "detective", + unicode_version: "7.0", + }, + { + emoji: "🕵️‍♂️", + aliases: ["male_detective"], + tags: ["sleuth"], + category: "People & Body", + description: "man detective", + unicode_version: "11.0", + }, + { + emoji: "🕵️‍♀️", + aliases: ["female_detective"], + tags: ["sleuth"], + category: "People & Body", + description: "woman detective", + unicode_version: "6.0", + }, + { + emoji: "💂", + aliases: ["guard"], + tags: [], + category: "People & Body", + description: "guard", + unicode_version: "6.0", + }, + { + emoji: "💂‍♂️", + aliases: ["guardsman"], + tags: [], + category: "People & Body", + description: "man guard", + unicode_version: "11.0", + }, + { + emoji: "💂‍♀️", + aliases: ["guardswoman"], + tags: [], + category: "People & Body", + description: "woman guard", + unicode_version: "6.0", + }, + { + emoji: "🥷", + aliases: ["ninja"], + tags: [], + category: "People & Body", + description: "ninja", + unicode_version: "13.0", + }, + { + emoji: "👷", + aliases: ["construction_worker"], + tags: ["helmet"], + category: "People & Body", + description: "construction worker", + unicode_version: "6.0", + }, + { + emoji: "👷‍♂️", + aliases: ["construction_worker_man"], + tags: ["helmet"], + category: "People & Body", + description: "man construction worker", + unicode_version: "11.0", + }, + { + emoji: "👷‍♀️", + aliases: ["construction_worker_woman"], + tags: ["helmet"], + category: "People & Body", + description: "woman construction worker", + unicode_version: "6.0", + }, + { + emoji: "🤴", + aliases: ["prince"], + tags: ["crown", "royal"], + category: "People & Body", + description: "prince", + unicode_version: "9.0", + }, + { + emoji: "👸", + aliases: ["princess"], + tags: ["crown", "royal"], + category: "People & Body", + description: "princess", + unicode_version: "6.0", + }, + { + emoji: "👳", + aliases: ["person_with_turban"], + tags: [], + category: "People & Body", + description: "person wearing turban", + unicode_version: "6.0", + }, + { + emoji: "👳‍♂️", + aliases: ["man_with_turban"], + tags: [], + category: "People & Body", + description: "man wearing turban", + unicode_version: "11.0", + }, + { + emoji: "👳‍♀️", + aliases: ["woman_with_turban"], + tags: [], + category: "People & Body", + description: "woman wearing turban", + unicode_version: "6.0", + }, + { + emoji: "👲", + aliases: ["man_with_gua_pi_mao"], + tags: [], + category: "People & Body", + description: "person with skullcap", + unicode_version: "6.0", + }, + { + emoji: "🧕", + aliases: ["woman_with_headscarf"], + tags: ["hijab"], + category: "People & Body", + description: "woman with headscarf", + unicode_version: "11.0", + }, + { + emoji: "🤵", + aliases: ["person_in_tuxedo"], + tags: ["groom", "marriage", "wedding"], + category: "People & Body", + description: "person in tuxedo", + unicode_version: "9.0", + }, + { + emoji: "🤵‍♂️", + aliases: ["man_in_tuxedo"], + tags: [], + category: "People & Body", + description: "man in tuxedo", + unicode_version: "13.0", + }, + { + emoji: "🤵‍♀️", + aliases: ["woman_in_tuxedo"], + tags: [], + category: "People & Body", + description: "woman in tuxedo", + unicode_version: "13.0", + }, + { + emoji: "👰", + aliases: ["person_with_veil"], + tags: ["marriage", "wedding"], + category: "People & Body", + description: "person with veil", + unicode_version: "6.0", + }, + { + emoji: "👰‍♂️", + aliases: ["man_with_veil"], + tags: [], + category: "People & Body", + description: "man with veil", + unicode_version: "13.0", + }, + { + emoji: "👰‍♀️", + aliases: ["woman_with_veil", "bride_with_veil"], + tags: [], + category: "People & Body", + description: "woman with veil", + unicode_version: "13.0", + }, + { + emoji: "🤰", + aliases: ["pregnant_woman"], + tags: [], + category: "People & Body", + description: "pregnant woman", + unicode_version: "9.0", + }, + { + emoji: "🤱", + aliases: ["breast_feeding"], + tags: ["nursing"], + category: "People & Body", + description: "breast-feeding", + unicode_version: "11.0", + }, + { + emoji: "👩‍🍼", + aliases: ["woman_feeding_baby"], + tags: [], + category: "People & Body", + description: "woman feeding baby", + unicode_version: "13.0", + }, + { + emoji: "👨‍🍼", + aliases: ["man_feeding_baby"], + tags: [], + category: "People & Body", + description: "man feeding baby", + unicode_version: "13.0", + }, + { + emoji: "🧑‍🍼", + aliases: ["person_feeding_baby"], + tags: [], + category: "People & Body", + description: "person feeding baby", + unicode_version: "13.0", + }, + { + emoji: "👼", + aliases: ["angel"], + tags: [], + category: "People & Body", + description: "baby angel", + unicode_version: "6.0", + }, + { + emoji: "🎅", + aliases: ["santa"], + tags: ["christmas"], + category: "People & Body", + description: "Santa Claus", + unicode_version: "6.0", + }, + { + emoji: "🤶", + aliases: ["mrs_claus"], + tags: ["santa"], + category: "People & Body", + description: "Mrs. Claus", + unicode_version: "9.0", + }, + { + emoji: "🧑‍🎄", + aliases: ["mx_claus"], + tags: [], + category: "People & Body", + description: "mx claus", + unicode_version: "13.0", + }, + { + emoji: "🦸", + aliases: ["superhero"], + tags: [], + category: "People & Body", + description: "superhero", + unicode_version: "11.0", + }, + { + emoji: "🦸‍♂️", + aliases: ["superhero_man"], + tags: [], + category: "People & Body", + description: "man superhero", + unicode_version: "11.0", + }, + { + emoji: "🦸‍♀️", + aliases: ["superhero_woman"], + tags: [], + category: "People & Body", + description: "woman superhero", + unicode_version: "11.0", + }, + { + emoji: "🦹", + aliases: ["supervillain"], + tags: [], + category: "People & Body", + description: "supervillain", + unicode_version: "11.0", + }, + { + emoji: "🦹‍♂️", + aliases: ["supervillain_man"], + tags: [], + category: "People & Body", + description: "man supervillain", + unicode_version: "11.0", + }, + { + emoji: "🦹‍♀️", + aliases: ["supervillain_woman"], + tags: [], + category: "People & Body", + description: "woman supervillain", + unicode_version: "11.0", + }, + { + emoji: "🧙", + aliases: ["mage"], + tags: ["wizard"], + category: "People & Body", + description: "mage", + unicode_version: "11.0", + }, + { + emoji: "🧙‍♂️", + aliases: ["mage_man"], + tags: ["wizard"], + category: "People & Body", + description: "man mage", + unicode_version: "11.0", + }, + { + emoji: "🧙‍♀️", + aliases: ["mage_woman"], + tags: ["wizard"], + category: "People & Body", + description: "woman mage", + unicode_version: "11.0", + }, + { + emoji: "🧚", + aliases: ["fairy"], + tags: [], + category: "People & Body", + description: "fairy", + unicode_version: "11.0", + }, + { + emoji: "🧚‍♂️", + aliases: ["fairy_man"], + tags: [], + category: "People & Body", + description: "man fairy", + unicode_version: "11.0", + }, + { + emoji: "🧚‍♀️", + aliases: ["fairy_woman"], + tags: [], + category: "People & Body", + description: "woman fairy", + unicode_version: "11.0", + }, + { + emoji: "🧛", + aliases: ["vampire"], + tags: [], + category: "People & Body", + description: "vampire", + unicode_version: "11.0", + }, + { + emoji: "🧛‍♂️", + aliases: ["vampire_man"], + tags: [], + category: "People & Body", + description: "man vampire", + unicode_version: "11.0", + }, + { + emoji: "🧛‍♀️", + aliases: ["vampire_woman"], + tags: [], + category: "People & Body", + description: "woman vampire", + unicode_version: "11.0", + }, + { + emoji: "🧜", + aliases: ["merperson"], + tags: [], + category: "People & Body", + description: "merperson", + unicode_version: "11.0", + }, + { + emoji: "🧜‍♂️", + aliases: ["merman"], + tags: [], + category: "People & Body", + description: "merman", + unicode_version: "11.0", + }, + { + emoji: "🧜‍♀️", + aliases: ["mermaid"], + tags: [], + category: "People & Body", + description: "mermaid", + unicode_version: "11.0", + }, + { + emoji: "🧝", + aliases: ["elf"], + tags: [], + category: "People & Body", + description: "elf", + unicode_version: "11.0", + }, + { + emoji: "🧝‍♂️", + aliases: ["elf_man"], + tags: [], + category: "People & Body", + description: "man elf", + unicode_version: "11.0", + }, + { + emoji: "🧝‍♀️", + aliases: ["elf_woman"], + tags: [], + category: "People & Body", + description: "woman elf", + unicode_version: "11.0", + }, + { + emoji: "🧞", + aliases: ["genie"], + tags: [], + category: "People & Body", + description: "genie", + unicode_version: "11.0", + }, + { + emoji: "🧞‍♂️", + aliases: ["genie_man"], + tags: [], + category: "People & Body", + description: "man genie", + unicode_version: "11.0", + }, + { + emoji: "🧞‍♀️", + aliases: ["genie_woman"], + tags: [], + category: "People & Body", + description: "woman genie", + unicode_version: "11.0", + }, + { + emoji: "🧟", + aliases: ["zombie"], + tags: [], + category: "People & Body", + description: "zombie", + unicode_version: "11.0", + }, + { + emoji: "🧟‍♂️", + aliases: ["zombie_man"], + tags: [], + category: "People & Body", + description: "man zombie", + unicode_version: "11.0", + }, + { + emoji: "🧟‍♀️", + aliases: ["zombie_woman"], + tags: [], + category: "People & Body", + description: "woman zombie", + unicode_version: "11.0", + }, + { + emoji: "💆", + aliases: ["massage"], + tags: ["spa"], + category: "People & Body", + description: "person getting massage", + unicode_version: "6.0", + }, + { + emoji: "💆‍♂️", + aliases: ["massage_man"], + tags: ["spa"], + category: "People & Body", + description: "man getting massage", + unicode_version: "6.0", + }, + { + emoji: "💆‍♀️", + aliases: ["massage_woman"], + tags: ["spa"], + category: "People & Body", + description: "woman getting massage", + unicode_version: "11.0", + }, + { + emoji: "💇", + aliases: ["haircut"], + tags: ["beauty"], + category: "People & Body", + description: "person getting haircut", + unicode_version: "6.0", + }, + { + emoji: "💇‍♂️", + aliases: ["haircut_man"], + tags: [], + category: "People & Body", + description: "man getting haircut", + unicode_version: "6.0", + }, + { + emoji: "💇‍♀️", + aliases: ["haircut_woman"], + tags: [], + category: "People & Body", + description: "woman getting haircut", + unicode_version: "11.0", + }, + { + emoji: "🚶", + aliases: ["walking"], + tags: [], + category: "People & Body", + description: "person walking", + unicode_version: "6.0", + }, + { + emoji: "🚶‍♂️", + aliases: ["walking_man"], + tags: [], + category: "People & Body", + description: "man walking", + unicode_version: "11.0", + }, + { + emoji: "🚶‍♀️", + aliases: ["walking_woman"], + tags: [], + category: "People & Body", + description: "woman walking", + unicode_version: "6.0", + }, + { + emoji: "🧍", + aliases: ["standing_person"], + tags: [], + category: "People & Body", + description: "person standing", + unicode_version: "12.0", + }, + { + emoji: "🧍‍♂️", + aliases: ["standing_man"], + tags: [], + category: "People & Body", + description: "man standing", + unicode_version: "12.0", + }, + { + emoji: "🧍‍♀️", + aliases: ["standing_woman"], + tags: [], + category: "People & Body", + description: "woman standing", + unicode_version: "12.0", + }, + { + emoji: "🧎", + aliases: ["kneeling_person"], + tags: [], + category: "People & Body", + description: "person kneeling", + unicode_version: "12.0", + }, + { + emoji: "🧎‍♂️", + aliases: ["kneeling_man"], + tags: [], + category: "People & Body", + description: "man kneeling", + unicode_version: "12.0", + }, + { + emoji: "🧎‍♀️", + aliases: ["kneeling_woman"], + tags: [], + category: "People & Body", + description: "woman kneeling", + unicode_version: "12.0", + }, + { + emoji: "🧑‍🦯", + aliases: ["person_with_probing_cane"], + tags: [], + category: "People & Body", + description: "person with white cane", + unicode_version: "12.1", + }, + { + emoji: "👨‍🦯", + aliases: ["man_with_probing_cane"], + tags: [], + category: "People & Body", + description: "man with white cane", + unicode_version: "12.0", + }, + { + emoji: "👩‍🦯", + aliases: ["woman_with_probing_cane"], + tags: [], + category: "People & Body", + description: "woman with white cane", + unicode_version: "12.0", + }, + { + emoji: "🧑‍🦼", + aliases: ["person_in_motorized_wheelchair"], + tags: [], + category: "People & Body", + description: "person in motorized wheelchair", + unicode_version: "12.1", + }, + { + emoji: "👨‍🦼", + aliases: ["man_in_motorized_wheelchair"], + tags: [], + category: "People & Body", + description: "man in motorized wheelchair", + unicode_version: "12.0", + }, + { + emoji: "👩‍🦼", + aliases: ["woman_in_motorized_wheelchair"], + tags: [], + category: "People & Body", + description: "woman in motorized wheelchair", + unicode_version: "12.0", + }, + { + emoji: "🧑‍🦽", + aliases: ["person_in_manual_wheelchair"], + tags: [], + category: "People & Body", + description: "person in manual wheelchair", + unicode_version: "12.1", + }, + { + emoji: "👨‍🦽", + aliases: ["man_in_manual_wheelchair"], + tags: [], + category: "People & Body", + description: "man in manual wheelchair", + unicode_version: "12.0", + }, + { + emoji: "👩‍🦽", + aliases: ["woman_in_manual_wheelchair"], + tags: [], + category: "People & Body", + description: "woman in manual wheelchair", + unicode_version: "12.0", + }, + { + emoji: "🏃", + aliases: ["runner", "running"], + tags: ["exercise", "workout", "marathon"], + category: "People & Body", + description: "person running", + unicode_version: "6.0", + }, + { + emoji: "🏃‍♂️", + aliases: ["running_man"], + tags: ["exercise", "workout", "marathon"], + category: "People & Body", + description: "man running", + unicode_version: "11.0", + }, + { + emoji: "🏃‍♀️", + aliases: ["running_woman"], + tags: ["exercise", "workout", "marathon"], + category: "People & Body", + description: "woman running", + unicode_version: "6.0", + }, + { + emoji: "💃", + aliases: ["woman_dancing", "dancer"], + tags: ["dress"], + category: "People & Body", + description: "woman dancing", + unicode_version: "6.0", + }, + { + emoji: "🕺", + aliases: ["man_dancing"], + tags: ["dancer"], + category: "People & Body", + description: "man dancing", + unicode_version: "9.0", + }, + { + emoji: "🕴️", + aliases: ["business_suit_levitating"], + tags: [], + category: "People & Body", + description: "person in suit levitating", + unicode_version: "7.0", + }, + { + emoji: "👯", + aliases: ["dancers"], + tags: ["bunny"], + category: "People & Body", + description: "people with bunny ears", + unicode_version: "6.0", + }, + { + emoji: "👯‍♂️", + aliases: ["dancing_men"], + tags: ["bunny"], + category: "People & Body", + description: "men with bunny ears", + unicode_version: "6.0", + }, + { + emoji: "👯‍♀️", + aliases: ["dancing_women"], + tags: ["bunny"], + category: "People & Body", + description: "women with bunny ears", + unicode_version: "11.0", + }, + { + emoji: "🧖", + aliases: ["sauna_person"], + tags: ["steamy"], + category: "People & Body", + description: "person in steamy room", + unicode_version: "11.0", + }, + { + emoji: "🧖‍♂️", + aliases: ["sauna_man"], + tags: ["steamy"], + category: "People & Body", + description: "man in steamy room", + unicode_version: "11.0", + }, + { + emoji: "🧖‍♀️", + aliases: ["sauna_woman"], + tags: ["steamy"], + category: "People & Body", + description: "woman in steamy room", + unicode_version: "11.0", + }, + { + emoji: "🧗", + aliases: ["climbing"], + tags: ["bouldering"], + category: "People & Body", + description: "person climbing", + unicode_version: "11.0", + }, + { + emoji: "🧗‍♂️", + aliases: ["climbing_man"], + tags: ["bouldering"], + category: "People & Body", + description: "man climbing", + unicode_version: "11.0", + }, + { + emoji: "🧗‍♀️", + aliases: ["climbing_woman"], + tags: ["bouldering"], + category: "People & Body", + description: "woman climbing", + unicode_version: "11.0", + }, + { + emoji: "🤺", + aliases: ["person_fencing"], + tags: [], + category: "People & Body", + description: "person fencing", + unicode_version: "9.0", + }, + { + emoji: "🏇", + aliases: ["horse_racing"], + tags: [], + category: "People & Body", + description: "horse racing", + unicode_version: "6.0", + }, + { + emoji: "⛷️", + aliases: ["skier"], + tags: [], + category: "People & Body", + description: "skier", + unicode_version: "5.2", + }, + { + emoji: "🏂", + aliases: ["snowboarder"], + tags: [], + category: "People & Body", + description: "snowboarder", + unicode_version: "6.0", + }, + { + emoji: "🏌️", + aliases: ["golfing"], + tags: [], + category: "People & Body", + description: "person golfing", + unicode_version: "7.0", + }, + { + emoji: "🏌️‍♂️", + aliases: ["golfing_man"], + tags: [], + category: "People & Body", + description: "man golfing", + unicode_version: "11.0", + }, + { + emoji: "🏌️‍♀️", + aliases: ["golfing_woman"], + tags: [], + category: "People & Body", + description: "woman golfing", + unicode_version: "", + }, + { + emoji: "🏄", + aliases: ["surfer"], + tags: [], + category: "People & Body", + description: "person surfing", + unicode_version: "6.0", + }, + { + emoji: "🏄‍♂️", + aliases: ["surfing_man"], + tags: [], + category: "People & Body", + description: "man surfing", + unicode_version: "11.0", + }, + { + emoji: "🏄‍♀️", + aliases: ["surfing_woman"], + tags: [], + category: "People & Body", + description: "woman surfing", + unicode_version: "7.0", + }, + { + emoji: "🚣", + aliases: ["rowboat"], + tags: [], + category: "People & Body", + description: "person rowing boat", + unicode_version: "6.0", + }, + { + emoji: "🚣‍♂️", + aliases: ["rowing_man"], + tags: [], + category: "People & Body", + description: "man rowing boat", + unicode_version: "11.0", + }, + { + emoji: "🚣‍♀️", + aliases: ["rowing_woman"], + tags: [], + category: "People & Body", + description: "woman rowing boat", + unicode_version: "6.0", + }, + { + emoji: "🏊", + aliases: ["swimmer"], + tags: [], + category: "People & Body", + description: "person swimming", + unicode_version: "6.0", + }, + { + emoji: "🏊‍♂️", + aliases: ["swimming_man"], + tags: [], + category: "People & Body", + description: "man swimming", + unicode_version: "11.0", + }, + { + emoji: "🏊‍♀️", + aliases: ["swimming_woman"], + tags: [], + category: "People & Body", + description: "woman swimming", + unicode_version: "6.0", + }, + { + emoji: "⛹️", + aliases: ["bouncing_ball_person"], + tags: ["basketball"], + category: "People & Body", + description: "person bouncing ball", + unicode_version: "5.2", + }, + { + emoji: "⛹️‍♂️", + aliases: ["bouncing_ball_man", "basketball_man"], + tags: [], + category: "People & Body", + description: "man bouncing ball", + unicode_version: "11.0", + }, + { + emoji: "⛹️‍♀️", + aliases: ["bouncing_ball_woman", "basketball_woman"], + tags: [], + category: "People & Body", + description: "woman bouncing ball", + unicode_version: "7.0", + }, + { + emoji: "🏋️", + aliases: ["weight_lifting"], + tags: ["gym", "workout"], + category: "People & Body", + description: "person lifting weights", + unicode_version: "7.0", + }, + { + emoji: "🏋️‍♂️", + aliases: ["weight_lifting_man"], + tags: ["gym", "workout"], + category: "People & Body", + description: "man lifting weights", + unicode_version: "11.0", + }, + { + emoji: "🏋️‍♀️", + aliases: ["weight_lifting_woman"], + tags: ["gym", "workout"], + category: "People & Body", + description: "woman lifting weights", + unicode_version: "6.0", + }, + { + emoji: "🚴", + aliases: ["bicyclist"], + tags: [], + category: "People & Body", + description: "person biking", + unicode_version: "6.0", + }, + { + emoji: "🚴‍♂️", + aliases: ["biking_man"], + tags: [], + category: "People & Body", + description: "man biking", + unicode_version: "11.0", + }, + { + emoji: "🚴‍♀️", + aliases: ["biking_woman"], + tags: [], + category: "People & Body", + description: "woman biking", + unicode_version: "6.0", + }, + { + emoji: "🚵", + aliases: ["mountain_bicyclist"], + tags: [], + category: "People & Body", + description: "person mountain biking", + unicode_version: "6.0", + }, + { + emoji: "🚵‍♂️", + aliases: ["mountain_biking_man"], + tags: [], + category: "People & Body", + description: "man mountain biking", + unicode_version: "11.0", + }, + { + emoji: "🚵‍♀️", + aliases: ["mountain_biking_woman"], + tags: [], + category: "People & Body", + description: "woman mountain biking", + unicode_version: "6.0", + }, + { + emoji: "🤸", + aliases: ["cartwheeling"], + tags: [], + category: "People & Body", + description: "person cartwheeling", + unicode_version: "11.0", + }, + { + emoji: "🤸‍♂️", + aliases: ["man_cartwheeling"], + tags: [], + category: "People & Body", + description: "man cartwheeling", + unicode_version: "", + }, + { + emoji: "🤸‍♀️", + aliases: ["woman_cartwheeling"], + tags: [], + category: "People & Body", + description: "woman cartwheeling", + unicode_version: "", + }, + { + emoji: "🤼", + aliases: ["wrestling"], + tags: [], + category: "People & Body", + description: "people wrestling", + unicode_version: "11.0", + }, + { + emoji: "🤼‍♂️", + aliases: ["men_wrestling"], + tags: [], + category: "People & Body", + description: "men wrestling", + unicode_version: "9.0", + }, + { + emoji: "🤼‍♀️", + aliases: ["women_wrestling"], + tags: [], + category: "People & Body", + description: "women wrestling", + unicode_version: "9.0", + }, + { + emoji: "🤽", + aliases: ["water_polo"], + tags: [], + category: "People & Body", + description: "person playing water polo", + unicode_version: "11.0", + }, + { + emoji: "🤽‍♂️", + aliases: ["man_playing_water_polo"], + tags: [], + category: "People & Body", + description: "man playing water polo", + unicode_version: "9.0", + }, + { + emoji: "🤽‍♀️", + aliases: ["woman_playing_water_polo"], + tags: [], + category: "People & Body", + description: "woman playing water polo", + unicode_version: "9.0", + }, + { + emoji: "🤾", + aliases: ["handball_person"], + tags: [], + category: "People & Body", + description: "person playing handball", + unicode_version: "11.0", + }, + { + emoji: "🤾‍♂️", + aliases: ["man_playing_handball"], + tags: [], + category: "People & Body", + description: "man playing handball", + unicode_version: "9.0", + }, + { + emoji: "🤾‍♀️", + aliases: ["woman_playing_handball"], + tags: [], + category: "People & Body", + description: "woman playing handball", + unicode_version: "9.0", + }, + { + emoji: "🤹", + aliases: ["juggling_person"], + tags: [], + category: "People & Body", + description: "person juggling", + unicode_version: "11.0", + }, + { + emoji: "🤹‍♂️", + aliases: ["man_juggling"], + tags: [], + category: "People & Body", + description: "man juggling", + unicode_version: "9.0", + }, + { + emoji: "🤹‍♀️", + aliases: ["woman_juggling"], + tags: [], + category: "People & Body", + description: "woman juggling", + unicode_version: "9.0", + }, + { + emoji: "🧘", + aliases: ["lotus_position"], + tags: ["meditation"], + category: "People & Body", + description: "person in lotus position", + unicode_version: "11.0", + }, + { + emoji: "🧘‍♂️", + aliases: ["lotus_position_man"], + tags: ["meditation"], + category: "People & Body", + description: "man in lotus position", + unicode_version: "11.0", + }, + { + emoji: "🧘‍♀️", + aliases: ["lotus_position_woman"], + tags: ["meditation"], + category: "People & Body", + description: "woman in lotus position", + unicode_version: "11.0", + }, + { + emoji: "🛀", + aliases: ["bath"], + tags: ["shower"], + category: "People & Body", + description: "person taking bath", + unicode_version: "6.0", + }, + { + emoji: "🛌", + aliases: ["sleeping_bed"], + tags: [], + category: "People & Body", + description: "person in bed", + unicode_version: "7.0", + }, + { + emoji: "🧑‍🤝‍🧑", + aliases: ["people_holding_hands"], + tags: ["couple", "date"], + category: "People & Body", + description: "people holding hands", + unicode_version: "12.0", + }, + { + emoji: "👭", + aliases: ["two_women_holding_hands"], + tags: ["couple", "date"], + category: "People & Body", + description: "women holding hands", + unicode_version: "6.0", + }, + { + emoji: "👫", + aliases: ["couple"], + tags: ["date"], + category: "People & Body", + description: "woman and man holding hands", + unicode_version: "6.0", + }, + { + emoji: "👬", + aliases: ["two_men_holding_hands"], + tags: ["couple", "date"], + category: "People & Body", + description: "men holding hands", + unicode_version: "6.0", + }, + { + emoji: "💏", + aliases: ["couplekiss"], + tags: [], + category: "People & Body", + description: "kiss", + unicode_version: "6.0", + }, + { + emoji: "👩‍❤️‍💋‍👨", + aliases: ["couplekiss_man_woman"], + tags: [], + category: "People & Body", + description: "kiss: woman, man", + unicode_version: "11.0", + }, + { + emoji: "👨‍❤️‍💋‍👨", + aliases: ["couplekiss_man_man"], + tags: [], + category: "People & Body", + description: "kiss: man, man", + unicode_version: "6.0", + }, + { + emoji: "👩‍❤️‍💋‍👩", + aliases: ["couplekiss_woman_woman"], + tags: [], + category: "People & Body", + description: "kiss: woman, woman", + unicode_version: "6.0", + }, + { + emoji: "💑", + aliases: ["couple_with_heart"], + tags: [], + category: "People & Body", + description: "couple with heart", + unicode_version: "6.0", + }, + { + emoji: "👩‍❤️‍👨", + aliases: ["couple_with_heart_woman_man"], + tags: [], + category: "People & Body", + description: "couple with heart: woman, man", + unicode_version: "11.0", + }, + { + emoji: "👨‍❤️‍👨", + aliases: ["couple_with_heart_man_man"], + tags: [], + category: "People & Body", + description: "couple with heart: man, man", + unicode_version: "6.0", + }, + { + emoji: "👩‍❤️‍👩", + aliases: ["couple_with_heart_woman_woman"], + tags: [], + category: "People & Body", + description: "couple with heart: woman, woman", + unicode_version: "6.0", + }, + { + emoji: "👪", + aliases: ["family"], + tags: ["home", "parents", "child"], + category: "People & Body", + description: "family", + unicode_version: "6.0", + }, + { + emoji: "👨‍👩‍👦", + aliases: ["family_man_woman_boy"], + tags: [], + category: "People & Body", + description: "family: man, woman, boy", + unicode_version: "11.0", + }, + { + emoji: "👨‍👩‍👧", + aliases: ["family_man_woman_girl"], + tags: [], + category: "People & Body", + description: "family: man, woman, girl", + unicode_version: "6.0", + }, + { + emoji: "👨‍👩‍👧‍👦", + aliases: ["family_man_woman_girl_boy"], + tags: [], + category: "People & Body", + description: "family: man, woman, girl, boy", + unicode_version: "6.0", + }, + { + emoji: "👨‍👩‍👦‍👦", + aliases: ["family_man_woman_boy_boy"], + tags: [], + category: "People & Body", + description: "family: man, woman, boy, boy", + unicode_version: "6.0", + }, + { + emoji: "👨‍👩‍👧‍👧", + aliases: ["family_man_woman_girl_girl"], + tags: [], + category: "People & Body", + description: "family: man, woman, girl, girl", + unicode_version: "6.0", + }, + { + emoji: "👨‍👨‍👦", + aliases: ["family_man_man_boy"], + tags: [], + category: "People & Body", + description: "family: man, man, boy", + unicode_version: "6.0", + }, + { + emoji: "👨‍👨‍👧", + aliases: ["family_man_man_girl"], + tags: [], + category: "People & Body", + description: "family: man, man, girl", + unicode_version: "6.0", + }, + { + emoji: "👨‍👨‍👧‍👦", + aliases: ["family_man_man_girl_boy"], + tags: [], + category: "People & Body", + description: "family: man, man, girl, boy", + unicode_version: "6.0", + }, + { + emoji: "👨‍👨‍👦‍👦", + aliases: ["family_man_man_boy_boy"], + tags: [], + category: "People & Body", + description: "family: man, man, boy, boy", + unicode_version: "6.0", + }, + { + emoji: "👨‍👨‍👧‍👧", + aliases: ["family_man_man_girl_girl"], + tags: [], + category: "People & Body", + description: "family: man, man, girl, girl", + unicode_version: "6.0", + }, + { + emoji: "👩‍👩‍👦", + aliases: ["family_woman_woman_boy"], + tags: [], + category: "People & Body", + description: "family: woman, woman, boy", + unicode_version: "6.0", + }, + { + emoji: "👩‍👩‍👧", + aliases: ["family_woman_woman_girl"], + tags: [], + category: "People & Body", + description: "family: woman, woman, girl", + unicode_version: "6.0", + }, + { + emoji: "👩‍👩‍👧‍👦", + aliases: ["family_woman_woman_girl_boy"], + tags: [], + category: "People & Body", + description: "family: woman, woman, girl, boy", + unicode_version: "6.0", + }, + { + emoji: "👩‍👩‍👦‍👦", + aliases: ["family_woman_woman_boy_boy"], + tags: [], + category: "People & Body", + description: "family: woman, woman, boy, boy", + unicode_version: "6.0", + }, + { + emoji: "👩‍👩‍👧‍👧", + aliases: ["family_woman_woman_girl_girl"], + tags: [], + category: "People & Body", + description: "family: woman, woman, girl, girl", + unicode_version: "6.0", + }, + { + emoji: "👨‍👦", + aliases: ["family_man_boy"], + tags: [], + category: "People & Body", + description: "family: man, boy", + unicode_version: "6.0", + }, + { + emoji: "👨‍👦‍👦", + aliases: ["family_man_boy_boy"], + tags: [], + category: "People & Body", + description: "family: man, boy, boy", + unicode_version: "6.0", + }, + { + emoji: "👨‍👧", + aliases: ["family_man_girl"], + tags: [], + category: "People & Body", + description: "family: man, girl", + unicode_version: "6.0", + }, + { + emoji: "👨‍👧‍👦", + aliases: ["family_man_girl_boy"], + tags: [], + category: "People & Body", + description: "family: man, girl, boy", + unicode_version: "6.0", + }, + { + emoji: "👨‍👧‍👧", + aliases: ["family_man_girl_girl"], + tags: [], + category: "People & Body", + description: "family: man, girl, girl", + unicode_version: "6.0", + }, + { + emoji: "👩‍👦", + aliases: ["family_woman_boy"], + tags: [], + category: "People & Body", + description: "family: woman, boy", + unicode_version: "6.0", + }, + { + emoji: "👩‍👦‍👦", + aliases: ["family_woman_boy_boy"], + tags: [], + category: "People & Body", + description: "family: woman, boy, boy", + unicode_version: "6.0", + }, + { + emoji: "👩‍👧", + aliases: ["family_woman_girl"], + tags: [], + category: "People & Body", + description: "family: woman, girl", + unicode_version: "6.0", + }, + { + emoji: "👩‍👧‍👦", + aliases: ["family_woman_girl_boy"], + tags: [], + category: "People & Body", + description: "family: woman, girl, boy", + unicode_version: "6.0", + }, + { + emoji: "👩‍👧‍👧", + aliases: ["family_woman_girl_girl"], + tags: [], + category: "People & Body", + description: "family: woman, girl, girl", + unicode_version: "6.0", + }, + { + emoji: "🗣️", + aliases: ["speaking_head"], + tags: [], + category: "People & Body", + description: "speaking head", + unicode_version: "7.0", + }, + { + emoji: "👤", + aliases: ["bust_in_silhouette"], + tags: ["user"], + category: "People & Body", + description: "bust in silhouette", + unicode_version: "6.0", + }, + { + emoji: "👥", + aliases: ["busts_in_silhouette"], + tags: ["users", "group", "team"], + category: "People & Body", + description: "busts in silhouette", + unicode_version: "6.0", + }, + { + emoji: "🫂", + aliases: ["people_hugging"], + tags: [], + category: "People & Body", + description: "people hugging", + unicode_version: "13.0", + }, + { + emoji: "👣", + aliases: ["footprints"], + tags: ["feet", "tracks"], + category: "People & Body", + description: "footprints", + unicode_version: "6.0", + }, + { + emoji: "🐵", + aliases: ["monkey_face"], + tags: [], + category: "Animals & Nature", + description: "monkey face", + unicode_version: "6.0", + }, + { + emoji: "🐒", + aliases: ["monkey"], + tags: [], + category: "Animals & Nature", + description: "monkey", + unicode_version: "6.0", + }, + { + emoji: "🦍", + aliases: ["gorilla"], + tags: [], + category: "Animals & Nature", + description: "gorilla", + unicode_version: "9.0", + }, + { + emoji: "🦧", + aliases: ["orangutan"], + tags: [], + category: "Animals & Nature", + description: "orangutan", + unicode_version: "12.0", + }, + { + emoji: "🐶", + aliases: ["dog"], + tags: ["pet"], + category: "Animals & Nature", + description: "dog face", + unicode_version: "6.0", + }, + { + emoji: "🐕", + aliases: ["dog2"], + tags: [], + category: "Animals & Nature", + description: "dog", + unicode_version: "6.0", + }, + { + emoji: "🦮", + aliases: ["guide_dog"], + tags: [], + category: "Animals & Nature", + description: "guide dog", + unicode_version: "12.0", + }, + { + emoji: "🐕‍🦺", + aliases: ["service_dog"], + tags: [], + category: "Animals & Nature", + description: "service dog", + unicode_version: "12.0", + }, + { + emoji: "🐩", + aliases: ["poodle"], + tags: ["dog"], + category: "Animals & Nature", + description: "poodle", + unicode_version: "6.0", + }, + { + emoji: "🐺", + aliases: ["wolf"], + tags: [], + category: "Animals & Nature", + description: "wolf", + unicode_version: "6.0", + }, + { + emoji: "🦊", + aliases: ["fox_face"], + tags: [], + category: "Animals & Nature", + description: "fox", + unicode_version: "9.0", + }, + { + emoji: "🦝", + aliases: ["raccoon"], + tags: [], + category: "Animals & Nature", + description: "raccoon", + unicode_version: "11.0", + }, + { + emoji: "🐱", + aliases: ["cat"], + tags: ["pet"], + category: "Animals & Nature", + description: "cat face", + unicode_version: "6.0", + }, + { + emoji: "🐈", + aliases: ["cat2"], + tags: [], + category: "Animals & Nature", + description: "cat", + unicode_version: "6.0", + }, + { + emoji: "🐈‍⬛", + aliases: ["black_cat"], + tags: [], + category: "Animals & Nature", + description: "black cat", + unicode_version: "13.0", + }, + { + emoji: "🦁", + aliases: ["lion"], + tags: [], + category: "Animals & Nature", + description: "lion", + unicode_version: "8.0", + }, + { + emoji: "🐯", + aliases: ["tiger"], + tags: [], + category: "Animals & Nature", + description: "tiger face", + unicode_version: "6.0", + }, + { + emoji: "🐅", + aliases: ["tiger2"], + tags: [], + category: "Animals & Nature", + description: "tiger", + unicode_version: "6.0", + }, + { + emoji: "🐆", + aliases: ["leopard"], + tags: [], + category: "Animals & Nature", + description: "leopard", + unicode_version: "6.0", + }, + { + emoji: "🐴", + aliases: ["horse"], + tags: [], + category: "Animals & Nature", + description: "horse face", + unicode_version: "6.0", + }, + { + emoji: "🐎", + aliases: ["racehorse"], + tags: ["speed"], + category: "Animals & Nature", + description: "horse", + unicode_version: "6.0", + }, + { + emoji: "🦄", + aliases: ["unicorn"], + tags: [], + category: "Animals & Nature", + description: "unicorn", + unicode_version: "8.0", + }, + { + emoji: "🦓", + aliases: ["zebra"], + tags: [], + category: "Animals & Nature", + description: "zebra", + unicode_version: "11.0", + }, + { + emoji: "🦌", + aliases: ["deer"], + tags: [], + category: "Animals & Nature", + description: "deer", + unicode_version: "9.0", + }, + { + emoji: "🦬", + aliases: ["bison"], + tags: [], + category: "Animals & Nature", + description: "bison", + unicode_version: "13.0", + }, + { + emoji: "🐮", + aliases: ["cow"], + tags: [], + category: "Animals & Nature", + description: "cow face", + unicode_version: "6.0", + }, + { + emoji: "🐂", + aliases: ["ox"], + tags: [], + category: "Animals & Nature", + description: "ox", + unicode_version: "6.0", + }, + { + emoji: "🐃", + aliases: ["water_buffalo"], + tags: [], + category: "Animals & Nature", + description: "water buffalo", + unicode_version: "6.0", + }, + { + emoji: "🐄", + aliases: ["cow2"], + tags: [], + category: "Animals & Nature", + description: "cow", + unicode_version: "6.0", + }, + { + emoji: "🐷", + aliases: ["pig"], + tags: [], + category: "Animals & Nature", + description: "pig face", + unicode_version: "6.0", + }, + { + emoji: "🐖", + aliases: ["pig2"], + tags: [], + category: "Animals & Nature", + description: "pig", + unicode_version: "6.0", + }, + { + emoji: "🐗", + aliases: ["boar"], + tags: [], + category: "Animals & Nature", + description: "boar", + unicode_version: "6.0", + }, + { + emoji: "🐽", + aliases: ["pig_nose"], + tags: [], + category: "Animals & Nature", + description: "pig nose", + unicode_version: "6.0", + }, + { + emoji: "🐏", + aliases: ["ram"], + tags: [], + category: "Animals & Nature", + description: "ram", + unicode_version: "6.0", + }, + { + emoji: "🐑", + aliases: ["sheep"], + tags: [], + category: "Animals & Nature", + description: "ewe", + unicode_version: "6.0", + }, + { + emoji: "🐐", + aliases: ["goat"], + tags: [], + category: "Animals & Nature", + description: "goat", + unicode_version: "6.0", + }, + { + emoji: "🐪", + aliases: ["dromedary_camel"], + tags: ["desert"], + category: "Animals & Nature", + description: "camel", + unicode_version: "6.0", + }, + { + emoji: "🐫", + aliases: ["camel"], + tags: [], + category: "Animals & Nature", + description: "two-hump camel", + unicode_version: "6.0", + }, + { + emoji: "🦙", + aliases: ["llama"], + tags: [], + category: "Animals & Nature", + description: "llama", + unicode_version: "11.0", + }, + { + emoji: "🦒", + aliases: ["giraffe"], + tags: [], + category: "Animals & Nature", + description: "giraffe", + unicode_version: "11.0", + }, + { + emoji: "🐘", + aliases: ["elephant"], + tags: [], + category: "Animals & Nature", + description: "elephant", + unicode_version: "6.0", + }, + { + emoji: "🦣", + aliases: ["mammoth"], + tags: [], + category: "Animals & Nature", + description: "mammoth", + unicode_version: "13.0", + }, + { + emoji: "🦏", + aliases: ["rhinoceros"], + tags: [], + category: "Animals & Nature", + description: "rhinoceros", + unicode_version: "9.0", + }, + { + emoji: "🦛", + aliases: ["hippopotamus"], + tags: [], + category: "Animals & Nature", + description: "hippopotamus", + unicode_version: "11.0", + }, + { + emoji: "🐭", + aliases: ["mouse"], + tags: [], + category: "Animals & Nature", + description: "mouse face", + unicode_version: "6.0", + }, + { + emoji: "🐁", + aliases: ["mouse2"], + tags: [], + category: "Animals & Nature", + description: "mouse", + unicode_version: "6.0", + }, + { + emoji: "🐀", + aliases: ["rat"], + tags: [], + category: "Animals & Nature", + description: "rat", + unicode_version: "6.0", + }, + { + emoji: "🐹", + aliases: ["hamster"], + tags: ["pet"], + category: "Animals & Nature", + description: "hamster", + unicode_version: "6.0", + }, + { + emoji: "🐰", + aliases: ["rabbit"], + tags: ["bunny"], + category: "Animals & Nature", + description: "rabbit face", + unicode_version: "6.0", + }, + { + emoji: "🐇", + aliases: ["rabbit2"], + tags: [], + category: "Animals & Nature", + description: "rabbit", + unicode_version: "6.0", + }, + { + emoji: "🐿️", + aliases: ["chipmunk"], + tags: [], + category: "Animals & Nature", + description: "chipmunk", + unicode_version: "7.0", + }, + { + emoji: "🦫", + aliases: ["beaver"], + tags: [], + category: "Animals & Nature", + description: "beaver", + unicode_version: "13.0", + }, + { + emoji: "🦔", + aliases: ["hedgehog"], + tags: [], + category: "Animals & Nature", + description: "hedgehog", + unicode_version: "11.0", + }, + { + emoji: "🦇", + aliases: ["bat"], + tags: [], + category: "Animals & Nature", + description: "bat", + unicode_version: "9.0", + }, + { + emoji: "🐻", + aliases: ["bear"], + tags: [], + category: "Animals & Nature", + description: "bear", + unicode_version: "6.0", + }, + { + emoji: "🐻‍❄️", + aliases: ["polar_bear"], + tags: [], + category: "Animals & Nature", + description: "polar bear", + unicode_version: "13.0", + }, + { + emoji: "🐨", + aliases: ["koala"], + tags: [], + category: "Animals & Nature", + description: "koala", + unicode_version: "6.0", + }, + { + emoji: "🐼", + aliases: ["panda_face"], + tags: [], + category: "Animals & Nature", + description: "panda", + unicode_version: "6.0", + }, + { + emoji: "🦥", + aliases: ["sloth"], + tags: [], + category: "Animals & Nature", + description: "sloth", + unicode_version: "12.0", + }, + { + emoji: "🦦", + aliases: ["otter"], + tags: [], + category: "Animals & Nature", + description: "otter", + unicode_version: "12.0", + }, + { + emoji: "🦨", + aliases: ["skunk"], + tags: [], + category: "Animals & Nature", + description: "skunk", + unicode_version: "12.0", + }, + { + emoji: "🦘", + aliases: ["kangaroo"], + tags: [], + category: "Animals & Nature", + description: "kangaroo", + unicode_version: "11.0", + }, + { + emoji: "🦡", + aliases: ["badger"], + tags: [], + category: "Animals & Nature", + description: "badger", + unicode_version: "11.0", + }, + { + emoji: "🐾", + aliases: ["feet", "paw_prints"], + tags: [], + category: "Animals & Nature", + description: "paw prints", + unicode_version: "6.0", + }, + { + emoji: "🦃", + aliases: ["turkey"], + tags: ["thanksgiving"], + category: "Animals & Nature", + description: "turkey", + unicode_version: "8.0", + }, + { + emoji: "🐔", + aliases: ["chicken"], + tags: [], + category: "Animals & Nature", + description: "chicken", + unicode_version: "6.0", + }, + { + emoji: "🐓", + aliases: ["rooster"], + tags: [], + category: "Animals & Nature", + description: "rooster", + unicode_version: "6.0", + }, + { + emoji: "🐣", + aliases: ["hatching_chick"], + tags: [], + category: "Animals & Nature", + description: "hatching chick", + unicode_version: "6.0", + }, + { + emoji: "🐤", + aliases: ["baby_chick"], + tags: [], + category: "Animals & Nature", + description: "baby chick", + unicode_version: "6.0", + }, + { + emoji: "🐥", + aliases: ["hatched_chick"], + tags: [], + category: "Animals & Nature", + description: "front-facing baby chick", + unicode_version: "6.0", + }, + { + emoji: "🐦", + aliases: ["bird"], + tags: [], + category: "Animals & Nature", + description: "bird", + unicode_version: "6.0", + }, + { + emoji: "🐧", + aliases: ["penguin"], + tags: [], + category: "Animals & Nature", + description: "penguin", + unicode_version: "6.0", + }, + { + emoji: "🕊️", + aliases: ["dove"], + tags: ["peace"], + category: "Animals & Nature", + description: "dove", + unicode_version: "7.0", + }, + { + emoji: "🦅", + aliases: ["eagle"], + tags: [], + category: "Animals & Nature", + description: "eagle", + unicode_version: "9.0", + }, + { + emoji: "🦆", + aliases: ["duck"], + tags: [], + category: "Animals & Nature", + description: "duck", + unicode_version: "9.0", + }, + { + emoji: "🦢", + aliases: ["swan"], + tags: [], + category: "Animals & Nature", + description: "swan", + unicode_version: "11.0", + }, + { + emoji: "🦉", + aliases: ["owl"], + tags: [], + category: "Animals & Nature", + description: "owl", + unicode_version: "9.0", + }, + { + emoji: "🦤", + aliases: ["dodo"], + tags: [], + category: "Animals & Nature", + description: "dodo", + unicode_version: "13.0", + }, + { + emoji: "🪶", + aliases: ["feather"], + tags: [], + category: "Animals & Nature", + description: "feather", + unicode_version: "13.0", + }, + { + emoji: "🦩", + aliases: ["flamingo"], + tags: [], + category: "Animals & Nature", + description: "flamingo", + unicode_version: "12.0", + }, + { + emoji: "🦚", + aliases: ["peacock"], + tags: [], + category: "Animals & Nature", + description: "peacock", + unicode_version: "11.0", + }, + { + emoji: "🦜", + aliases: ["parrot"], + tags: [], + category: "Animals & Nature", + description: "parrot", + unicode_version: "11.0", + }, + { + emoji: "🐸", + aliases: ["frog"], + tags: [], + category: "Animals & Nature", + description: "frog", + unicode_version: "6.0", + }, + { + emoji: "🐊", + aliases: ["crocodile"], + tags: [], + category: "Animals & Nature", + description: "crocodile", + unicode_version: "6.0", + }, + { + emoji: "🐢", + aliases: ["turtle"], + tags: ["slow"], + category: "Animals & Nature", + description: "turtle", + unicode_version: "6.0", + }, + { + emoji: "🦎", + aliases: ["lizard"], + tags: [], + category: "Animals & Nature", + description: "lizard", + unicode_version: "9.0", + }, + { + emoji: "🐍", + aliases: ["snake"], + tags: [], + category: "Animals & Nature", + description: "snake", + unicode_version: "6.0", + }, + { + emoji: "🐲", + aliases: ["dragon_face"], + tags: [], + category: "Animals & Nature", + description: "dragon face", + unicode_version: "6.0", + }, + { + emoji: "🐉", + aliases: ["dragon"], + tags: [], + category: "Animals & Nature", + description: "dragon", + unicode_version: "6.0", + }, + { + emoji: "🦕", + aliases: ["sauropod"], + tags: ["dinosaur"], + category: "Animals & Nature", + description: "sauropod", + unicode_version: "11.0", + }, + { + emoji: "🦖", + aliases: ["t-rex"], + tags: ["dinosaur"], + category: "Animals & Nature", + description: "T-Rex", + unicode_version: "11.0", + }, + { + emoji: "🐳", + aliases: ["whale"], + tags: ["sea"], + category: "Animals & Nature", + description: "spouting whale", + unicode_version: "6.0", + }, + { + emoji: "🐋", + aliases: ["whale2"], + tags: [], + category: "Animals & Nature", + description: "whale", + unicode_version: "6.0", + }, + { + emoji: "🐬", + aliases: ["dolphin", "flipper"], + tags: [], + category: "Animals & Nature", + description: "dolphin", + unicode_version: "6.0", + }, + { + emoji: "🦭", + aliases: ["seal"], + tags: [], + category: "Animals & Nature", + description: "seal", + unicode_version: "13.0", + }, + { + emoji: "🐟", + aliases: ["fish"], + tags: [], + category: "Animals & Nature", + description: "fish", + unicode_version: "6.0", + }, + { + emoji: "🐠", + aliases: ["tropical_fish"], + tags: [], + category: "Animals & Nature", + description: "tropical fish", + unicode_version: "6.0", + }, + { + emoji: "🐡", + aliases: ["blowfish"], + tags: [], + category: "Animals & Nature", + description: "blowfish", + unicode_version: "6.0", + }, + { + emoji: "🦈", + aliases: ["shark"], + tags: [], + category: "Animals & Nature", + description: "shark", + unicode_version: "9.0", + }, + { + emoji: "🐙", + aliases: ["octopus"], + tags: [], + category: "Animals & Nature", + description: "octopus", + unicode_version: "6.0", + }, + { + emoji: "🐚", + aliases: ["shell"], + tags: ["sea", "beach"], + category: "Animals & Nature", + description: "spiral shell", + unicode_version: "6.0", + }, + { + emoji: "🐌", + aliases: ["snail"], + tags: ["slow"], + category: "Animals & Nature", + description: "snail", + unicode_version: "6.0", + }, + { + emoji: "🦋", + aliases: ["butterfly"], + tags: [], + category: "Animals & Nature", + description: "butterfly", + unicode_version: "9.0", + }, + { + emoji: "🐛", + aliases: ["bug"], + tags: [], + category: "Animals & Nature", + description: "bug", + unicode_version: "6.0", + }, + { + emoji: "🐜", + aliases: ["ant"], + tags: [], + category: "Animals & Nature", + description: "ant", + unicode_version: "6.0", + }, + { + emoji: "🐝", + aliases: ["bee", "honeybee"], + tags: [], + category: "Animals & Nature", + description: "honeybee", + unicode_version: "6.0", + }, + { + emoji: "🪲", + aliases: ["beetle"], + tags: [], + category: "Animals & Nature", + description: "beetle", + unicode_version: "13.0", + }, + { + emoji: "🐞", + aliases: ["lady_beetle"], + tags: ["bug"], + category: "Animals & Nature", + description: "lady beetle", + unicode_version: "6.0", + }, + { + emoji: "🦗", + aliases: ["cricket"], + tags: [], + category: "Animals & Nature", + description: "cricket", + unicode_version: "11.0", + }, + { + emoji: "🪳", + aliases: ["cockroach"], + tags: [], + category: "Animals & Nature", + description: "cockroach", + unicode_version: "13.0", + }, + { + emoji: "🕷️", + aliases: ["spider"], + tags: [], + category: "Animals & Nature", + description: "spider", + unicode_version: "7.0", + }, + { + emoji: "🕸️", + aliases: ["spider_web"], + tags: [], + category: "Animals & Nature", + description: "spider web", + unicode_version: "7.0", + }, + { + emoji: "🦂", + aliases: ["scorpion"], + tags: [], + category: "Animals & Nature", + description: "scorpion", + unicode_version: "8.0", + }, + { + emoji: "🦟", + aliases: ["mosquito"], + tags: [], + category: "Animals & Nature", + description: "mosquito", + unicode_version: "11.0", + }, + { + emoji: "🪰", + aliases: ["fly"], + tags: [], + category: "Animals & Nature", + description: "fly", + unicode_version: "13.0", + }, + { + emoji: "🪱", + aliases: ["worm"], + tags: [], + category: "Animals & Nature", + description: "worm", + unicode_version: "13.0", + }, + { + emoji: "🦠", + aliases: ["microbe"], + tags: ["germ"], + category: "Animals & Nature", + description: "microbe", + unicode_version: "11.0", + }, + { + emoji: "💐", + aliases: ["bouquet"], + tags: ["flowers"], + category: "Animals & Nature", + description: "bouquet", + unicode_version: "6.0", + }, + { + emoji: "🌸", + aliases: ["cherry_blossom"], + tags: ["flower", "spring"], + category: "Animals & Nature", + description: "cherry blossom", + unicode_version: "6.0", + }, + { + emoji: "💮", + aliases: ["white_flower"], + tags: [], + category: "Animals & Nature", + description: "white flower", + unicode_version: "6.0", + }, + { + emoji: "🏵️", + aliases: ["rosette"], + tags: [], + category: "Animals & Nature", + description: "rosette", + unicode_version: "7.0", + }, + { + emoji: "🌹", + aliases: ["rose"], + tags: ["flower"], + category: "Animals & Nature", + description: "rose", + unicode_version: "6.0", + }, + { + emoji: "🥀", + aliases: ["wilted_flower"], + tags: [], + category: "Animals & Nature", + description: "wilted flower", + unicode_version: "9.0", + }, + { + emoji: "🌺", + aliases: ["hibiscus"], + tags: [], + category: "Animals & Nature", + description: "hibiscus", + unicode_version: "6.0", + }, + { + emoji: "🌻", + aliases: ["sunflower"], + tags: [], + category: "Animals & Nature", + description: "sunflower", + unicode_version: "6.0", + }, + { + emoji: "🌼", + aliases: ["blossom"], + tags: [], + category: "Animals & Nature", + description: "blossom", + unicode_version: "6.0", + }, + { + emoji: "🌷", + aliases: ["tulip"], + tags: ["flower"], + category: "Animals & Nature", + description: "tulip", + unicode_version: "6.0", + }, + { + emoji: "🌱", + aliases: ["seedling"], + tags: ["plant"], + category: "Animals & Nature", + description: "seedling", + unicode_version: "6.0", + }, + { + emoji: "🪴", + aliases: ["potted_plant"], + tags: [], + category: "Animals & Nature", + description: "potted plant", + unicode_version: "13.0", + }, + { + emoji: "🌲", + aliases: ["evergreen_tree"], + tags: ["wood"], + category: "Animals & Nature", + description: "evergreen tree", + unicode_version: "6.0", + }, + { + emoji: "🌳", + aliases: ["deciduous_tree"], + tags: ["wood"], + category: "Animals & Nature", + description: "deciduous tree", + unicode_version: "6.0", + }, + { + emoji: "🌴", + aliases: ["palm_tree"], + tags: [], + category: "Animals & Nature", + description: "palm tree", + unicode_version: "6.0", + }, + { + emoji: "🌵", + aliases: ["cactus"], + tags: [], + category: "Animals & Nature", + description: "cactus", + unicode_version: "6.0", + }, + { + emoji: "🌾", + aliases: ["ear_of_rice"], + tags: [], + category: "Animals & Nature", + description: "sheaf of rice", + unicode_version: "6.0", + }, + { + emoji: "🌿", + aliases: ["herb"], + tags: [], + category: "Animals & Nature", + description: "herb", + unicode_version: "6.0", + }, + { + emoji: "☘️", + aliases: ["shamrock"], + tags: [], + category: "Animals & Nature", + description: "shamrock", + unicode_version: "4.1", + }, + { + emoji: "🍀", + aliases: ["four_leaf_clover"], + tags: ["luck"], + category: "Animals & Nature", + description: "four leaf clover", + unicode_version: "6.0", + }, + { + emoji: "🍁", + aliases: ["maple_leaf"], + tags: ["canada"], + category: "Animals & Nature", + description: "maple leaf", + unicode_version: "6.0", + }, + { + emoji: "🍂", + aliases: ["fallen_leaf"], + tags: ["autumn"], + category: "Animals & Nature", + description: "fallen leaf", + unicode_version: "6.0", + }, + { + emoji: "🍃", + aliases: ["leaves"], + tags: ["leaf"], + category: "Animals & Nature", + description: "leaf fluttering in wind", + unicode_version: "6.0", + }, + { + emoji: "🍇", + aliases: ["grapes"], + tags: [], + category: "Food & Drink", + description: "grapes", + unicode_version: "6.0", + }, + { + emoji: "🍈", + aliases: ["melon"], + tags: [], + category: "Food & Drink", + description: "melon", + unicode_version: "6.0", + }, + { + emoji: "🍉", + aliases: ["watermelon"], + tags: [], + category: "Food & Drink", + description: "watermelon", + unicode_version: "6.0", + }, + { + emoji: "🍊", + aliases: ["tangerine", "orange", "mandarin"], + tags: [], + category: "Food & Drink", + description: "tangerine", + unicode_version: "6.0", + }, + { + emoji: "🍋", + aliases: ["lemon"], + tags: [], + category: "Food & Drink", + description: "lemon", + unicode_version: "6.0", + }, + { + emoji: "🍌", + aliases: ["banana"], + tags: ["fruit"], + category: "Food & Drink", + description: "banana", + unicode_version: "6.0", + }, + { + emoji: "🍍", + aliases: ["pineapple"], + tags: [], + category: "Food & Drink", + description: "pineapple", + unicode_version: "6.0", + }, + { + emoji: "🥭", + aliases: ["mango"], + tags: [], + category: "Food & Drink", + description: "mango", + unicode_version: "11.0", + }, + { + emoji: "🍎", + aliases: ["apple"], + tags: [], + category: "Food & Drink", + description: "red apple", + unicode_version: "6.0", + }, + { + emoji: "🍏", + aliases: ["green_apple"], + tags: ["fruit"], + category: "Food & Drink", + description: "green apple", + unicode_version: "6.0", + }, + { + emoji: "🍐", + aliases: ["pear"], + tags: [], + category: "Food & Drink", + description: "pear", + unicode_version: "6.0", + }, + { + emoji: "🍑", + aliases: ["peach"], + tags: [], + category: "Food & Drink", + description: "peach", + unicode_version: "6.0", + }, + { + emoji: "🍒", + aliases: ["cherries"], + tags: ["fruit"], + category: "Food & Drink", + description: "cherries", + unicode_version: "6.0", + }, + { + emoji: "🍓", + aliases: ["strawberry"], + tags: ["fruit"], + category: "Food & Drink", + description: "strawberry", + unicode_version: "6.0", + }, + { + emoji: "🫐", + aliases: ["blueberries"], + tags: [], + category: "Food & Drink", + description: "blueberries", + unicode_version: "13.0", + }, + { + emoji: "🥝", + aliases: ["kiwi_fruit"], + tags: [], + category: "Food & Drink", + description: "kiwi fruit", + unicode_version: "9.0", + }, + { + emoji: "🍅", + aliases: ["tomato"], + tags: [], + category: "Food & Drink", + description: "tomato", + unicode_version: "6.0", + }, + { + emoji: "🫒", + aliases: ["olive"], + tags: [], + category: "Food & Drink", + description: "olive", + unicode_version: "13.0", + }, + { + emoji: "🥥", + aliases: ["coconut"], + tags: [], + category: "Food & Drink", + description: "coconut", + unicode_version: "11.0", + }, + { + emoji: "🥑", + aliases: ["avocado"], + tags: [], + category: "Food & Drink", + description: "avocado", + unicode_version: "9.0", + }, + { + emoji: "🍆", + aliases: ["eggplant"], + tags: ["aubergine"], + category: "Food & Drink", + description: "eggplant", + unicode_version: "6.0", + }, + { + emoji: "🥔", + aliases: ["potato"], + tags: [], + category: "Food & Drink", + description: "potato", + unicode_version: "9.0", + }, + { + emoji: "🥕", + aliases: ["carrot"], + tags: [], + category: "Food & Drink", + description: "carrot", + unicode_version: "9.0", + }, + { + emoji: "🌽", + aliases: ["corn"], + tags: [], + category: "Food & Drink", + description: "ear of corn", + unicode_version: "6.0", + }, + { + emoji: "🌶️", + aliases: ["hot_pepper"], + tags: ["spicy"], + category: "Food & Drink", + description: "hot pepper", + unicode_version: "7.0", + }, + { + emoji: "🫑", + aliases: ["bell_pepper"], + tags: [], + category: "Food & Drink", + description: "bell pepper", + unicode_version: "13.0", + }, + { + emoji: "🥒", + aliases: ["cucumber"], + tags: [], + category: "Food & Drink", + description: "cucumber", + unicode_version: "9.0", + }, + { + emoji: "🥬", + aliases: ["leafy_green"], + tags: [], + category: "Food & Drink", + description: "leafy green", + unicode_version: "11.0", + }, + { + emoji: "🥦", + aliases: ["broccoli"], + tags: [], + category: "Food & Drink", + description: "broccoli", + unicode_version: "11.0", + }, + { + emoji: "🧄", + aliases: ["garlic"], + tags: [], + category: "Food & Drink", + description: "garlic", + unicode_version: "12.0", + }, + { + emoji: "🧅", + aliases: ["onion"], + tags: [], + category: "Food & Drink", + description: "onion", + unicode_version: "12.0", + }, + { + emoji: "🍄", + aliases: ["mushroom"], + tags: [], + category: "Food & Drink", + description: "mushroom", + unicode_version: "6.0", + }, + { + emoji: "🥜", + aliases: ["peanuts"], + tags: [], + category: "Food & Drink", + description: "peanuts", + unicode_version: "9.0", + }, + { + emoji: "🌰", + aliases: ["chestnut"], + tags: [], + category: "Food & Drink", + description: "chestnut", + unicode_version: "6.0", + }, + { + emoji: "🍞", + aliases: ["bread"], + tags: ["toast"], + category: "Food & Drink", + description: "bread", + unicode_version: "6.0", + }, + { + emoji: "🥐", + aliases: ["croissant"], + tags: [], + category: "Food & Drink", + description: "croissant", + unicode_version: "9.0", + }, + { + emoji: "🥖", + aliases: ["baguette_bread"], + tags: [], + category: "Food & Drink", + description: "baguette bread", + unicode_version: "9.0", + }, + { + emoji: "🫓", + aliases: ["flatbread"], + tags: [], + category: "Food & Drink", + description: "flatbread", + unicode_version: "13.0", + }, + { + emoji: "🥨", + aliases: ["pretzel"], + tags: [], + category: "Food & Drink", + description: "pretzel", + unicode_version: "11.0", + }, + { + emoji: "🥯", + aliases: ["bagel"], + tags: [], + category: "Food & Drink", + description: "bagel", + unicode_version: "11.0", + }, + { + emoji: "🥞", + aliases: ["pancakes"], + tags: [], + category: "Food & Drink", + description: "pancakes", + unicode_version: "9.0", + }, + { + emoji: "🧇", + aliases: ["waffle"], + tags: [], + category: "Food & Drink", + description: "waffle", + unicode_version: "12.0", + }, + { + emoji: "🧀", + aliases: ["cheese"], + tags: [], + category: "Food & Drink", + description: "cheese wedge", + unicode_version: "8.0", + }, + { + emoji: "🍖", + aliases: ["meat_on_bone"], + tags: [], + category: "Food & Drink", + description: "meat on bone", + unicode_version: "6.0", + }, + { + emoji: "🍗", + aliases: ["poultry_leg"], + tags: ["meat", "chicken"], + category: "Food & Drink", + description: "poultry leg", + unicode_version: "6.0", + }, + { + emoji: "🥩", + aliases: ["cut_of_meat"], + tags: [], + category: "Food & Drink", + description: "cut of meat", + unicode_version: "11.0", + }, + { + emoji: "🥓", + aliases: ["bacon"], + tags: [], + category: "Food & Drink", + description: "bacon", + unicode_version: "9.0", + }, + { + emoji: "🍔", + aliases: ["hamburger"], + tags: ["burger"], + category: "Food & Drink", + description: "hamburger", + unicode_version: "6.0", + }, + { + emoji: "🍟", + aliases: ["fries"], + tags: [], + category: "Food & Drink", + description: "french fries", + unicode_version: "6.0", + }, + { + emoji: "🍕", + aliases: ["pizza"], + tags: [], + category: "Food & Drink", + description: "pizza", + unicode_version: "6.0", + }, + { + emoji: "🌭", + aliases: ["hotdog"], + tags: [], + category: "Food & Drink", + description: "hot dog", + unicode_version: "8.0", + }, + { + emoji: "🥪", + aliases: ["sandwich"], + tags: [], + category: "Food & Drink", + description: "sandwich", + unicode_version: "11.0", + }, + { + emoji: "🌮", + aliases: ["taco"], + tags: [], + category: "Food & Drink", + description: "taco", + unicode_version: "8.0", + }, + { + emoji: "🌯", + aliases: ["burrito"], + tags: [], + category: "Food & Drink", + description: "burrito", + unicode_version: "8.0", + }, + { + emoji: "🫔", + aliases: ["tamale"], + tags: [], + category: "Food & Drink", + description: "tamale", + unicode_version: "13.0", + }, + { + emoji: "🥙", + aliases: ["stuffed_flatbread"], + tags: [], + category: "Food & Drink", + description: "stuffed flatbread", + unicode_version: "9.0", + }, + { + emoji: "🧆", + aliases: ["falafel"], + tags: [], + category: "Food & Drink", + description: "falafel", + unicode_version: "12.0", + }, + { + emoji: "🥚", + aliases: ["egg"], + tags: [], + category: "Food & Drink", + description: "egg", + unicode_version: "9.0", + }, + { + emoji: "🍳", + aliases: ["fried_egg"], + tags: ["breakfast"], + category: "Food & Drink", + description: "cooking", + unicode_version: "6.0", + }, + { + emoji: "🥘", + aliases: ["shallow_pan_of_food"], + tags: ["paella", "curry"], + category: "Food & Drink", + description: "shallow pan of food", + unicode_version: "", + }, + { + emoji: "🍲", + aliases: ["stew"], + tags: [], + category: "Food & Drink", + description: "pot of food", + unicode_version: "6.0", + }, + { + emoji: "🫕", + aliases: ["fondue"], + tags: [], + category: "Food & Drink", + description: "fondue", + unicode_version: "13.0", + }, + { + emoji: "🥣", + aliases: ["bowl_with_spoon"], + tags: [], + category: "Food & Drink", + description: "bowl with spoon", + unicode_version: "11.0", + }, + { + emoji: "🥗", + aliases: ["green_salad"], + tags: [], + category: "Food & Drink", + description: "green salad", + unicode_version: "9.0", + }, + { + emoji: "🍿", + aliases: ["popcorn"], + tags: [], + category: "Food & Drink", + description: "popcorn", + unicode_version: "8.0", + }, + { + emoji: "🧈", + aliases: ["butter"], + tags: [], + category: "Food & Drink", + description: "butter", + unicode_version: "12.0", + }, + { + emoji: "🧂", + aliases: ["salt"], + tags: [], + category: "Food & Drink", + description: "salt", + unicode_version: "11.0", + }, + { + emoji: "🥫", + aliases: ["canned_food"], + tags: [], + category: "Food & Drink", + description: "canned food", + unicode_version: "11.0", + }, + { + emoji: "🍱", + aliases: ["bento"], + tags: [], + category: "Food & Drink", + description: "bento box", + unicode_version: "6.0", + }, + { + emoji: "🍘", + aliases: ["rice_cracker"], + tags: [], + category: "Food & Drink", + description: "rice cracker", + unicode_version: "6.0", + }, + { + emoji: "🍙", + aliases: ["rice_ball"], + tags: [], + category: "Food & Drink", + description: "rice ball", + unicode_version: "6.0", + }, + { + emoji: "🍚", + aliases: ["rice"], + tags: [], + category: "Food & Drink", + description: "cooked rice", + unicode_version: "6.0", + }, + { + emoji: "🍛", + aliases: ["curry"], + tags: [], + category: "Food & Drink", + description: "curry rice", + unicode_version: "6.0", + }, + { + emoji: "🍜", + aliases: ["ramen"], + tags: ["noodle"], + category: "Food & Drink", + description: "steaming bowl", + unicode_version: "6.0", + }, + { + emoji: "🍝", + aliases: ["spaghetti"], + tags: ["pasta"], + category: "Food & Drink", + description: "spaghetti", + unicode_version: "6.0", + }, + { + emoji: "🍠", + aliases: ["sweet_potato"], + tags: [], + category: "Food & Drink", + description: "roasted sweet potato", + unicode_version: "6.0", + }, + { + emoji: "🍢", + aliases: ["oden"], + tags: [], + category: "Food & Drink", + description: "oden", + unicode_version: "6.0", + }, + { + emoji: "🍣", + aliases: ["sushi"], + tags: [], + category: "Food & Drink", + description: "sushi", + unicode_version: "6.0", + }, + { + emoji: "🍤", + aliases: ["fried_shrimp"], + tags: ["tempura"], + category: "Food & Drink", + description: "fried shrimp", + unicode_version: "6.0", + }, + { + emoji: "🍥", + aliases: ["fish_cake"], + tags: [], + category: "Food & Drink", + description: "fish cake with swirl", + unicode_version: "6.0", + }, + { + emoji: "🥮", + aliases: ["moon_cake"], + tags: [], + category: "Food & Drink", + description: "moon cake", + unicode_version: "11.0", + }, + { + emoji: "🍡", + aliases: ["dango"], + tags: [], + category: "Food & Drink", + description: "dango", + unicode_version: "6.0", + }, + { + emoji: "🥟", + aliases: ["dumpling"], + tags: [], + category: "Food & Drink", + description: "dumpling", + unicode_version: "11.0", + }, + { + emoji: "🥠", + aliases: ["fortune_cookie"], + tags: [], + category: "Food & Drink", + description: "fortune cookie", + unicode_version: "11.0", + }, + { + emoji: "🥡", + aliases: ["takeout_box"], + tags: [], + category: "Food & Drink", + description: "takeout box", + unicode_version: "11.0", + }, + { + emoji: "🦀", + aliases: ["crab"], + tags: [], + category: "Food & Drink", + description: "crab", + unicode_version: "8.0", + }, + { + emoji: "🦞", + aliases: ["lobster"], + tags: [], + category: "Food & Drink", + description: "lobster", + unicode_version: "11.0", + }, + { + emoji: "🦐", + aliases: ["shrimp"], + tags: [], + category: "Food & Drink", + description: "shrimp", + unicode_version: "9.0", + }, + { + emoji: "🦑", + aliases: ["squid"], + tags: [], + category: "Food & Drink", + description: "squid", + unicode_version: "9.0", + }, + { + emoji: "🦪", + aliases: ["oyster"], + tags: [], + category: "Food & Drink", + description: "oyster", + unicode_version: "12.0", + }, + { + emoji: "🍦", + aliases: ["icecream"], + tags: [], + category: "Food & Drink", + description: "soft ice cream", + unicode_version: "6.0", + }, + { + emoji: "🍧", + aliases: ["shaved_ice"], + tags: [], + category: "Food & Drink", + description: "shaved ice", + unicode_version: "6.0", + }, + { + emoji: "🍨", + aliases: ["ice_cream"], + tags: [], + category: "Food & Drink", + description: "ice cream", + unicode_version: "6.0", + }, + { + emoji: "🍩", + aliases: ["doughnut"], + tags: [], + category: "Food & Drink", + description: "doughnut", + unicode_version: "6.0", + }, + { + emoji: "🍪", + aliases: ["cookie"], + tags: [], + category: "Food & Drink", + description: "cookie", + unicode_version: "6.0", + }, + { + emoji: "🎂", + aliases: ["birthday"], + tags: ["party"], + category: "Food & Drink", + description: "birthday cake", + unicode_version: "6.0", + }, + { + emoji: "🍰", + aliases: ["cake"], + tags: ["dessert"], + category: "Food & Drink", + description: "shortcake", + unicode_version: "6.0", + }, + { + emoji: "🧁", + aliases: ["cupcake"], + tags: [], + category: "Food & Drink", + description: "cupcake", + unicode_version: "11.0", + }, + { + emoji: "🥧", + aliases: ["pie"], + tags: [], + category: "Food & Drink", + description: "pie", + unicode_version: "11.0", + }, + { + emoji: "🍫", + aliases: ["chocolate_bar"], + tags: [], + category: "Food & Drink", + description: "chocolate bar", + unicode_version: "6.0", + }, + { + emoji: "🍬", + aliases: ["candy"], + tags: ["sweet"], + category: "Food & Drink", + description: "candy", + unicode_version: "6.0", + }, + { + emoji: "🍭", + aliases: ["lollipop"], + tags: [], + category: "Food & Drink", + description: "lollipop", + unicode_version: "6.0", + }, + { + emoji: "🍮", + aliases: ["custard"], + tags: [], + category: "Food & Drink", + description: "custard", + unicode_version: "6.0", + }, + { + emoji: "🍯", + aliases: ["honey_pot"], + tags: [], + category: "Food & Drink", + description: "honey pot", + unicode_version: "6.0", + }, + { + emoji: "🍼", + aliases: ["baby_bottle"], + tags: ["milk"], + category: "Food & Drink", + description: "baby bottle", + unicode_version: "6.0", + }, + { + emoji: "🥛", + aliases: ["milk_glass"], + tags: [], + category: "Food & Drink", + description: "glass of milk", + unicode_version: "9.0", + }, + { + emoji: "☕", + aliases: ["coffee"], + tags: ["cafe", "espresso"], + category: "Food & Drink", + description: "hot beverage", + unicode_version: "4.0", + }, + { + emoji: "🫖", + aliases: ["teapot"], + tags: [], + category: "Food & Drink", + description: "teapot", + unicode_version: "13.0", + }, + { + emoji: "🍵", + aliases: ["tea"], + tags: ["green", "breakfast"], + category: "Food & Drink", + description: "teacup without handle", + unicode_version: "6.0", + }, + { + emoji: "🍶", + aliases: ["sake"], + tags: [], + category: "Food & Drink", + description: "sake", + unicode_version: "6.0", + }, + { + emoji: "🍾", + aliases: ["champagne"], + tags: ["bottle", "bubbly", "celebration"], + category: "Food & Drink", + description: "bottle with popping cork", + unicode_version: "8.0", + }, + { + emoji: "🍷", + aliases: ["wine_glass"], + tags: [], + category: "Food & Drink", + description: "wine glass", + unicode_version: "6.0", + }, + { + emoji: "🍸", + aliases: ["cocktail"], + tags: ["drink"], + category: "Food & Drink", + description: "cocktail glass", + unicode_version: "6.0", + }, + { + emoji: "🍹", + aliases: ["tropical_drink"], + tags: ["summer", "vacation"], + category: "Food & Drink", + description: "tropical drink", + unicode_version: "6.0", + }, + { + emoji: "🍺", + aliases: ["beer"], + tags: ["drink"], + category: "Food & Drink", + description: "beer mug", + unicode_version: "6.0", + }, + { + emoji: "🍻", + aliases: ["beers"], + tags: ["drinks"], + category: "Food & Drink", + description: "clinking beer mugs", + unicode_version: "6.0", + }, + { + emoji: "🥂", + aliases: ["clinking_glasses"], + tags: ["cheers", "toast"], + category: "Food & Drink", + description: "clinking glasses", + unicode_version: "9.0", + }, + { + emoji: "🥃", + aliases: ["tumbler_glass"], + tags: ["whisky"], + category: "Food & Drink", + description: "tumbler glass", + unicode_version: "9.0", + }, + { + emoji: "🥤", + aliases: ["cup_with_straw"], + tags: [], + category: "Food & Drink", + description: "cup with straw", + unicode_version: "11.0", + }, + { + emoji: "🧋", + aliases: ["bubble_tea"], + tags: [], + category: "Food & Drink", + description: "bubble tea", + unicode_version: "13.0", + }, + { + emoji: "🧃", + aliases: ["beverage_box"], + tags: [], + category: "Food & Drink", + description: "beverage box", + unicode_version: "12.0", + }, + { + emoji: "🧉", + aliases: ["mate"], + tags: [], + category: "Food & Drink", + description: "mate", + unicode_version: "12.0", + }, + { + emoji: "🧊", + aliases: ["ice_cube"], + tags: [], + category: "Food & Drink", + description: "ice", + unicode_version: "12.0", + }, + { + emoji: "🥢", + aliases: ["chopsticks"], + tags: [], + category: "Food & Drink", + description: "chopsticks", + unicode_version: "11.0", + }, + { + emoji: "🍽️", + aliases: ["plate_with_cutlery"], + tags: ["dining", "dinner"], + category: "Food & Drink", + description: "fork and knife with plate", + unicode_version: "7.0", + }, + { + emoji: "🍴", + aliases: ["fork_and_knife"], + tags: ["cutlery"], + category: "Food & Drink", + description: "fork and knife", + unicode_version: "6.0", + }, + { + emoji: "🥄", + aliases: ["spoon"], + tags: [], + category: "Food & Drink", + description: "spoon", + unicode_version: "9.0", + }, + { + emoji: "🔪", + aliases: ["hocho", "knife"], + tags: ["cut", "chop"], + category: "Food & Drink", + description: "kitchen knife", + unicode_version: "6.0", + }, + { + emoji: "🏺", + aliases: ["amphora"], + tags: [], + category: "Food & Drink", + description: "amphora", + unicode_version: "8.0", + }, + { + emoji: "🌍", + aliases: ["earth_africa"], + tags: ["globe", "world", "international"], + category: "Travel & Places", + description: "globe showing Europe-Africa", + unicode_version: "6.0", + }, + { + emoji: "🌎", + aliases: ["earth_americas"], + tags: ["globe", "world", "international"], + category: "Travel & Places", + description: "globe showing Americas", + unicode_version: "6.0", + }, + { + emoji: "🌏", + aliases: ["earth_asia"], + tags: ["globe", "world", "international"], + category: "Travel & Places", + description: "globe showing Asia-Australia", + unicode_version: "6.0", + }, + { + emoji: "🌐", + aliases: ["globe_with_meridians"], + tags: ["world", "global", "international"], + category: "Travel & Places", + description: "globe with meridians", + unicode_version: "6.0", + }, + { + emoji: "🗺️", + aliases: ["world_map"], + tags: ["travel"], + category: "Travel & Places", + description: "world map", + unicode_version: "7.0", + }, + { + emoji: "🗾", + aliases: ["japan"], + tags: [], + category: "Travel & Places", + description: "map of Japan", + unicode_version: "6.0", + }, + { + emoji: "🧭", + aliases: ["compass"], + tags: [], + category: "Travel & Places", + description: "compass", + unicode_version: "11.0", + }, + { + emoji: "🏔️", + aliases: ["mountain_snow"], + tags: [], + category: "Travel & Places", + description: "snow-capped mountain", + unicode_version: "7.0", + }, + { + emoji: "⛰️", + aliases: ["mountain"], + tags: [], + category: "Travel & Places", + description: "mountain", + unicode_version: "5.2", + }, + { + emoji: "🌋", + aliases: ["volcano"], + tags: [], + category: "Travel & Places", + description: "volcano", + unicode_version: "6.0", + }, + { + emoji: "🗻", + aliases: ["mount_fuji"], + tags: [], + category: "Travel & Places", + description: "mount fuji", + unicode_version: "6.0", + }, + { + emoji: "🏕️", + aliases: ["camping"], + tags: [], + category: "Travel & Places", + description: "camping", + unicode_version: "7.0", + }, + { + emoji: "🏖️", + aliases: ["beach_umbrella"], + tags: [], + category: "Travel & Places", + description: "beach with umbrella", + unicode_version: "7.0", + }, + { + emoji: "🏜️", + aliases: ["desert"], + tags: [], + category: "Travel & Places", + description: "desert", + unicode_version: "7.0", + }, + { + emoji: "🏝️", + aliases: ["desert_island"], + tags: [], + category: "Travel & Places", + description: "desert island", + unicode_version: "7.0", + }, + { + emoji: "🏞️", + aliases: ["national_park"], + tags: [], + category: "Travel & Places", + description: "national park", + unicode_version: "7.0", + }, + { + emoji: "🏟️", + aliases: ["stadium"], + tags: [], + category: "Travel & Places", + description: "stadium", + unicode_version: "7.0", + }, + { + emoji: "🏛️", + aliases: ["classical_building"], + tags: [], + category: "Travel & Places", + description: "classical building", + unicode_version: "7.0", + }, + { + emoji: "🏗️", + aliases: ["building_construction"], + tags: [], + category: "Travel & Places", + description: "building construction", + unicode_version: "7.0", + }, + { + emoji: "🧱", + aliases: ["bricks"], + tags: [], + category: "Travel & Places", + description: "brick", + unicode_version: "11.0", + }, + { + emoji: "🪨", + aliases: ["rock"], + tags: [], + category: "Travel & Places", + description: "rock", + unicode_version: "13.0", + }, + { + emoji: "🪵", + aliases: ["wood"], + tags: [], + category: "Travel & Places", + description: "wood", + unicode_version: "13.0", + }, + { + emoji: "🛖", + aliases: ["hut"], + tags: [], + category: "Travel & Places", + description: "hut", + unicode_version: "13.0", + }, + { + emoji: "🏘️", + aliases: ["houses"], + tags: [], + category: "Travel & Places", + description: "houses", + unicode_version: "7.0", + }, + { + emoji: "🏚️", + aliases: ["derelict_house"], + tags: [], + category: "Travel & Places", + description: "derelict house", + unicode_version: "7.0", + }, + { + emoji: "🏠", + aliases: ["house"], + tags: [], + category: "Travel & Places", + description: "house", + unicode_version: "6.0", + }, + { + emoji: "🏡", + aliases: ["house_with_garden"], + tags: [], + category: "Travel & Places", + description: "house with garden", + unicode_version: "6.0", + }, + { + emoji: "🏢", + aliases: ["office"], + tags: [], + category: "Travel & Places", + description: "office building", + unicode_version: "6.0", + }, + { + emoji: "🏣", + aliases: ["post_office"], + tags: [], + category: "Travel & Places", + description: "Japanese post office", + unicode_version: "6.0", + }, + { + emoji: "🏤", + aliases: ["european_post_office"], + tags: [], + category: "Travel & Places", + description: "post office", + unicode_version: "6.0", + }, + { + emoji: "🏥", + aliases: ["hospital"], + tags: [], + category: "Travel & Places", + description: "hospital", + unicode_version: "6.0", + }, + { + emoji: "🏦", + aliases: ["bank"], + tags: [], + category: "Travel & Places", + description: "bank", + unicode_version: "6.0", + }, + { + emoji: "🏨", + aliases: ["hotel"], + tags: [], + category: "Travel & Places", + description: "hotel", + unicode_version: "6.0", + }, + { + emoji: "🏩", + aliases: ["love_hotel"], + tags: [], + category: "Travel & Places", + description: "love hotel", + unicode_version: "6.0", + }, + { + emoji: "🏪", + aliases: ["convenience_store"], + tags: [], + category: "Travel & Places", + description: "convenience store", + unicode_version: "6.0", + }, + { + emoji: "🏫", + aliases: ["school"], + tags: [], + category: "Travel & Places", + description: "school", + unicode_version: "6.0", + }, + { + emoji: "🏬", + aliases: ["department_store"], + tags: [], + category: "Travel & Places", + description: "department store", + unicode_version: "6.0", + }, + { + emoji: "🏭", + aliases: ["factory"], + tags: [], + category: "Travel & Places", + description: "factory", + unicode_version: "6.0", + }, + { + emoji: "🏯", + aliases: ["japanese_castle"], + tags: [], + category: "Travel & Places", + description: "Japanese castle", + unicode_version: "6.0", + }, + { + emoji: "🏰", + aliases: ["european_castle"], + tags: [], + category: "Travel & Places", + description: "castle", + unicode_version: "6.0", + }, + { + emoji: "💒", + aliases: ["wedding"], + tags: ["marriage"], + category: "Travel & Places", + description: "wedding", + unicode_version: "6.0", + }, + { + emoji: "🗼", + aliases: ["tokyo_tower"], + tags: [], + category: "Travel & Places", + description: "Tokyo tower", + unicode_version: "6.0", + }, + { + emoji: "🗽", + aliases: ["statue_of_liberty"], + tags: [], + category: "Travel & Places", + description: "Statue of Liberty", + unicode_version: "6.0", + }, + { + emoji: "⛪", + aliases: ["church"], + tags: [], + category: "Travel & Places", + description: "church", + unicode_version: "5.2", + }, + { + emoji: "🕌", + aliases: ["mosque"], + tags: [], + category: "Travel & Places", + description: "mosque", + unicode_version: "8.0", + }, + { + emoji: "🛕", + aliases: ["hindu_temple"], + tags: [], + category: "Travel & Places", + description: "hindu temple", + unicode_version: "12.0", + }, + { + emoji: "🕍", + aliases: ["synagogue"], + tags: [], + category: "Travel & Places", + description: "synagogue", + unicode_version: "8.0", + }, + { + emoji: "⛩️", + aliases: ["shinto_shrine"], + tags: [], + category: "Travel & Places", + description: "shinto shrine", + unicode_version: "5.2", + }, + { + emoji: "🕋", + aliases: ["kaaba"], + tags: [], + category: "Travel & Places", + description: "kaaba", + unicode_version: "8.0", + }, + { + emoji: "⛲", + aliases: ["fountain"], + tags: [], + category: "Travel & Places", + description: "fountain", + unicode_version: "5.2", + }, + { + emoji: "⛺", + aliases: ["tent"], + tags: ["camping"], + category: "Travel & Places", + description: "tent", + unicode_version: "5.2", + }, + { + emoji: "🌁", + aliases: ["foggy"], + tags: ["karl"], + category: "Travel & Places", + description: "foggy", + unicode_version: "6.0", + }, + { + emoji: "🌃", + aliases: ["night_with_stars"], + tags: [], + category: "Travel & Places", + description: "night with stars", + unicode_version: "6.0", + }, + { + emoji: "🏙️", + aliases: ["cityscape"], + tags: ["skyline"], + category: "Travel & Places", + description: "cityscape", + unicode_version: "7.0", + }, + { + emoji: "🌄", + aliases: ["sunrise_over_mountains"], + tags: [], + category: "Travel & Places", + description: "sunrise over mountains", + unicode_version: "6.0", + }, + { + emoji: "🌅", + aliases: ["sunrise"], + tags: [], + category: "Travel & Places", + description: "sunrise", + unicode_version: "6.0", + }, + { + emoji: "🌆", + aliases: ["city_sunset"], + tags: [], + category: "Travel & Places", + description: "cityscape at dusk", + unicode_version: "6.0", + }, + { + emoji: "🌇", + aliases: ["city_sunrise"], + tags: [], + category: "Travel & Places", + description: "sunset", + unicode_version: "6.0", + }, + { + emoji: "🌉", + aliases: ["bridge_at_night"], + tags: [], + category: "Travel & Places", + description: "bridge at night", + unicode_version: "6.0", + }, + { + emoji: "♨️", + aliases: ["hotsprings"], + tags: [], + category: "Travel & Places", + description: "hot springs", + unicode_version: "", + }, + { + emoji: "🎠", + aliases: ["carousel_horse"], + tags: [], + category: "Travel & Places", + description: "carousel horse", + unicode_version: "6.0", + }, + { + emoji: "🎡", + aliases: ["ferris_wheel"], + tags: [], + category: "Travel & Places", + description: "ferris wheel", + unicode_version: "6.0", + }, + { + emoji: "🎢", + aliases: ["roller_coaster"], + tags: [], + category: "Travel & Places", + description: "roller coaster", + unicode_version: "6.0", + }, + { + emoji: "💈", + aliases: ["barber"], + tags: [], + category: "Travel & Places", + description: "barber pole", + unicode_version: "6.0", + }, + { + emoji: "🎪", + aliases: ["circus_tent"], + tags: [], + category: "Travel & Places", + description: "circus tent", + unicode_version: "6.0", + }, + { + emoji: "🚂", + aliases: ["steam_locomotive"], + tags: ["train"], + category: "Travel & Places", + description: "locomotive", + unicode_version: "6.0", + }, + { + emoji: "🚃", + aliases: ["railway_car"], + tags: [], + category: "Travel & Places", + description: "railway car", + unicode_version: "6.0", + }, + { + emoji: "🚄", + aliases: ["bullettrain_side"], + tags: ["train"], + category: "Travel & Places", + description: "high-speed train", + unicode_version: "6.0", + }, + { + emoji: "🚅", + aliases: ["bullettrain_front"], + tags: ["train"], + category: "Travel & Places", + description: "bullet train", + unicode_version: "6.0", + }, + { + emoji: "🚆", + aliases: ["train2"], + tags: [], + category: "Travel & Places", + description: "train", + unicode_version: "6.0", + }, + { + emoji: "🚇", + aliases: ["metro"], + tags: [], + category: "Travel & Places", + description: "metro", + unicode_version: "6.0", + }, + { + emoji: "🚈", + aliases: ["light_rail"], + tags: [], + category: "Travel & Places", + description: "light rail", + unicode_version: "6.0", + }, + { + emoji: "🚉", + aliases: ["station"], + tags: [], + category: "Travel & Places", + description: "station", + unicode_version: "6.0", + }, + { + emoji: "🚊", + aliases: ["tram"], + tags: [], + category: "Travel & Places", + description: "tram", + unicode_version: "6.0", + }, + { + emoji: "🚝", + aliases: ["monorail"], + tags: [], + category: "Travel & Places", + description: "monorail", + unicode_version: "6.0", + }, + { + emoji: "🚞", + aliases: ["mountain_railway"], + tags: [], + category: "Travel & Places", + description: "mountain railway", + unicode_version: "6.0", + }, + { + emoji: "🚋", + aliases: ["train"], + tags: [], + category: "Travel & Places", + description: "tram car", + unicode_version: "6.0", + }, + { + emoji: "🚌", + aliases: ["bus"], + tags: [], + category: "Travel & Places", + description: "bus", + unicode_version: "6.0", + }, + { + emoji: "🚍", + aliases: ["oncoming_bus"], + tags: [], + category: "Travel & Places", + description: "oncoming bus", + unicode_version: "6.0", + }, + { + emoji: "🚎", + aliases: ["trolleybus"], + tags: [], + category: "Travel & Places", + description: "trolleybus", + unicode_version: "6.0", + }, + { + emoji: "🚐", + aliases: ["minibus"], + tags: [], + category: "Travel & Places", + description: "minibus", + unicode_version: "6.0", + }, + { + emoji: "🚑", + aliases: ["ambulance"], + tags: [], + category: "Travel & Places", + description: "ambulance", + unicode_version: "6.0", + }, + { + emoji: "🚒", + aliases: ["fire_engine"], + tags: [], + category: "Travel & Places", + description: "fire engine", + unicode_version: "6.0", + }, + { + emoji: "🚓", + aliases: ["police_car"], + tags: [], + category: "Travel & Places", + description: "police car", + unicode_version: "6.0", + }, + { + emoji: "🚔", + aliases: ["oncoming_police_car"], + tags: [], + category: "Travel & Places", + description: "oncoming police car", + unicode_version: "6.0", + }, + { + emoji: "🚕", + aliases: ["taxi"], + tags: [], + category: "Travel & Places", + description: "taxi", + unicode_version: "6.0", + }, + { + emoji: "🚖", + aliases: ["oncoming_taxi"], + tags: [], + category: "Travel & Places", + description: "oncoming taxi", + unicode_version: "6.0", + }, + { + emoji: "🚗", + aliases: ["car", "red_car"], + tags: [], + category: "Travel & Places", + description: "automobile", + unicode_version: "6.0", + }, + { + emoji: "🚘", + aliases: ["oncoming_automobile"], + tags: [], + category: "Travel & Places", + description: "oncoming automobile", + unicode_version: "6.0", + }, + { + emoji: "🚙", + aliases: ["blue_car"], + tags: [], + category: "Travel & Places", + description: "sport utility vehicle", + unicode_version: "6.0", + }, + { + emoji: "🛻", + aliases: ["pickup_truck"], + tags: [], + category: "Travel & Places", + description: "pickup truck", + unicode_version: "13.0", + }, + { + emoji: "🚚", + aliases: ["truck"], + tags: [], + category: "Travel & Places", + description: "delivery truck", + unicode_version: "6.0", + }, + { + emoji: "🚛", + aliases: ["articulated_lorry"], + tags: [], + category: "Travel & Places", + description: "articulated lorry", + unicode_version: "6.0", + }, + { + emoji: "🚜", + aliases: ["tractor"], + tags: [], + category: "Travel & Places", + description: "tractor", + unicode_version: "6.0", + }, + { + emoji: "🏎️", + aliases: ["racing_car"], + tags: [], + category: "Travel & Places", + description: "racing car", + unicode_version: "7.0", + }, + { + emoji: "🏍️", + aliases: ["motorcycle"], + tags: [], + category: "Travel & Places", + description: "motorcycle", + unicode_version: "7.0", + }, + { + emoji: "🛵", + aliases: ["motor_scooter"], + tags: [], + category: "Travel & Places", + description: "motor scooter", + unicode_version: "9.0", + }, + { + emoji: "🦽", + aliases: ["manual_wheelchair"], + tags: [], + category: "Travel & Places", + description: "manual wheelchair", + unicode_version: "12.0", + }, + { + emoji: "🦼", + aliases: ["motorized_wheelchair"], + tags: [], + category: "Travel & Places", + description: "motorized wheelchair", + unicode_version: "12.0", + }, + { + emoji: "🛺", + aliases: ["auto_rickshaw"], + tags: [], + category: "Travel & Places", + description: "auto rickshaw", + unicode_version: "12.0", + }, + { + emoji: "🚲", + aliases: ["bike"], + tags: ["bicycle"], + category: "Travel & Places", + description: "bicycle", + unicode_version: "6.0", + }, + { + emoji: "🛴", + aliases: ["kick_scooter"], + tags: [], + category: "Travel & Places", + description: "kick scooter", + unicode_version: "9.0", + }, + { + emoji: "🛹", + aliases: ["skateboard"], + tags: [], + category: "Travel & Places", + description: "skateboard", + unicode_version: "11.0", + }, + { + emoji: "🛼", + aliases: ["roller_skate"], + tags: [], + category: "Travel & Places", + description: "roller skate", + unicode_version: "13.0", + }, + { + emoji: "🚏", + aliases: ["busstop"], + tags: [], + category: "Travel & Places", + description: "bus stop", + unicode_version: "6.0", + }, + { + emoji: "🛣️", + aliases: ["motorway"], + tags: [], + category: "Travel & Places", + description: "motorway", + unicode_version: "7.0", + }, + { + emoji: "🛤️", + aliases: ["railway_track"], + tags: [], + category: "Travel & Places", + description: "railway track", + unicode_version: "7.0", + }, + { + emoji: "🛢️", + aliases: ["oil_drum"], + tags: [], + category: "Travel & Places", + description: "oil drum", + unicode_version: "7.0", + }, + { + emoji: "⛽", + aliases: ["fuelpump"], + tags: [], + category: "Travel & Places", + description: "fuel pump", + unicode_version: "5.2", + }, + { + emoji: "🚨", + aliases: ["rotating_light"], + tags: ["911", "emergency"], + category: "Travel & Places", + description: "police car light", + unicode_version: "6.0", + }, + { + emoji: "🚥", + aliases: ["traffic_light"], + tags: [], + category: "Travel & Places", + description: "horizontal traffic light", + unicode_version: "6.0", + }, + { + emoji: "🚦", + aliases: ["vertical_traffic_light"], + tags: ["semaphore"], + category: "Travel & Places", + description: "vertical traffic light", + unicode_version: "6.0", + }, + { + emoji: "🛑", + aliases: ["stop_sign"], + tags: [], + category: "Travel & Places", + description: "stop sign", + unicode_version: "9.0", + }, + { + emoji: "🚧", + aliases: ["construction"], + tags: ["wip"], + category: "Travel & Places", + description: "construction", + unicode_version: "6.0", + }, + { + emoji: "⚓", + aliases: ["anchor"], + tags: ["ship"], + category: "Travel & Places", + description: "anchor", + unicode_version: "4.1", + }, + { + emoji: "⛵", + aliases: ["boat", "sailboat"], + tags: [], + category: "Travel & Places", + description: "sailboat", + unicode_version: "5.2", + }, + { + emoji: "🛶", + aliases: ["canoe"], + tags: [], + category: "Travel & Places", + description: "canoe", + unicode_version: "9.0", + }, + { + emoji: "🚤", + aliases: ["speedboat"], + tags: ["ship"], + category: "Travel & Places", + description: "speedboat", + unicode_version: "6.0", + }, + { + emoji: "🛳️", + aliases: ["passenger_ship"], + tags: ["cruise"], + category: "Travel & Places", + description: "passenger ship", + unicode_version: "7.0", + }, + { + emoji: "⛴️", + aliases: ["ferry"], + tags: [], + category: "Travel & Places", + description: "ferry", + unicode_version: "5.2", + }, + { + emoji: "🛥️", + aliases: ["motor_boat"], + tags: [], + category: "Travel & Places", + description: "motor boat", + unicode_version: "7.0", + }, + { + emoji: "🚢", + aliases: ["ship"], + tags: [], + category: "Travel & Places", + description: "ship", + unicode_version: "6.0", + }, + { + emoji: "✈️", + aliases: ["airplane"], + tags: ["flight"], + category: "Travel & Places", + description: "airplane", + unicode_version: "", + }, + { + emoji: "🛩️", + aliases: ["small_airplane"], + tags: ["flight"], + category: "Travel & Places", + description: "small airplane", + unicode_version: "7.0", + }, + { + emoji: "🛫", + aliases: ["flight_departure"], + tags: [], + category: "Travel & Places", + description: "airplane departure", + unicode_version: "7.0", + }, + { + emoji: "🛬", + aliases: ["flight_arrival"], + tags: [], + category: "Travel & Places", + description: "airplane arrival", + unicode_version: "7.0", + }, + { + emoji: "🪂", + aliases: ["parachute"], + tags: [], + category: "Travel & Places", + description: "parachute", + unicode_version: "12.0", + }, + { + emoji: "💺", + aliases: ["seat"], + tags: [], + category: "Travel & Places", + description: "seat", + unicode_version: "6.0", + }, + { + emoji: "🚁", + aliases: ["helicopter"], + tags: [], + category: "Travel & Places", + description: "helicopter", + unicode_version: "6.0", + }, + { + emoji: "🚟", + aliases: ["suspension_railway"], + tags: [], + category: "Travel & Places", + description: "suspension railway", + unicode_version: "6.0", + }, + { + emoji: "🚠", + aliases: ["mountain_cableway"], + tags: [], + category: "Travel & Places", + description: "mountain cableway", + unicode_version: "6.0", + }, + { + emoji: "🚡", + aliases: ["aerial_tramway"], + tags: [], + category: "Travel & Places", + description: "aerial tramway", + unicode_version: "6.0", + }, + { + emoji: "🛰️", + aliases: ["artificial_satellite"], + tags: ["orbit", "space"], + category: "Travel & Places", + description: "satellite", + unicode_version: "7.0", + }, + { + emoji: "🚀", + aliases: ["rocket"], + tags: ["ship", "launch"], + category: "Travel & Places", + description: "rocket", + unicode_version: "6.0", + }, + { + emoji: "🛸", + aliases: ["flying_saucer"], + tags: ["ufo"], + category: "Travel & Places", + description: "flying saucer", + unicode_version: "11.0", + }, + { + emoji: "🛎️", + aliases: ["bellhop_bell"], + tags: [], + category: "Travel & Places", + description: "bellhop bell", + unicode_version: "7.0", + }, + { + emoji: "🧳", + aliases: ["luggage"], + tags: [], + category: "Travel & Places", + description: "luggage", + unicode_version: "11.0", + }, + { + emoji: "⌛", + aliases: ["hourglass"], + tags: ["time"], + category: "Travel & Places", + description: "hourglass done", + unicode_version: "", + }, + { + emoji: "⏳", + aliases: ["hourglass_flowing_sand"], + tags: ["time"], + category: "Travel & Places", + description: "hourglass not done", + unicode_version: "6.0", + }, + { + emoji: "⌚", + aliases: ["watch"], + tags: ["time"], + category: "Travel & Places", + description: "watch", + unicode_version: "", + }, + { + emoji: "⏰", + aliases: ["alarm_clock"], + tags: ["morning"], + category: "Travel & Places", + description: "alarm clock", + unicode_version: "6.0", + }, + { + emoji: "⏱️", + aliases: ["stopwatch"], + tags: [], + category: "Travel & Places", + description: "stopwatch", + unicode_version: "6.0", + }, + { + emoji: "⏲️", + aliases: ["timer_clock"], + tags: [], + category: "Travel & Places", + description: "timer clock", + unicode_version: "6.0", + }, + { + emoji: "🕰️", + aliases: ["mantelpiece_clock"], + tags: [], + category: "Travel & Places", + description: "mantelpiece clock", + unicode_version: "7.0", + }, + { + emoji: "🕛", + aliases: ["clock12"], + tags: [], + category: "Travel & Places", + description: "twelve o’clock", + unicode_version: "6.0", + }, + { + emoji: "🕧", + aliases: ["clock1230"], + tags: [], + category: "Travel & Places", + description: "twelve-thirty", + unicode_version: "6.0", + }, + { + emoji: "🕐", + aliases: ["clock1"], + tags: [], + category: "Travel & Places", + description: "one o’clock", + unicode_version: "6.0", + }, + { + emoji: "🕜", + aliases: ["clock130"], + tags: [], + category: "Travel & Places", + description: "one-thirty", + unicode_version: "6.0", + }, + { + emoji: "🕑", + aliases: ["clock2"], + tags: [], + category: "Travel & Places", + description: "two o’clock", + unicode_version: "6.0", + }, + { + emoji: "🕝", + aliases: ["clock230"], + tags: [], + category: "Travel & Places", + description: "two-thirty", + unicode_version: "6.0", + }, + { + emoji: "🕒", + aliases: ["clock3"], + tags: [], + category: "Travel & Places", + description: "three o’clock", + unicode_version: "6.0", + }, + { + emoji: "🕞", + aliases: ["clock330"], + tags: [], + category: "Travel & Places", + description: "three-thirty", + unicode_version: "6.0", + }, + { + emoji: "🕓", + aliases: ["clock4"], + tags: [], + category: "Travel & Places", + description: "four o’clock", + unicode_version: "6.0", + }, + { + emoji: "🕟", + aliases: ["clock430"], + tags: [], + category: "Travel & Places", + description: "four-thirty", + unicode_version: "6.0", + }, + { + emoji: "🕔", + aliases: ["clock5"], + tags: [], + category: "Travel & Places", + description: "five o’clock", + unicode_version: "6.0", + }, + { + emoji: "🕠", + aliases: ["clock530"], + tags: [], + category: "Travel & Places", + description: "five-thirty", + unicode_version: "6.0", + }, + { + emoji: "🕕", + aliases: ["clock6"], + tags: [], + category: "Travel & Places", + description: "six o’clock", + unicode_version: "6.0", + }, + { + emoji: "🕡", + aliases: ["clock630"], + tags: [], + category: "Travel & Places", + description: "six-thirty", + unicode_version: "6.0", + }, + { + emoji: "🕖", + aliases: ["clock7"], + tags: [], + category: "Travel & Places", + description: "seven o’clock", + unicode_version: "6.0", + }, + { + emoji: "🕢", + aliases: ["clock730"], + tags: [], + category: "Travel & Places", + description: "seven-thirty", + unicode_version: "6.0", + }, + { + emoji: "🕗", + aliases: ["clock8"], + tags: [], + category: "Travel & Places", + description: "eight o’clock", + unicode_version: "6.0", + }, + { + emoji: "🕣", + aliases: ["clock830"], + tags: [], + category: "Travel & Places", + description: "eight-thirty", + unicode_version: "6.0", + }, + { + emoji: "🕘", + aliases: ["clock9"], + tags: [], + category: "Travel & Places", + description: "nine o’clock", + unicode_version: "6.0", + }, + { + emoji: "🕤", + aliases: ["clock930"], + tags: [], + category: "Travel & Places", + description: "nine-thirty", + unicode_version: "6.0", + }, + { + emoji: "🕙", + aliases: ["clock10"], + tags: [], + category: "Travel & Places", + description: "ten o’clock", + unicode_version: "6.0", + }, + { + emoji: "🕥", + aliases: ["clock1030"], + tags: [], + category: "Travel & Places", + description: "ten-thirty", + unicode_version: "6.0", + }, + { + emoji: "🕚", + aliases: ["clock11"], + tags: [], + category: "Travel & Places", + description: "eleven o’clock", + unicode_version: "6.0", + }, + { + emoji: "🕦", + aliases: ["clock1130"], + tags: [], + category: "Travel & Places", + description: "eleven-thirty", + unicode_version: "6.0", + }, + { + emoji: "🌑", + aliases: ["new_moon"], + tags: [], + category: "Travel & Places", + description: "new moon", + unicode_version: "6.0", + }, + { + emoji: "🌒", + aliases: ["waxing_crescent_moon"], + tags: [], + category: "Travel & Places", + description: "waxing crescent moon", + unicode_version: "6.0", + }, + { + emoji: "🌓", + aliases: ["first_quarter_moon"], + tags: [], + category: "Travel & Places", + description: "first quarter moon", + unicode_version: "6.0", + }, + { + emoji: "🌔", + aliases: ["moon", "waxing_gibbous_moon"], + tags: [], + category: "Travel & Places", + description: "waxing gibbous moon", + unicode_version: "6.0", + }, + { + emoji: "🌕", + aliases: ["full_moon"], + tags: [], + category: "Travel & Places", + description: "full moon", + unicode_version: "6.0", + }, + { + emoji: "🌖", + aliases: ["waning_gibbous_moon"], + tags: [], + category: "Travel & Places", + description: "waning gibbous moon", + unicode_version: "6.0", + }, + { + emoji: "🌗", + aliases: ["last_quarter_moon"], + tags: [], + category: "Travel & Places", + description: "last quarter moon", + unicode_version: "6.0", + }, + { + emoji: "🌘", + aliases: ["waning_crescent_moon"], + tags: [], + category: "Travel & Places", + description: "waning crescent moon", + unicode_version: "6.0", + }, + { + emoji: "🌙", + aliases: ["crescent_moon"], + tags: ["night"], + category: "Travel & Places", + description: "crescent moon", + unicode_version: "6.0", + }, + { + emoji: "🌚", + aliases: ["new_moon_with_face"], + tags: [], + category: "Travel & Places", + description: "new moon face", + unicode_version: "6.0", + }, + { + emoji: "🌛", + aliases: ["first_quarter_moon_with_face"], + tags: [], + category: "Travel & Places", + description: "first quarter moon face", + unicode_version: "6.0", + }, + { + emoji: "🌜", + aliases: ["last_quarter_moon_with_face"], + tags: [], + category: "Travel & Places", + description: "last quarter moon face", + unicode_version: "6.0", + }, + { + emoji: "🌡️", + aliases: ["thermometer"], + tags: [], + category: "Travel & Places", + description: "thermometer", + unicode_version: "7.0", + }, + { + emoji: "☀️", + aliases: ["sunny"], + tags: ["weather"], + category: "Travel & Places", + description: "sun", + unicode_version: "", + }, + { + emoji: "🌝", + aliases: ["full_moon_with_face"], + tags: [], + category: "Travel & Places", + description: "full moon face", + unicode_version: "6.0", + }, + { + emoji: "🌞", + aliases: ["sun_with_face"], + tags: ["summer"], + category: "Travel & Places", + description: "sun with face", + unicode_version: "6.0", + }, + { + emoji: "🪐", + aliases: ["ringed_planet"], + tags: [], + category: "Travel & Places", + description: "ringed planet", + unicode_version: "12.0", + }, + { + emoji: "⭐", + aliases: ["star"], + tags: [], + category: "Travel & Places", + description: "star", + unicode_version: "5.1", + }, + { + emoji: "🌟", + aliases: ["star2"], + tags: [], + category: "Travel & Places", + description: "glowing star", + unicode_version: "6.0", + }, + { + emoji: "🌠", + aliases: ["stars"], + tags: [], + category: "Travel & Places", + description: "shooting star", + unicode_version: "6.0", + }, + { + emoji: "🌌", + aliases: ["milky_way"], + tags: [], + category: "Travel & Places", + description: "milky way", + unicode_version: "6.0", + }, + { + emoji: "☁️", + aliases: ["cloud"], + tags: [], + category: "Travel & Places", + description: "cloud", + unicode_version: "", + }, + { + emoji: "⛅", + aliases: ["partly_sunny"], + tags: ["weather", "cloud"], + category: "Travel & Places", + description: "sun behind cloud", + unicode_version: "5.2", + }, + { + emoji: "⛈️", + aliases: ["cloud_with_lightning_and_rain"], + tags: [], + category: "Travel & Places", + description: "cloud with lightning and rain", + unicode_version: "5.2", + }, + { + emoji: "🌤️", + aliases: ["sun_behind_small_cloud"], + tags: [], + category: "Travel & Places", + description: "sun behind small cloud", + unicode_version: "7.0", + }, + { + emoji: "🌥️", + aliases: ["sun_behind_large_cloud"], + tags: [], + category: "Travel & Places", + description: "sun behind large cloud", + unicode_version: "7.0", + }, + { + emoji: "🌦️", + aliases: ["sun_behind_rain_cloud"], + tags: [], + category: "Travel & Places", + description: "sun behind rain cloud", + unicode_version: "7.0", + }, + { + emoji: "🌧️", + aliases: ["cloud_with_rain"], + tags: [], + category: "Travel & Places", + description: "cloud with rain", + unicode_version: "7.0", + }, + { + emoji: "🌨️", + aliases: ["cloud_with_snow"], + tags: [], + category: "Travel & Places", + description: "cloud with snow", + unicode_version: "7.0", + }, + { + emoji: "🌩️", + aliases: ["cloud_with_lightning"], + tags: [], + category: "Travel & Places", + description: "cloud with lightning", + unicode_version: "7.0", + }, + { + emoji: "🌪️", + aliases: ["tornado"], + tags: [], + category: "Travel & Places", + description: "tornado", + unicode_version: "7.0", + }, + { + emoji: "🌫️", + aliases: ["fog"], + tags: [], + category: "Travel & Places", + description: "fog", + unicode_version: "7.0", + }, + { + emoji: "🌬️", + aliases: ["wind_face"], + tags: [], + category: "Travel & Places", + description: "wind face", + unicode_version: "7.0", + }, + { + emoji: "🌀", + aliases: ["cyclone"], + tags: ["swirl"], + category: "Travel & Places", + description: "cyclone", + unicode_version: "6.0", + }, + { + emoji: "🌈", + aliases: ["rainbow"], + tags: [], + category: "Travel & Places", + description: "rainbow", + unicode_version: "6.0", + }, + { + emoji: "🌂", + aliases: ["closed_umbrella"], + tags: ["weather", "rain"], + category: "Travel & Places", + description: "closed umbrella", + unicode_version: "6.0", + }, + { + emoji: "☂️", + aliases: ["open_umbrella"], + tags: [], + category: "Travel & Places", + description: "umbrella", + unicode_version: "", + }, + { + emoji: "☔", + aliases: ["umbrella"], + tags: ["rain", "weather"], + category: "Travel & Places", + description: "umbrella with rain drops", + unicode_version: "4.0", + }, + { + emoji: "⛱️", + aliases: ["parasol_on_ground"], + tags: ["beach_umbrella"], + category: "Travel & Places", + description: "umbrella on ground", + unicode_version: "5.2", + }, + { + emoji: "⚡", + aliases: ["zap"], + tags: ["lightning", "thunder"], + category: "Travel & Places", + description: "high voltage", + unicode_version: "4.0", + }, + { + emoji: "❄️", + aliases: ["snowflake"], + tags: ["winter", "cold", "weather"], + category: "Travel & Places", + description: "snowflake", + unicode_version: "", + }, + { + emoji: "☃️", + aliases: ["snowman_with_snow"], + tags: ["winter", "christmas"], + category: "Travel & Places", + description: "snowman", + unicode_version: "", + }, + { + emoji: "⛄", + aliases: ["snowman"], + tags: ["winter"], + category: "Travel & Places", + description: "snowman without snow", + unicode_version: "5.2", + }, + { + emoji: "☄️", + aliases: ["comet"], + tags: [], + category: "Travel & Places", + description: "comet", + unicode_version: "", + }, + { + emoji: "🔥", + aliases: ["fire"], + tags: ["burn"], + category: "Travel & Places", + description: "fire", + unicode_version: "6.0", + }, + { + emoji: "💧", + aliases: ["droplet"], + tags: ["water"], + category: "Travel & Places", + description: "droplet", + unicode_version: "6.0", + }, + { + emoji: "🌊", + aliases: ["ocean"], + tags: ["sea"], + category: "Travel & Places", + description: "water wave", + unicode_version: "6.0", + }, + { + emoji: "🎃", + aliases: ["jack_o_lantern"], + tags: ["halloween"], + category: "Activities", + description: "jack-o-lantern", + unicode_version: "6.0", + }, + { + emoji: "🎄", + aliases: ["christmas_tree"], + tags: [], + category: "Activities", + description: "Christmas tree", + unicode_version: "6.0", + }, + { + emoji: "🎆", + aliases: ["fireworks"], + tags: ["festival", "celebration"], + category: "Activities", + description: "fireworks", + unicode_version: "6.0", + }, + { + emoji: "🎇", + aliases: ["sparkler"], + tags: [], + category: "Activities", + description: "sparkler", + unicode_version: "6.0", + }, + { + emoji: "🧨", + aliases: ["firecracker"], + tags: [], + category: "Activities", + description: "firecracker", + unicode_version: "11.0", + }, + { + emoji: "✨", + aliases: ["sparkles"], + tags: ["shiny"], + category: "Activities", + description: "sparkles", + unicode_version: "6.0", + }, + { + emoji: "🎈", + aliases: ["balloon"], + tags: ["party", "birthday"], + category: "Activities", + description: "balloon", + unicode_version: "6.0", + }, + { + emoji: "🎉", + aliases: ["tada"], + tags: ["hooray", "party"], + category: "Activities", + description: "party popper", + unicode_version: "6.0", + }, + { + emoji: "🎊", + aliases: ["confetti_ball"], + tags: [], + category: "Activities", + description: "confetti ball", + unicode_version: "6.0", + }, + { + emoji: "🎋", + aliases: ["tanabata_tree"], + tags: [], + category: "Activities", + description: "tanabata tree", + unicode_version: "6.0", + }, + { + emoji: "🎍", + aliases: ["bamboo"], + tags: [], + category: "Activities", + description: "pine decoration", + unicode_version: "6.0", + }, + { + emoji: "🎎", + aliases: ["dolls"], + tags: [], + category: "Activities", + description: "Japanese dolls", + unicode_version: "6.0", + }, + { + emoji: "🎏", + aliases: ["flags"], + tags: [], + category: "Activities", + description: "carp streamer", + unicode_version: "6.0", + }, + { + emoji: "🎐", + aliases: ["wind_chime"], + tags: [], + category: "Activities", + description: "wind chime", + unicode_version: "6.0", + }, + { + emoji: "🎑", + aliases: ["rice_scene"], + tags: [], + category: "Activities", + description: "moon viewing ceremony", + unicode_version: "6.0", + }, + { + emoji: "🧧", + aliases: ["red_envelope"], + tags: [], + category: "Activities", + description: "red envelope", + unicode_version: "11.0", + }, + { + emoji: "🎀", + aliases: ["ribbon"], + tags: [], + category: "Activities", + description: "ribbon", + unicode_version: "6.0", + }, + { + emoji: "🎁", + aliases: ["gift"], + tags: ["present", "birthday", "christmas"], + category: "Activities", + description: "wrapped gift", + unicode_version: "6.0", + }, + { + emoji: "🎗️", + aliases: ["reminder_ribbon"], + tags: [], + category: "Activities", + description: "reminder ribbon", + unicode_version: "7.0", + }, + { + emoji: "🎟️", + aliases: ["tickets"], + tags: [], + category: "Activities", + description: "admission tickets", + unicode_version: "7.0", + }, + { + emoji: "🎫", + aliases: ["ticket"], + tags: [], + category: "Activities", + description: "ticket", + unicode_version: "6.0", + }, + { + emoji: "🎖️", + aliases: ["medal_military"], + tags: [], + category: "Activities", + description: "military medal", + unicode_version: "7.0", + }, + { + emoji: "🏆", + aliases: ["trophy"], + tags: ["award", "contest", "winner"], + category: "Activities", + description: "trophy", + unicode_version: "6.0", + }, + { + emoji: "🏅", + aliases: ["medal_sports"], + tags: ["gold", "winner"], + category: "Activities", + description: "sports medal", + unicode_version: "7.0", + }, + { + emoji: "🥇", + aliases: ["1st_place_medal"], + tags: ["gold"], + category: "Activities", + description: "1st place medal", + unicode_version: "9.0", + }, + { + emoji: "🥈", + aliases: ["2nd_place_medal"], + tags: ["silver"], + category: "Activities", + description: "2nd place medal", + unicode_version: "9.0", + }, + { + emoji: "🥉", + aliases: ["3rd_place_medal"], + tags: ["bronze"], + category: "Activities", + description: "3rd place medal", + unicode_version: "9.0", + }, + { + emoji: "⚽", + aliases: ["soccer"], + tags: ["sports"], + category: "Activities", + description: "soccer ball", + unicode_version: "5.2", + }, + { + emoji: "⚾", + aliases: ["baseball"], + tags: ["sports"], + category: "Activities", + description: "baseball", + unicode_version: "5.2", + }, + { + emoji: "🥎", + aliases: ["softball"], + tags: [], + category: "Activities", + description: "softball", + unicode_version: "11.0", + }, + { + emoji: "🏀", + aliases: ["basketball"], + tags: ["sports"], + category: "Activities", + description: "basketball", + unicode_version: "6.0", + }, + { + emoji: "🏐", + aliases: ["volleyball"], + tags: [], + category: "Activities", + description: "volleyball", + unicode_version: "8.0", + }, + { + emoji: "🏈", + aliases: ["football"], + tags: ["sports"], + category: "Activities", + description: "american football", + unicode_version: "6.0", + }, + { + emoji: "🏉", + aliases: ["rugby_football"], + tags: [], + category: "Activities", + description: "rugby football", + unicode_version: "6.0", + }, + { + emoji: "🎾", + aliases: ["tennis"], + tags: ["sports"], + category: "Activities", + description: "tennis", + unicode_version: "6.0", + }, + { + emoji: "🥏", + aliases: ["flying_disc"], + tags: [], + category: "Activities", + description: "flying disc", + unicode_version: "11.0", + }, + { + emoji: "🎳", + aliases: ["bowling"], + tags: [], + category: "Activities", + description: "bowling", + unicode_version: "6.0", + }, + { + emoji: "🏏", + aliases: ["cricket_game"], + tags: [], + category: "Activities", + description: "cricket game", + unicode_version: "8.0", + }, + { + emoji: "🏑", + aliases: ["field_hockey"], + tags: [], + category: "Activities", + description: "field hockey", + unicode_version: "8.0", + }, + { + emoji: "🏒", + aliases: ["ice_hockey"], + tags: [], + category: "Activities", + description: "ice hockey", + unicode_version: "8.0", + }, + { + emoji: "🥍", + aliases: ["lacrosse"], + tags: [], + category: "Activities", + description: "lacrosse", + unicode_version: "11.0", + }, + { + emoji: "🏓", + aliases: ["ping_pong"], + tags: [], + category: "Activities", + description: "ping pong", + unicode_version: "8.0", + }, + { + emoji: "🏸", + aliases: ["badminton"], + tags: [], + category: "Activities", + description: "badminton", + unicode_version: "8.0", + }, + { + emoji: "🥊", + aliases: ["boxing_glove"], + tags: [], + category: "Activities", + description: "boxing glove", + unicode_version: "9.0", + }, + { + emoji: "🥋", + aliases: ["martial_arts_uniform"], + tags: [], + category: "Activities", + description: "martial arts uniform", + unicode_version: "9.0", + }, + { + emoji: "🥅", + aliases: ["goal_net"], + tags: [], + category: "Activities", + description: "goal net", + unicode_version: "9.0", + }, + { + emoji: "⛳", + aliases: ["golf"], + tags: [], + category: "Activities", + description: "flag in hole", + unicode_version: "5.2", + }, + { + emoji: "⛸️", + aliases: ["ice_skate"], + tags: ["skating"], + category: "Activities", + description: "ice skate", + unicode_version: "5.2", + }, + { + emoji: "🎣", + aliases: ["fishing_pole_and_fish"], + tags: [], + category: "Activities", + description: "fishing pole", + unicode_version: "6.0", + }, + { + emoji: "🤿", + aliases: ["diving_mask"], + tags: [], + category: "Activities", + description: "diving mask", + unicode_version: "12.0", + }, + { + emoji: "🎽", + aliases: ["running_shirt_with_sash"], + tags: ["marathon"], + category: "Activities", + description: "running shirt", + unicode_version: "6.0", + }, + { + emoji: "🎿", + aliases: ["ski"], + tags: [], + category: "Activities", + description: "skis", + unicode_version: "6.0", + }, + { + emoji: "🛷", + aliases: ["sled"], + tags: [], + category: "Activities", + description: "sled", + unicode_version: "11.0", + }, + { + emoji: "🥌", + aliases: ["curling_stone"], + tags: [], + category: "Activities", + description: "curling stone", + unicode_version: "11.0", + }, + { + emoji: "🎯", + aliases: ["dart"], + tags: ["target"], + category: "Activities", + description: "bullseye", + unicode_version: "6.0", + }, + { + emoji: "🪀", + aliases: ["yo_yo"], + tags: [], + category: "Activities", + description: "yo-yo", + unicode_version: "12.0", + }, + { + emoji: "🪁", + aliases: ["kite"], + tags: [], + category: "Activities", + description: "kite", + unicode_version: "12.0", + }, + { + emoji: "🎱", + aliases: ["8ball"], + tags: ["pool", "billiards"], + category: "Activities", + description: "pool 8 ball", + unicode_version: "6.0", + }, + { + emoji: "🔮", + aliases: ["crystal_ball"], + tags: ["fortune"], + category: "Activities", + description: "crystal ball", + unicode_version: "6.0", + }, + { + emoji: "🪄", + aliases: ["magic_wand"], + tags: [], + category: "Activities", + description: "magic wand", + unicode_version: "13.0", + }, + { + emoji: "🧿", + aliases: ["nazar_amulet"], + tags: [], + category: "Activities", + description: "nazar amulet", + unicode_version: "11.0", + }, + { + emoji: "🎮", + aliases: ["video_game"], + tags: ["play", "controller", "console"], + category: "Activities", + description: "video game", + unicode_version: "6.0", + }, + { + emoji: "🕹️", + aliases: ["joystick"], + tags: [], + category: "Activities", + description: "joystick", + unicode_version: "7.0", + }, + { + emoji: "🎰", + aliases: ["slot_machine"], + tags: [], + category: "Activities", + description: "slot machine", + unicode_version: "6.0", + }, + { + emoji: "🎲", + aliases: ["game_die"], + tags: ["dice", "gambling"], + category: "Activities", + description: "game die", + unicode_version: "6.0", + }, + { + emoji: "🧩", + aliases: ["jigsaw"], + tags: [], + category: "Activities", + description: "puzzle piece", + unicode_version: "11.0", + }, + { + emoji: "🧸", + aliases: ["teddy_bear"], + tags: [], + category: "Activities", + description: "teddy bear", + unicode_version: "11.0", + }, + { + emoji: "🪅", + aliases: ["pinata"], + tags: [], + category: "Activities", + description: "piñata", + unicode_version: "13.0", + }, + { + emoji: "🪆", + aliases: ["nesting_dolls"], + tags: [], + category: "Activities", + description: "nesting dolls", + unicode_version: "13.0", + }, + { + emoji: "♠️", + aliases: ["spades"], + tags: [], + category: "Activities", + description: "spade suit", + unicode_version: "", + }, + { + emoji: "♥️", + aliases: ["hearts"], + tags: [], + category: "Activities", + description: "heart suit", + unicode_version: "", + }, + { + emoji: "♦️", + aliases: ["diamonds"], + tags: [], + category: "Activities", + description: "diamond suit", + unicode_version: "", + }, + { + emoji: "♣️", + aliases: ["clubs"], + tags: [], + category: "Activities", + description: "club suit", + unicode_version: "", + }, + { + emoji: "♟️", + aliases: ["chess_pawn"], + tags: [], + category: "Activities", + description: "chess pawn", + unicode_version: "11.0", + }, + { + emoji: "🃏", + aliases: ["black_joker"], + tags: [], + category: "Activities", + description: "joker", + unicode_version: "6.0", + }, + { + emoji: "🀄", + aliases: ["mahjong"], + tags: [], + category: "Activities", + description: "mahjong red dragon", + unicode_version: "", + }, + { + emoji: "🎴", + aliases: ["flower_playing_cards"], + tags: [], + category: "Activities", + description: "flower playing cards", + unicode_version: "6.0", + }, + { + emoji: "🎭", + aliases: ["performing_arts"], + tags: ["theater", "drama"], + category: "Activities", + description: "performing arts", + unicode_version: "6.0", + }, + { + emoji: "🖼️", + aliases: ["framed_picture"], + tags: [], + category: "Activities", + description: "framed picture", + unicode_version: "7.0", + }, + { + emoji: "🎨", + aliases: ["art"], + tags: ["design", "paint"], + category: "Activities", + description: "artist palette", + unicode_version: "6.0", + }, + { + emoji: "🧵", + aliases: ["thread"], + tags: [], + category: "Activities", + description: "thread", + unicode_version: "11.0", + }, + { + emoji: "🪡", + aliases: ["sewing_needle"], + tags: [], + category: "Activities", + description: "sewing needle", + unicode_version: "13.0", + }, + { + emoji: "🧶", + aliases: ["yarn"], + tags: [], + category: "Activities", + description: "yarn", + unicode_version: "11.0", + }, + { + emoji: "🪢", + aliases: ["knot"], + tags: [], + category: "Activities", + description: "knot", + unicode_version: "13.0", + }, + { + emoji: "👓", + aliases: ["eyeglasses"], + tags: ["glasses"], + category: "Objects", + description: "glasses", + unicode_version: "6.0", + }, + { + emoji: "🕶️", + aliases: ["dark_sunglasses"], + tags: [], + category: "Objects", + description: "sunglasses", + unicode_version: "7.0", + }, + { + emoji: "🥽", + aliases: ["goggles"], + tags: [], + category: "Objects", + description: "goggles", + unicode_version: "11.0", + }, + { + emoji: "🥼", + aliases: ["lab_coat"], + tags: [], + category: "Objects", + description: "lab coat", + unicode_version: "11.0", + }, + { + emoji: "🦺", + aliases: ["safety_vest"], + tags: [], + category: "Objects", + description: "safety vest", + unicode_version: "12.0", + }, + { + emoji: "👔", + aliases: ["necktie"], + tags: ["shirt", "formal"], + category: "Objects", + description: "necktie", + unicode_version: "6.0", + }, + { + emoji: "👕", + aliases: ["shirt", "tshirt"], + tags: [], + category: "Objects", + description: "t-shirt", + unicode_version: "6.0", + }, + { + emoji: "👖", + aliases: ["jeans"], + tags: ["pants"], + category: "Objects", + description: "jeans", + unicode_version: "6.0", + }, + { + emoji: "🧣", + aliases: ["scarf"], + tags: [], + category: "Objects", + description: "scarf", + unicode_version: "11.0", + }, + { + emoji: "🧤", + aliases: ["gloves"], + tags: [], + category: "Objects", + description: "gloves", + unicode_version: "11.0", + }, + { + emoji: "🧥", + aliases: ["coat"], + tags: [], + category: "Objects", + description: "coat", + unicode_version: "11.0", + }, + { + emoji: "🧦", + aliases: ["socks"], + tags: [], + category: "Objects", + description: "socks", + unicode_version: "11.0", + }, + { + emoji: "👗", + aliases: ["dress"], + tags: [], + category: "Objects", + description: "dress", + unicode_version: "6.0", + }, + { + emoji: "👘", + aliases: ["kimono"], + tags: [], + category: "Objects", + description: "kimono", + unicode_version: "6.0", + }, + { + emoji: "🥻", + aliases: ["sari"], + tags: [], + category: "Objects", + description: "sari", + unicode_version: "12.0", + }, + { + emoji: "🩱", + aliases: ["one_piece_swimsuit"], + tags: [], + category: "Objects", + description: "one-piece swimsuit", + unicode_version: "12.0", + }, + { + emoji: "🩲", + aliases: ["swim_brief"], + tags: [], + category: "Objects", + description: "briefs", + unicode_version: "12.0", + }, + { + emoji: "🩳", + aliases: ["shorts"], + tags: [], + category: "Objects", + description: "shorts", + unicode_version: "12.0", + }, + { + emoji: "👙", + aliases: ["bikini"], + tags: ["beach"], + category: "Objects", + description: "bikini", + unicode_version: "6.0", + }, + { + emoji: "👚", + aliases: ["womans_clothes"], + tags: [], + category: "Objects", + description: "woman’s clothes", + unicode_version: "6.0", + }, + { + emoji: "👛", + aliases: ["purse"], + tags: [], + category: "Objects", + description: "purse", + unicode_version: "6.0", + }, + { + emoji: "👜", + aliases: ["handbag"], + tags: ["bag"], + category: "Objects", + description: "handbag", + unicode_version: "6.0", + }, + { + emoji: "👝", + aliases: ["pouch"], + tags: ["bag"], + category: "Objects", + description: "clutch bag", + unicode_version: "6.0", + }, + { + emoji: "🛍️", + aliases: ["shopping"], + tags: ["bags"], + category: "Objects", + description: "shopping bags", + unicode_version: "7.0", + }, + { + emoji: "🎒", + aliases: ["school_satchel"], + tags: [], + category: "Objects", + description: "backpack", + unicode_version: "6.0", + }, + { + emoji: "🩴", + aliases: ["thong_sandal"], + tags: [], + category: "Objects", + description: "thong sandal", + unicode_version: "13.0", + }, + { + emoji: "👞", + aliases: ["mans_shoe", "shoe"], + tags: [], + category: "Objects", + description: "man’s shoe", + unicode_version: "6.0", + }, + { + emoji: "👟", + aliases: ["athletic_shoe"], + tags: ["sneaker", "sport", "running"], + category: "Objects", + description: "running shoe", + unicode_version: "6.0", + }, + { + emoji: "🥾", + aliases: ["hiking_boot"], + tags: [], + category: "Objects", + description: "hiking boot", + unicode_version: "11.0", + }, + { + emoji: "🥿", + aliases: ["flat_shoe"], + tags: [], + category: "Objects", + description: "flat shoe", + unicode_version: "11.0", + }, + { + emoji: "👠", + aliases: ["high_heel"], + tags: ["shoe"], + category: "Objects", + description: "high-heeled shoe", + unicode_version: "6.0", + }, + { + emoji: "👡", + aliases: ["sandal"], + tags: ["shoe"], + category: "Objects", + description: "woman’s sandal", + unicode_version: "6.0", + }, + { + emoji: "🩰", + aliases: ["ballet_shoes"], + tags: [], + category: "Objects", + description: "ballet shoes", + unicode_version: "12.0", + }, + { + emoji: "👢", + aliases: ["boot"], + tags: [], + category: "Objects", + description: "woman’s boot", + unicode_version: "6.0", + }, + { + emoji: "👑", + aliases: ["crown"], + tags: ["king", "queen", "royal"], + category: "Objects", + description: "crown", + unicode_version: "6.0", + }, + { + emoji: "👒", + aliases: ["womans_hat"], + tags: [], + category: "Objects", + description: "woman’s hat", + unicode_version: "6.0", + }, + { + emoji: "🎩", + aliases: ["tophat"], + tags: ["hat", "classy"], + category: "Objects", + description: "top hat", + unicode_version: "6.0", + }, + { + emoji: "🎓", + aliases: ["mortar_board"], + tags: ["education", "college", "university", "graduation"], + category: "Objects", + description: "graduation cap", + unicode_version: "6.0", + }, + { + emoji: "🧢", + aliases: ["billed_cap"], + tags: [], + category: "Objects", + description: "billed cap", + unicode_version: "11.0", + }, + { + emoji: "🪖", + aliases: ["military_helmet"], + tags: [], + category: "Objects", + description: "military helmet", + unicode_version: "13.0", + }, + { + emoji: "⛑️", + aliases: ["rescue_worker_helmet"], + tags: [], + category: "Objects", + description: "rescue worker’s helmet", + unicode_version: "5.2", + }, + { + emoji: "📿", + aliases: ["prayer_beads"], + tags: [], + category: "Objects", + description: "prayer beads", + unicode_version: "8.0", + }, + { + emoji: "💄", + aliases: ["lipstick"], + tags: ["makeup"], + category: "Objects", + description: "lipstick", + unicode_version: "6.0", + }, + { + emoji: "💍", + aliases: ["ring"], + tags: ["wedding", "marriage", "engaged"], + category: "Objects", + description: "ring", + unicode_version: "6.0", + }, + { + emoji: "💎", + aliases: ["gem"], + tags: ["diamond"], + category: "Objects", + description: "gem stone", + unicode_version: "6.0", + }, + { + emoji: "🔇", + aliases: ["mute"], + tags: ["sound", "volume"], + category: "Objects", + description: "muted speaker", + unicode_version: "6.0", + }, + { + emoji: "🔈", + aliases: ["speaker"], + tags: [], + category: "Objects", + description: "speaker low volume", + unicode_version: "6.0", + }, + { + emoji: "🔉", + aliases: ["sound"], + tags: ["volume"], + category: "Objects", + description: "speaker medium volume", + unicode_version: "6.0", + }, + { + emoji: "🔊", + aliases: ["loud_sound"], + tags: ["volume"], + category: "Objects", + description: "speaker high volume", + unicode_version: "6.0", + }, + { + emoji: "📢", + aliases: ["loudspeaker"], + tags: ["announcement"], + category: "Objects", + description: "loudspeaker", + unicode_version: "6.0", + }, + { + emoji: "📣", + aliases: ["mega"], + tags: [], + category: "Objects", + description: "megaphone", + unicode_version: "6.0", + }, + { + emoji: "📯", + aliases: ["postal_horn"], + tags: [], + category: "Objects", + description: "postal horn", + unicode_version: "6.0", + }, + { + emoji: "🔔", + aliases: ["bell"], + tags: ["sound", "notification"], + category: "Objects", + description: "bell", + unicode_version: "6.0", + }, + { + emoji: "🔕", + aliases: ["no_bell"], + tags: ["volume", "off"], + category: "Objects", + description: "bell with slash", + unicode_version: "6.0", + }, + { + emoji: "🎼", + aliases: ["musical_score"], + tags: [], + category: "Objects", + description: "musical score", + unicode_version: "6.0", + }, + { + emoji: "🎵", + aliases: ["musical_note"], + tags: [], + category: "Objects", + description: "musical note", + unicode_version: "6.0", + }, + { + emoji: "🎶", + aliases: ["notes"], + tags: ["music"], + category: "Objects", + description: "musical notes", + unicode_version: "6.0", + }, + { + emoji: "🎙️", + aliases: ["studio_microphone"], + tags: ["podcast"], + category: "Objects", + description: "studio microphone", + unicode_version: "7.0", + }, + { + emoji: "🎚️", + aliases: ["level_slider"], + tags: [], + category: "Objects", + description: "level slider", + unicode_version: "7.0", + }, + { + emoji: "🎛️", + aliases: ["control_knobs"], + tags: [], + category: "Objects", + description: "control knobs", + unicode_version: "7.0", + }, + { + emoji: "🎤", + aliases: ["microphone"], + tags: ["sing"], + category: "Objects", + description: "microphone", + unicode_version: "6.0", + }, + { + emoji: "🎧", + aliases: ["headphones"], + tags: ["music", "earphones"], + category: "Objects", + description: "headphone", + unicode_version: "6.0", + }, + { + emoji: "📻", + aliases: ["radio"], + tags: ["podcast"], + category: "Objects", + description: "radio", + unicode_version: "6.0", + }, + { + emoji: "🎷", + aliases: ["saxophone"], + tags: [], + category: "Objects", + description: "saxophone", + unicode_version: "6.0", + }, + { + emoji: "🪗", + aliases: ["accordion"], + tags: [], + category: "Objects", + description: "accordion", + unicode_version: "13.0", + }, + { + emoji: "🎸", + aliases: ["guitar"], + tags: ["rock"], + category: "Objects", + description: "guitar", + unicode_version: "6.0", + }, + { + emoji: "🎹", + aliases: ["musical_keyboard"], + tags: ["piano"], + category: "Objects", + description: "musical keyboard", + unicode_version: "6.0", + }, + { + emoji: "🎺", + aliases: ["trumpet"], + tags: [], + category: "Objects", + description: "trumpet", + unicode_version: "6.0", + }, + { + emoji: "🎻", + aliases: ["violin"], + tags: [], + category: "Objects", + description: "violin", + unicode_version: "6.0", + }, + { + emoji: "🪕", + aliases: ["banjo"], + tags: [], + category: "Objects", + description: "banjo", + unicode_version: "12.0", + }, + { + emoji: "🥁", + aliases: ["drum"], + tags: [], + category: "Objects", + description: "drum", + unicode_version: "", + }, + { + emoji: "🪘", + aliases: ["long_drum"], + tags: [], + category: "Objects", + description: "long drum", + unicode_version: "13.0", + }, + { + emoji: "📱", + aliases: ["iphone"], + tags: ["smartphone", "mobile"], + category: "Objects", + description: "mobile phone", + unicode_version: "6.0", + }, + { + emoji: "📲", + aliases: ["calling"], + tags: ["call", "incoming"], + category: "Objects", + description: "mobile phone with arrow", + unicode_version: "6.0", + }, + { + emoji: "☎️", + aliases: ["phone", "telephone"], + tags: [], + category: "Objects", + description: "telephone", + unicode_version: "", + }, + { + emoji: "📞", + aliases: ["telephone_receiver"], + tags: ["phone", "call"], + category: "Objects", + description: "telephone receiver", + unicode_version: "6.0", + }, + { + emoji: "📟", + aliases: ["pager"], + tags: [], + category: "Objects", + description: "pager", + unicode_version: "6.0", + }, + { + emoji: "📠", + aliases: ["fax"], + tags: [], + category: "Objects", + description: "fax machine", + unicode_version: "6.0", + }, + { + emoji: "🔋", + aliases: ["battery"], + tags: ["power"], + category: "Objects", + description: "battery", + unicode_version: "6.0", + }, + { + emoji: "🔌", + aliases: ["electric_plug"], + tags: [], + category: "Objects", + description: "electric plug", + unicode_version: "6.0", + }, + { + emoji: "💻", + aliases: ["computer"], + tags: ["desktop", "screen"], + category: "Objects", + description: "laptop", + unicode_version: "6.0", + }, + { + emoji: "🖥️", + aliases: ["desktop_computer"], + tags: [], + category: "Objects", + description: "desktop computer", + unicode_version: "7.0", + }, + { + emoji: "🖨️", + aliases: ["printer"], + tags: [], + category: "Objects", + description: "printer", + unicode_version: "7.0", + }, + { + emoji: "⌨️", + aliases: ["keyboard"], + tags: [], + category: "Objects", + description: "keyboard", + unicode_version: "", + }, + { + emoji: "🖱️", + aliases: ["computer_mouse"], + tags: [], + category: "Objects", + description: "computer mouse", + unicode_version: "7.0", + }, + { + emoji: "🖲️", + aliases: ["trackball"], + tags: [], + category: "Objects", + description: "trackball", + unicode_version: "7.0", + }, + { + emoji: "💽", + aliases: ["minidisc"], + tags: [], + category: "Objects", + description: "computer disk", + unicode_version: "6.0", + }, + { + emoji: "💾", + aliases: ["floppy_disk"], + tags: ["save"], + category: "Objects", + description: "floppy disk", + unicode_version: "6.0", + }, + { + emoji: "💿", + aliases: ["cd"], + tags: [], + category: "Objects", + description: "optical disk", + unicode_version: "6.0", + }, + { + emoji: "📀", + aliases: ["dvd"], + tags: [], + category: "Objects", + description: "dvd", + unicode_version: "6.0", + }, + { + emoji: "🧮", + aliases: ["abacus"], + tags: [], + category: "Objects", + description: "abacus", + unicode_version: "11.0", + }, + { + emoji: "🎥", + aliases: ["movie_camera"], + tags: ["film", "video"], + category: "Objects", + description: "movie camera", + unicode_version: "6.0", + }, + { + emoji: "🎞️", + aliases: ["film_strip"], + tags: [], + category: "Objects", + description: "film frames", + unicode_version: "7.0", + }, + { + emoji: "📽️", + aliases: ["film_projector"], + tags: [], + category: "Objects", + description: "film projector", + unicode_version: "7.0", + }, + { + emoji: "🎬", + aliases: ["clapper"], + tags: ["film"], + category: "Objects", + description: "clapper board", + unicode_version: "6.0", + }, + { + emoji: "📺", + aliases: ["tv"], + tags: [], + category: "Objects", + description: "television", + unicode_version: "6.0", + }, + { + emoji: "📷", + aliases: ["camera"], + tags: ["photo"], + category: "Objects", + description: "camera", + unicode_version: "6.0", + }, + { + emoji: "📸", + aliases: ["camera_flash"], + tags: ["photo"], + category: "Objects", + description: "camera with flash", + unicode_version: "7.0", + }, + { + emoji: "📹", + aliases: ["video_camera"], + tags: [], + category: "Objects", + description: "video camera", + unicode_version: "6.0", + }, + { + emoji: "📼", + aliases: ["vhs"], + tags: [], + category: "Objects", + description: "videocassette", + unicode_version: "6.0", + }, + { + emoji: "🔍", + aliases: ["mag"], + tags: ["search", "zoom"], + category: "Objects", + description: "magnifying glass tilted left", + unicode_version: "6.0", + }, + { + emoji: "🔎", + aliases: ["mag_right"], + tags: [], + category: "Objects", + description: "magnifying glass tilted right", + unicode_version: "6.0", + }, + { + emoji: "🕯️", + aliases: ["candle"], + tags: [], + category: "Objects", + description: "candle", + unicode_version: "7.0", + }, + { + emoji: "💡", + aliases: ["bulb"], + tags: ["idea", "light"], + category: "Objects", + description: "light bulb", + unicode_version: "6.0", + }, + { + emoji: "🔦", + aliases: ["flashlight"], + tags: [], + category: "Objects", + description: "flashlight", + unicode_version: "6.0", + }, + { + emoji: "🏮", + aliases: ["izakaya_lantern", "lantern"], + tags: [], + category: "Objects", + description: "red paper lantern", + unicode_version: "6.0", + }, + { + emoji: "🪔", + aliases: ["diya_lamp"], + tags: [], + category: "Objects", + description: "diya lamp", + unicode_version: "12.0", + }, + { + emoji: "📔", + aliases: ["notebook_with_decorative_cover"], + tags: [], + category: "Objects", + description: "notebook with decorative cover", + unicode_version: "6.0", + }, + { + emoji: "📕", + aliases: ["closed_book"], + tags: [], + category: "Objects", + description: "closed book", + unicode_version: "6.0", + }, + { + emoji: "📖", + aliases: ["book", "open_book"], + tags: [], + category: "Objects", + description: "open book", + unicode_version: "6.0", + }, + { + emoji: "📗", + aliases: ["green_book"], + tags: [], + category: "Objects", + description: "green book", + unicode_version: "6.0", + }, + { + emoji: "📘", + aliases: ["blue_book"], + tags: [], + category: "Objects", + description: "blue book", + unicode_version: "6.0", + }, + { + emoji: "📙", + aliases: ["orange_book"], + tags: [], + category: "Objects", + description: "orange book", + unicode_version: "6.0", + }, + { + emoji: "📚", + aliases: ["books"], + tags: ["library"], + category: "Objects", + description: "books", + unicode_version: "6.0", + }, + { + emoji: "📓", + aliases: ["notebook"], + tags: [], + category: "Objects", + description: "notebook", + unicode_version: "6.0", + }, + { + emoji: "📒", + aliases: ["ledger"], + tags: [], + category: "Objects", + description: "ledger", + unicode_version: "6.0", + }, + { + emoji: "📃", + aliases: ["page_with_curl"], + tags: [], + category: "Objects", + description: "page with curl", + unicode_version: "6.0", + }, + { + emoji: "📜", + aliases: ["scroll"], + tags: ["document"], + category: "Objects", + description: "scroll", + unicode_version: "6.0", + }, + { + emoji: "📄", + aliases: ["page_facing_up"], + tags: ["document"], + category: "Objects", + description: "page facing up", + unicode_version: "6.0", + }, + { + emoji: "📰", + aliases: ["newspaper"], + tags: ["press"], + category: "Objects", + description: "newspaper", + unicode_version: "6.0", + }, + { + emoji: "🗞️", + aliases: ["newspaper_roll"], + tags: ["press"], + category: "Objects", + description: "rolled-up newspaper", + unicode_version: "7.0", + }, + { + emoji: "📑", + aliases: ["bookmark_tabs"], + tags: [], + category: "Objects", + description: "bookmark tabs", + unicode_version: "6.0", + }, + { + emoji: "🔖", + aliases: ["bookmark"], + tags: [], + category: "Objects", + description: "bookmark", + unicode_version: "6.0", + }, + { + emoji: "🏷️", + aliases: ["label"], + tags: ["tag"], + category: "Objects", + description: "label", + unicode_version: "7.0", + }, + { + emoji: "💰", + aliases: ["moneybag"], + tags: ["dollar", "cream"], + category: "Objects", + description: "money bag", + unicode_version: "6.0", + }, + { + emoji: "🪙", + aliases: ["coin"], + tags: [], + category: "Objects", + description: "coin", + unicode_version: "13.0", + }, + { + emoji: "💴", + aliases: ["yen"], + tags: [], + category: "Objects", + description: "yen banknote", + unicode_version: "6.0", + }, + { + emoji: "💵", + aliases: ["dollar"], + tags: ["money"], + category: "Objects", + description: "dollar banknote", + unicode_version: "6.0", + }, + { + emoji: "💶", + aliases: ["euro"], + tags: [], + category: "Objects", + description: "euro banknote", + unicode_version: "6.0", + }, + { + emoji: "💷", + aliases: ["pound"], + tags: [], + category: "Objects", + description: "pound banknote", + unicode_version: "6.0", + }, + { + emoji: "💸", + aliases: ["money_with_wings"], + tags: ["dollar"], + category: "Objects", + description: "money with wings", + unicode_version: "6.0", + }, + { + emoji: "💳", + aliases: ["credit_card"], + tags: ["subscription"], + category: "Objects", + description: "credit card", + unicode_version: "6.0", + }, + { + emoji: "🧾", + aliases: ["receipt"], + tags: [], + category: "Objects", + description: "receipt", + unicode_version: "11.0", + }, + { + emoji: "💹", + aliases: ["chart"], + tags: [], + category: "Objects", + description: "chart increasing with yen", + unicode_version: "6.0", + }, + { + emoji: "✉️", + aliases: ["envelope"], + tags: ["letter", "email"], + category: "Objects", + description: "envelope", + unicode_version: "", + }, + { + emoji: "📧", + aliases: ["email", "e-mail"], + tags: [], + category: "Objects", + description: "e-mail", + unicode_version: "6.0", + }, + { + emoji: "📨", + aliases: ["incoming_envelope"], + tags: [], + category: "Objects", + description: "incoming envelope", + unicode_version: "6.0", + }, + { + emoji: "📩", + aliases: ["envelope_with_arrow"], + tags: [], + category: "Objects", + description: "envelope with arrow", + unicode_version: "6.0", + }, + { + emoji: "📤", + aliases: ["outbox_tray"], + tags: [], + category: "Objects", + description: "outbox tray", + unicode_version: "6.0", + }, + { + emoji: "📥", + aliases: ["inbox_tray"], + tags: [], + category: "Objects", + description: "inbox tray", + unicode_version: "6.0", + }, + { + emoji: "📦", + aliases: ["package"], + tags: ["shipping"], + category: "Objects", + description: "package", + unicode_version: "6.0", + }, + { + emoji: "📫", + aliases: ["mailbox"], + tags: [], + category: "Objects", + description: "closed mailbox with raised flag", + unicode_version: "6.0", + }, + { + emoji: "📪", + aliases: ["mailbox_closed"], + tags: [], + category: "Objects", + description: "closed mailbox with lowered flag", + unicode_version: "6.0", + }, + { + emoji: "📬", + aliases: ["mailbox_with_mail"], + tags: [], + category: "Objects", + description: "open mailbox with raised flag", + unicode_version: "6.0", + }, + { + emoji: "📭", + aliases: ["mailbox_with_no_mail"], + tags: [], + category: "Objects", + description: "open mailbox with lowered flag", + unicode_version: "6.0", + }, + { + emoji: "📮", + aliases: ["postbox"], + tags: [], + category: "Objects", + description: "postbox", + unicode_version: "6.0", + }, + { + emoji: "🗳️", + aliases: ["ballot_box"], + tags: [], + category: "Objects", + description: "ballot box with ballot", + unicode_version: "7.0", + }, + { + emoji: "✏️", + aliases: ["pencil2"], + tags: [], + category: "Objects", + description: "pencil", + unicode_version: "", + }, + { + emoji: "✒️", + aliases: ["black_nib"], + tags: [], + category: "Objects", + description: "black nib", + unicode_version: "", + }, + { + emoji: "🖋️", + aliases: ["fountain_pen"], + tags: [], + category: "Objects", + description: "fountain pen", + unicode_version: "7.0", + }, + { + emoji: "🖊️", + aliases: ["pen"], + tags: [], + category: "Objects", + description: "pen", + unicode_version: "7.0", + }, + { + emoji: "🖌️", + aliases: ["paintbrush"], + tags: [], + category: "Objects", + description: "paintbrush", + unicode_version: "7.0", + }, + { + emoji: "🖍️", + aliases: ["crayon"], + tags: [], + category: "Objects", + description: "crayon", + unicode_version: "7.0", + }, + { + emoji: "📝", + aliases: ["memo", "pencil"], + tags: ["document", "note"], + category: "Objects", + description: "memo", + unicode_version: "6.0", + }, + { + emoji: "💼", + aliases: ["briefcase"], + tags: ["business"], + category: "Objects", + description: "briefcase", + unicode_version: "6.0", + }, + { + emoji: "📁", + aliases: ["file_folder"], + tags: ["directory"], + category: "Objects", + description: "file folder", + unicode_version: "6.0", + }, + { + emoji: "📂", + aliases: ["open_file_folder"], + tags: [], + category: "Objects", + description: "open file folder", + unicode_version: "6.0", + }, + { + emoji: "🗂️", + aliases: ["card_index_dividers"], + tags: [], + category: "Objects", + description: "card index dividers", + unicode_version: "7.0", + }, + { + emoji: "📅", + aliases: ["date"], + tags: ["calendar", "schedule"], + category: "Objects", + description: "calendar", + unicode_version: "6.0", + }, + { + emoji: "📆", + aliases: ["calendar"], + tags: ["schedule"], + category: "Objects", + description: "tear-off calendar", + unicode_version: "6.0", + }, + { + emoji: "🗒️", + aliases: ["spiral_notepad"], + tags: [], + category: "Objects", + description: "spiral notepad", + unicode_version: "7.0", + }, + { + emoji: "🗓️", + aliases: ["spiral_calendar"], + tags: [], + category: "Objects", + description: "spiral calendar", + unicode_version: "7.0", + }, + { + emoji: "📇", + aliases: ["card_index"], + tags: [], + category: "Objects", + description: "card index", + unicode_version: "6.0", + }, + { + emoji: "📈", + aliases: ["chart_with_upwards_trend"], + tags: ["graph", "metrics"], + category: "Objects", + description: "chart increasing", + unicode_version: "6.0", + }, + { + emoji: "📉", + aliases: ["chart_with_downwards_trend"], + tags: ["graph", "metrics"], + category: "Objects", + description: "chart decreasing", + unicode_version: "6.0", + }, + { + emoji: "📊", + aliases: ["bar_chart"], + tags: ["stats", "metrics"], + category: "Objects", + description: "bar chart", + unicode_version: "6.0", + }, + { + emoji: "📋", + aliases: ["clipboard"], + tags: [], + category: "Objects", + description: "clipboard", + unicode_version: "6.0", + }, + { + emoji: "📌", + aliases: ["pushpin"], + tags: ["location"], + category: "Objects", + description: "pushpin", + unicode_version: "6.0", + }, + { + emoji: "📍", + aliases: ["round_pushpin"], + tags: ["location"], + category: "Objects", + description: "round pushpin", + unicode_version: "6.0", + }, + { + emoji: "📎", + aliases: ["paperclip"], + tags: [], + category: "Objects", + description: "paperclip", + unicode_version: "6.0", + }, + { + emoji: "🖇️", + aliases: ["paperclips"], + tags: [], + category: "Objects", + description: "linked paperclips", + unicode_version: "7.0", + }, + { + emoji: "📏", + aliases: ["straight_ruler"], + tags: [], + category: "Objects", + description: "straight ruler", + unicode_version: "6.0", + }, + { + emoji: "📐", + aliases: ["triangular_ruler"], + tags: [], + category: "Objects", + description: "triangular ruler", + unicode_version: "6.0", + }, + { + emoji: "✂️", + aliases: ["scissors"], + tags: ["cut"], + category: "Objects", + description: "scissors", + unicode_version: "", + }, + { + emoji: "🗃️", + aliases: ["card_file_box"], + tags: [], + category: "Objects", + description: "card file box", + unicode_version: "7.0", + }, + { + emoji: "🗄️", + aliases: ["file_cabinet"], + tags: [], + category: "Objects", + description: "file cabinet", + unicode_version: "7.0", + }, + { + emoji: "🗑️", + aliases: ["wastebasket"], + tags: ["trash"], + category: "Objects", + description: "wastebasket", + unicode_version: "7.0", + }, + { + emoji: "🔒", + aliases: ["lock"], + tags: ["security", "private"], + category: "Objects", + description: "locked", + unicode_version: "6.0", + }, + { + emoji: "🔓", + aliases: ["unlock"], + tags: ["security"], + category: "Objects", + description: "unlocked", + unicode_version: "6.0", + }, + { + emoji: "🔏", + aliases: ["lock_with_ink_pen"], + tags: [], + category: "Objects", + description: "locked with pen", + unicode_version: "6.0", + }, + { + emoji: "🔐", + aliases: ["closed_lock_with_key"], + tags: ["security"], + category: "Objects", + description: "locked with key", + unicode_version: "6.0", + }, + { + emoji: "🔑", + aliases: ["key"], + tags: ["lock", "password"], + category: "Objects", + description: "key", + unicode_version: "6.0", + }, + { + emoji: "🗝️", + aliases: ["old_key"], + tags: [], + category: "Objects", + description: "old key", + unicode_version: "7.0", + }, + { + emoji: "🔨", + aliases: ["hammer"], + tags: ["tool"], + category: "Objects", + description: "hammer", + unicode_version: "6.0", + }, + { + emoji: "🪓", + aliases: ["axe"], + tags: [], + category: "Objects", + description: "axe", + unicode_version: "12.0", + }, + { + emoji: "⛏️", + aliases: ["pick"], + tags: [], + category: "Objects", + description: "pick", + unicode_version: "5.2", + }, + { + emoji: "⚒️", + aliases: ["hammer_and_pick"], + tags: [], + category: "Objects", + description: "hammer and pick", + unicode_version: "4.1", + }, + { + emoji: "🛠️", + aliases: ["hammer_and_wrench"], + tags: [], + category: "Objects", + description: "hammer and wrench", + unicode_version: "7.0", + }, + { + emoji: "🗡️", + aliases: ["dagger"], + tags: [], + category: "Objects", + description: "dagger", + unicode_version: "7.0", + }, + { + emoji: "⚔️", + aliases: ["crossed_swords"], + tags: [], + category: "Objects", + description: "crossed swords", + unicode_version: "4.1", + }, + { + emoji: "🔫", + aliases: ["gun"], + tags: ["shoot", "weapon"], + category: "Objects", + description: "water pistol", + unicode_version: "6.0", + }, + { + emoji: "🪃", + aliases: ["boomerang"], + tags: [], + category: "Objects", + description: "boomerang", + unicode_version: "13.0", + }, + { + emoji: "🏹", + aliases: ["bow_and_arrow"], + tags: ["archery"], + category: "Objects", + description: "bow and arrow", + unicode_version: "8.0", + }, + { + emoji: "🛡️", + aliases: ["shield"], + tags: [], + category: "Objects", + description: "shield", + unicode_version: "7.0", + }, + { + emoji: "🪚", + aliases: ["carpentry_saw"], + tags: [], + category: "Objects", + description: "carpentry saw", + unicode_version: "13.0", + }, + { + emoji: "🔧", + aliases: ["wrench"], + tags: ["tool"], + category: "Objects", + description: "wrench", + unicode_version: "6.0", + }, + { + emoji: "🪛", + aliases: ["screwdriver"], + tags: [], + category: "Objects", + description: "screwdriver", + unicode_version: "13.0", + }, + { + emoji: "🔩", + aliases: ["nut_and_bolt"], + tags: [], + category: "Objects", + description: "nut and bolt", + unicode_version: "6.0", + }, + { + emoji: "⚙️", + aliases: ["gear"], + tags: [], + category: "Objects", + description: "gear", + unicode_version: "4.1", + }, + { + emoji: "🗜️", + aliases: ["clamp"], + tags: [], + category: "Objects", + description: "clamp", + unicode_version: "7.0", + }, + { + emoji: "⚖️", + aliases: ["balance_scale"], + tags: [], + category: "Objects", + description: "balance scale", + unicode_version: "4.1", + }, + { + emoji: "🦯", + aliases: ["probing_cane"], + tags: [], + category: "Objects", + description: "white cane", + unicode_version: "12.0", + }, + { + emoji: "🔗", + aliases: ["link"], + tags: [], + category: "Objects", + description: "link", + unicode_version: "6.0", + }, + { + emoji: "⛓️", + aliases: ["chains"], + tags: [], + category: "Objects", + description: "chains", + unicode_version: "5.2", + }, + { + emoji: "🪝", + aliases: ["hook"], + tags: [], + category: "Objects", + description: "hook", + unicode_version: "13.0", + }, + { + emoji: "🧰", + aliases: ["toolbox"], + tags: [], + category: "Objects", + description: "toolbox", + unicode_version: "11.0", + }, + { + emoji: "🧲", + aliases: ["magnet"], + tags: [], + category: "Objects", + description: "magnet", + unicode_version: "11.0", + }, + { + emoji: "🪜", + aliases: ["ladder"], + tags: [], + category: "Objects", + description: "ladder", + unicode_version: "13.0", + }, + { + emoji: "⚗️", + aliases: ["alembic"], + tags: [], + category: "Objects", + description: "alembic", + unicode_version: "4.1", + }, + { + emoji: "🧪", + aliases: ["test_tube"], + tags: [], + category: "Objects", + description: "test tube", + unicode_version: "11.0", + }, + { + emoji: "🧫", + aliases: ["petri_dish"], + tags: [], + category: "Objects", + description: "petri dish", + unicode_version: "11.0", + }, + { + emoji: "🧬", + aliases: ["dna"], + tags: [], + category: "Objects", + description: "dna", + unicode_version: "11.0", + }, + { + emoji: "🔬", + aliases: ["microscope"], + tags: ["science", "laboratory", "investigate"], + category: "Objects", + description: "microscope", + unicode_version: "6.0", + }, + { + emoji: "🔭", + aliases: ["telescope"], + tags: [], + category: "Objects", + description: "telescope", + unicode_version: "6.0", + }, + { + emoji: "📡", + aliases: ["satellite"], + tags: ["signal"], + category: "Objects", + description: "satellite antenna", + unicode_version: "6.0", + }, + { + emoji: "💉", + aliases: ["syringe"], + tags: ["health", "hospital", "needle"], + category: "Objects", + description: "syringe", + unicode_version: "6.0", + }, + { + emoji: "🩸", + aliases: ["drop_of_blood"], + tags: [], + category: "Objects", + description: "drop of blood", + unicode_version: "12.0", + }, + { + emoji: "💊", + aliases: ["pill"], + tags: ["health", "medicine"], + category: "Objects", + description: "pill", + unicode_version: "6.0", + }, + { + emoji: "🩹", + aliases: ["adhesive_bandage"], + tags: [], + category: "Objects", + description: "adhesive bandage", + unicode_version: "12.0", + }, + { + emoji: "🩺", + aliases: ["stethoscope"], + tags: [], + category: "Objects", + description: "stethoscope", + unicode_version: "12.0", + }, + { + emoji: "🚪", + aliases: ["door"], + tags: [], + category: "Objects", + description: "door", + unicode_version: "6.0", + }, + { + emoji: "🛗", + aliases: ["elevator"], + tags: [], + category: "Objects", + description: "elevator", + unicode_version: "13.0", + }, + { + emoji: "🪞", + aliases: ["mirror"], + tags: [], + category: "Objects", + description: "mirror", + unicode_version: "13.0", + }, + { + emoji: "🪟", + aliases: ["window"], + tags: [], + category: "Objects", + description: "window", + unicode_version: "13.0", + }, + { + emoji: "🛏️", + aliases: ["bed"], + tags: [], + category: "Objects", + description: "bed", + unicode_version: "7.0", + }, + { + emoji: "🛋️", + aliases: ["couch_and_lamp"], + tags: [], + category: "Objects", + description: "couch and lamp", + unicode_version: "7.0", + }, + { + emoji: "🪑", + aliases: ["chair"], + tags: [], + category: "Objects", + description: "chair", + unicode_version: "12.0", + }, + { + emoji: "🚽", + aliases: ["toilet"], + tags: ["wc"], + category: "Objects", + description: "toilet", + unicode_version: "6.0", + }, + { + emoji: "🪠", + aliases: ["plunger"], + tags: [], + category: "Objects", + description: "plunger", + unicode_version: "13.0", + }, + { + emoji: "🚿", + aliases: ["shower"], + tags: ["bath"], + category: "Objects", + description: "shower", + unicode_version: "6.0", + }, + { + emoji: "🛁", + aliases: ["bathtub"], + tags: [], + category: "Objects", + description: "bathtub", + unicode_version: "6.0", + }, + { + emoji: "🪤", + aliases: ["mouse_trap"], + tags: [], + category: "Objects", + description: "mouse trap", + unicode_version: "13.0", + }, + { + emoji: "🪒", + aliases: ["razor"], + tags: [], + category: "Objects", + description: "razor", + unicode_version: "12.0", + }, + { + emoji: "🧴", + aliases: ["lotion_bottle"], + tags: [], + category: "Objects", + description: "lotion bottle", + unicode_version: "11.0", + }, + { + emoji: "🧷", + aliases: ["safety_pin"], + tags: [], + category: "Objects", + description: "safety pin", + unicode_version: "11.0", + }, + { + emoji: "🧹", + aliases: ["broom"], + tags: [], + category: "Objects", + description: "broom", + unicode_version: "11.0", + }, + { + emoji: "🧺", + aliases: ["basket"], + tags: [], + category: "Objects", + description: "basket", + unicode_version: "11.0", + }, + { + emoji: "🧻", + aliases: ["roll_of_paper"], + tags: ["toilet"], + category: "Objects", + description: "roll of paper", + unicode_version: "11.0", + }, + { + emoji: "🪣", + aliases: ["bucket"], + tags: [], + category: "Objects", + description: "bucket", + unicode_version: "13.0", + }, + { + emoji: "🧼", + aliases: ["soap"], + tags: [], + category: "Objects", + description: "soap", + unicode_version: "11.0", + }, + { + emoji: "🪥", + aliases: ["toothbrush"], + tags: [], + category: "Objects", + description: "toothbrush", + unicode_version: "13.0", + }, + { + emoji: "🧽", + aliases: ["sponge"], + tags: [], + category: "Objects", + description: "sponge", + unicode_version: "11.0", + }, + { + emoji: "🧯", + aliases: ["fire_extinguisher"], + tags: [], + category: "Objects", + description: "fire extinguisher", + unicode_version: "11.0", + }, + { + emoji: "🛒", + aliases: ["shopping_cart"], + tags: [], + category: "Objects", + description: "shopping cart", + unicode_version: "9.0", + }, + { + emoji: "🚬", + aliases: ["smoking"], + tags: ["cigarette"], + category: "Objects", + description: "cigarette", + unicode_version: "6.0", + }, + { + emoji: "⚰️", + aliases: ["coffin"], + tags: ["funeral"], + category: "Objects", + description: "coffin", + unicode_version: "4.1", + }, + { + emoji: "🪦", + aliases: ["headstone"], + tags: [], + category: "Objects", + description: "headstone", + unicode_version: "13.0", + }, + { + emoji: "⚱️", + aliases: ["funeral_urn"], + tags: [], + category: "Objects", + description: "funeral urn", + unicode_version: "4.1", + }, + { + emoji: "🗿", + aliases: ["moyai"], + tags: ["stone"], + category: "Objects", + description: "moai", + unicode_version: "6.0", + }, + { + emoji: "🪧", + aliases: ["placard"], + tags: [], + category: "Objects", + description: "placard", + unicode_version: "13.0", + }, + { + emoji: "🏧", + aliases: ["atm"], + tags: [], + category: "Symbols", + description: "ATM sign", + unicode_version: "6.0", + }, + { + emoji: "🚮", + aliases: ["put_litter_in_its_place"], + tags: [], + category: "Symbols", + description: "litter in bin sign", + unicode_version: "6.0", + }, + { + emoji: "🚰", + aliases: ["potable_water"], + tags: [], + category: "Symbols", + description: "potable water", + unicode_version: "6.0", + }, + { + emoji: "♿", + aliases: ["wheelchair"], + tags: ["accessibility"], + category: "Symbols", + description: "wheelchair symbol", + unicode_version: "4.1", + }, + { + emoji: "🚹", + aliases: ["mens"], + tags: [], + category: "Symbols", + description: "men’s room", + unicode_version: "6.0", + }, + { + emoji: "🚺", + aliases: ["womens"], + tags: [], + category: "Symbols", + description: "women’s room", + unicode_version: "6.0", + }, + { + emoji: "🚻", + aliases: ["restroom"], + tags: ["toilet"], + category: "Symbols", + description: "restroom", + unicode_version: "6.0", + }, + { + emoji: "🚼", + aliases: ["baby_symbol"], + tags: [], + category: "Symbols", + description: "baby symbol", + unicode_version: "6.0", + }, + { + emoji: "🚾", + aliases: ["wc"], + tags: ["toilet", "restroom"], + category: "Symbols", + description: "water closet", + unicode_version: "6.0", + }, + { + emoji: "🛂", + aliases: ["passport_control"], + tags: [], + category: "Symbols", + description: "passport control", + unicode_version: "6.0", + }, + { + emoji: "🛃", + aliases: ["customs"], + tags: [], + category: "Symbols", + description: "customs", + unicode_version: "6.0", + }, + { + emoji: "🛄", + aliases: ["baggage_claim"], + tags: ["airport"], + category: "Symbols", + description: "baggage claim", + unicode_version: "6.0", + }, + { + emoji: "🛅", + aliases: ["left_luggage"], + tags: [], + category: "Symbols", + description: "left luggage", + unicode_version: "6.0", + }, + { + emoji: "⚠️", + aliases: ["warning"], + tags: ["wip"], + category: "Symbols", + description: "warning", + unicode_version: "4.0", + }, + { + emoji: "🚸", + aliases: ["children_crossing"], + tags: [], + category: "Symbols", + description: "children crossing", + unicode_version: "6.0", + }, + { + emoji: "⛔", + aliases: ["no_entry"], + tags: ["limit"], + category: "Symbols", + description: "no entry", + unicode_version: "5.2", + }, + { + emoji: "🚫", + aliases: ["no_entry_sign"], + tags: ["block", "forbidden"], + category: "Symbols", + description: "prohibited", + unicode_version: "6.0", + }, + { + emoji: "🚳", + aliases: ["no_bicycles"], + tags: [], + category: "Symbols", + description: "no bicycles", + unicode_version: "6.0", + }, + { + emoji: "🚭", + aliases: ["no_smoking"], + tags: [], + category: "Symbols", + description: "no smoking", + unicode_version: "6.0", + }, + { + emoji: "🚯", + aliases: ["do_not_litter"], + tags: [], + category: "Symbols", + description: "no littering", + unicode_version: "6.0", + }, + { + emoji: "🚱", + aliases: ["non-potable_water"], + tags: [], + category: "Symbols", + description: "non-potable water", + unicode_version: "6.0", + }, + { + emoji: "🚷", + aliases: ["no_pedestrians"], + tags: [], + category: "Symbols", + description: "no pedestrians", + unicode_version: "6.0", + }, + { + emoji: "📵", + aliases: ["no_mobile_phones"], + tags: [], + category: "Symbols", + description: "no mobile phones", + unicode_version: "6.0", + }, + { + emoji: "🔞", + aliases: ["underage"], + tags: [], + category: "Symbols", + description: "no one under eighteen", + unicode_version: "6.0", + }, + { + emoji: "☢️", + aliases: ["radioactive"], + tags: [], + category: "Symbols", + description: "radioactive", + unicode_version: "", + }, + { + emoji: "☣️", + aliases: ["biohazard"], + tags: [], + category: "Symbols", + description: "biohazard", + unicode_version: "", + }, + { + emoji: "⬆️", + aliases: ["arrow_up"], + tags: [], + category: "Symbols", + description: "up arrow", + unicode_version: "4.0", + }, + { + emoji: "↗️", + aliases: ["arrow_upper_right"], + tags: [], + category: "Symbols", + description: "up-right arrow", + unicode_version: "", + }, + { + emoji: "➡️", + aliases: ["arrow_right"], + tags: [], + category: "Symbols", + description: "right arrow", + unicode_version: "", + }, + { + emoji: "↘️", + aliases: ["arrow_lower_right"], + tags: [], + category: "Symbols", + description: "down-right arrow", + unicode_version: "", + }, + { + emoji: "⬇️", + aliases: ["arrow_down"], + tags: [], + category: "Symbols", + description: "down arrow", + unicode_version: "4.0", + }, + { + emoji: "↙️", + aliases: ["arrow_lower_left"], + tags: [], + category: "Symbols", + description: "down-left arrow", + unicode_version: "", + }, + { + emoji: "⬅️", + aliases: ["arrow_left"], + tags: [], + category: "Symbols", + description: "left arrow", + unicode_version: "4.0", + }, + { + emoji: "↖️", + aliases: ["arrow_upper_left"], + tags: [], + category: "Symbols", + description: "up-left arrow", + unicode_version: "", + }, + { + emoji: "↕️", + aliases: ["arrow_up_down"], + tags: [], + category: "Symbols", + description: "up-down arrow", + unicode_version: "", + }, + { + emoji: "↔️", + aliases: ["left_right_arrow"], + tags: [], + category: "Symbols", + description: "left-right arrow", + unicode_version: "", + }, + { + emoji: "↩️", + aliases: ["leftwards_arrow_with_hook"], + tags: ["return"], + category: "Symbols", + description: "right arrow curving left", + unicode_version: "", + }, + { + emoji: "↪️", + aliases: ["arrow_right_hook"], + tags: [], + category: "Symbols", + description: "left arrow curving right", + unicode_version: "", + }, + { + emoji: "⤴️", + aliases: ["arrow_heading_up"], + tags: [], + category: "Symbols", + description: "right arrow curving up", + unicode_version: "", + }, + { + emoji: "⤵️", + aliases: ["arrow_heading_down"], + tags: [], + category: "Symbols", + description: "right arrow curving down", + unicode_version: "", + }, + { + emoji: "🔃", + aliases: ["arrows_clockwise"], + tags: [], + category: "Symbols", + description: "clockwise vertical arrows", + unicode_version: "6.0", + }, + { + emoji: "🔄", + aliases: ["arrows_counterclockwise"], + tags: ["sync"], + category: "Symbols", + description: "counterclockwise arrows button", + unicode_version: "6.0", + }, + { + emoji: "🔙", + aliases: ["back"], + tags: [], + category: "Symbols", + description: "BACK arrow", + unicode_version: "6.0", + }, + { + emoji: "🔚", + aliases: ["end"], + tags: [], + category: "Symbols", + description: "END arrow", + unicode_version: "6.0", + }, + { + emoji: "🔛", + aliases: ["on"], + tags: [], + category: "Symbols", + description: "ON! arrow", + unicode_version: "6.0", + }, + { + emoji: "🔜", + aliases: ["soon"], + tags: [], + category: "Symbols", + description: "SOON arrow", + unicode_version: "6.0", + }, + { + emoji: "🔝", + aliases: ["top"], + tags: [], + category: "Symbols", + description: "TOP arrow", + unicode_version: "6.0", + }, + { + emoji: "🛐", + aliases: ["place_of_worship"], + tags: [], + category: "Symbols", + description: "place of worship", + unicode_version: "8.0", + }, + { + emoji: "⚛️", + aliases: ["atom_symbol"], + tags: [], + category: "Symbols", + description: "atom symbol", + unicode_version: "4.1", + }, + { + emoji: "🕉️", + aliases: ["om"], + tags: [], + category: "Symbols", + description: "om", + unicode_version: "7.0", + }, + { + emoji: "✡️", + aliases: ["star_of_david"], + tags: [], + category: "Symbols", + description: "star of David", + unicode_version: "", + }, + { + emoji: "☸️", + aliases: ["wheel_of_dharma"], + tags: [], + category: "Symbols", + description: "wheel of dharma", + unicode_version: "", + }, + { + emoji: "☯️", + aliases: ["yin_yang"], + tags: [], + category: "Symbols", + description: "yin yang", + unicode_version: "", + }, + { + emoji: "✝️", + aliases: ["latin_cross"], + tags: [], + category: "Symbols", + description: "latin cross", + unicode_version: "", + }, + { + emoji: "☦️", + aliases: ["orthodox_cross"], + tags: [], + category: "Symbols", + description: "orthodox cross", + unicode_version: "", + }, + { + emoji: "☪️", + aliases: ["star_and_crescent"], + tags: [], + category: "Symbols", + description: "star and crescent", + unicode_version: "", + }, + { + emoji: "☮️", + aliases: ["peace_symbol"], + tags: [], + category: "Symbols", + description: "peace symbol", + unicode_version: "", + }, + { + emoji: "🕎", + aliases: ["menorah"], + tags: [], + category: "Symbols", + description: "menorah", + unicode_version: "8.0", + }, + { + emoji: "🔯", + aliases: ["six_pointed_star"], + tags: [], + category: "Symbols", + description: "dotted six-pointed star", + unicode_version: "6.0", + }, + { + emoji: "♈", + aliases: ["aries"], + tags: [], + category: "Symbols", + description: "Aries", + unicode_version: "", + }, + { + emoji: "♉", + aliases: ["taurus"], + tags: [], + category: "Symbols", + description: "Taurus", + unicode_version: "", + }, + { + emoji: "♊", + aliases: ["gemini"], + tags: [], + category: "Symbols", + description: "Gemini", + unicode_version: "", + }, + { + emoji: "♋", + aliases: ["cancer"], + tags: [], + category: "Symbols", + description: "Cancer", + unicode_version: "", + }, + { + emoji: "♌", + aliases: ["leo"], + tags: [], + category: "Symbols", + description: "Leo", + unicode_version: "", + }, + { + emoji: "♍", + aliases: ["virgo"], + tags: [], + category: "Symbols", + description: "Virgo", + unicode_version: "", + }, + { + emoji: "♎", + aliases: ["libra"], + tags: [], + category: "Symbols", + description: "Libra", + unicode_version: "", + }, + { + emoji: "♏", + aliases: ["scorpius"], + tags: [], + category: "Symbols", + description: "Scorpio", + unicode_version: "", + }, + { + emoji: "♐", + aliases: ["sagittarius"], + tags: [], + category: "Symbols", + description: "Sagittarius", + unicode_version: "", + }, + { + emoji: "♑", + aliases: ["capricorn"], + tags: [], + category: "Symbols", + description: "Capricorn", + unicode_version: "", + }, + { + emoji: "♒", + aliases: ["aquarius"], + tags: [], + category: "Symbols", + description: "Aquarius", + unicode_version: "", + }, + { + emoji: "♓", + aliases: ["pisces"], + tags: [], + category: "Symbols", + description: "Pisces", + unicode_version: "", + }, + { + emoji: "⛎", + aliases: ["ophiuchus"], + tags: [], + category: "Symbols", + description: "Ophiuchus", + unicode_version: "6.0", + }, + { + emoji: "🔀", + aliases: ["twisted_rightwards_arrows"], + tags: ["shuffle"], + category: "Symbols", + description: "shuffle tracks button", + unicode_version: "6.0", + }, + { + emoji: "🔁", + aliases: ["repeat"], + tags: ["loop"], + category: "Symbols", + description: "repeat button", + unicode_version: "6.0", + }, + { + emoji: "🔂", + aliases: ["repeat_one"], + tags: [], + category: "Symbols", + description: "repeat single button", + unicode_version: "6.0", + }, + { + emoji: "▶️", + aliases: ["arrow_forward"], + tags: [], + category: "Symbols", + description: "play button", + unicode_version: "", + }, + { + emoji: "⏩", + aliases: ["fast_forward"], + tags: [], + category: "Symbols", + description: "fast-forward button", + unicode_version: "6.0", + }, + { + emoji: "⏭️", + aliases: ["next_track_button"], + tags: [], + category: "Symbols", + description: "next track button", + unicode_version: "6.0", + }, + { + emoji: "⏯️", + aliases: ["play_or_pause_button"], + tags: [], + category: "Symbols", + description: "play or pause button", + unicode_version: "6.0", + }, + { + emoji: "◀️", + aliases: ["arrow_backward"], + tags: [], + category: "Symbols", + description: "reverse button", + unicode_version: "", + }, + { + emoji: "⏪", + aliases: ["rewind"], + tags: [], + category: "Symbols", + description: "fast reverse button", + unicode_version: "6.0", + }, + { + emoji: "⏮️", + aliases: ["previous_track_button"], + tags: [], + category: "Symbols", + description: "last track button", + unicode_version: "6.0", + }, + { + emoji: "🔼", + aliases: ["arrow_up_small"], + tags: [], + category: "Symbols", + description: "upwards button", + unicode_version: "6.0", + }, + { + emoji: "⏫", + aliases: ["arrow_double_up"], + tags: [], + category: "Symbols", + description: "fast up button", + unicode_version: "6.0", + }, + { + emoji: "🔽", + aliases: ["arrow_down_small"], + tags: [], + category: "Symbols", + description: "downwards button", + unicode_version: "6.0", + }, + { + emoji: "⏬", + aliases: ["arrow_double_down"], + tags: [], + category: "Symbols", + description: "fast down button", + unicode_version: "6.0", + }, + { + emoji: "⏸️", + aliases: ["pause_button"], + tags: [], + category: "Symbols", + description: "pause button", + unicode_version: "7.0", + }, + { + emoji: "⏹️", + aliases: ["stop_button"], + tags: [], + category: "Symbols", + description: "stop button", + unicode_version: "7.0", + }, + { + emoji: "⏺️", + aliases: ["record_button"], + tags: [], + category: "Symbols", + description: "record button", + unicode_version: "7.0", + }, + { + emoji: "⏏️", + aliases: ["eject_button"], + tags: [], + category: "Symbols", + description: "eject button", + unicode_version: "11.0", + }, + { + emoji: "🎦", + aliases: ["cinema"], + tags: ["film", "movie"], + category: "Symbols", + description: "cinema", + unicode_version: "6.0", + }, + { + emoji: "🔅", + aliases: ["low_brightness"], + tags: [], + category: "Symbols", + description: "dim button", + unicode_version: "6.0", + }, + { + emoji: "🔆", + aliases: ["high_brightness"], + tags: [], + category: "Symbols", + description: "bright button", + unicode_version: "6.0", + }, + { + emoji: "📶", + aliases: ["signal_strength"], + tags: ["wifi"], + category: "Symbols", + description: "antenna bars", + unicode_version: "6.0", + }, + { + emoji: "📳", + aliases: ["vibration_mode"], + tags: [], + category: "Symbols", + description: "vibration mode", + unicode_version: "6.0", + }, + { + emoji: "📴", + aliases: ["mobile_phone_off"], + tags: ["mute", "off"], + category: "Symbols", + description: "mobile phone off", + unicode_version: "6.0", + }, + { + emoji: "♀️", + aliases: ["female_sign"], + tags: [], + category: "Symbols", + description: "female sign", + unicode_version: "11.0", + }, + { + emoji: "♂️", + aliases: ["male_sign"], + tags: [], + category: "Symbols", + description: "male sign", + unicode_version: "11.0", + }, + { + emoji: "⚧️", + aliases: ["transgender_symbol"], + tags: [], + category: "Symbols", + description: "transgender symbol", + unicode_version: "13.0", + }, + { + emoji: "✖️", + aliases: ["heavy_multiplication_x"], + tags: [], + category: "Symbols", + description: "multiply", + unicode_version: "", + }, + { + emoji: "➕", + aliases: ["heavy_plus_sign"], + tags: [], + category: "Symbols", + description: "plus", + unicode_version: "6.0", + }, + { + emoji: "➖", + aliases: ["heavy_minus_sign"], + tags: [], + category: "Symbols", + description: "minus", + unicode_version: "6.0", + }, + { + emoji: "➗", + aliases: ["heavy_division_sign"], + tags: [], + category: "Symbols", + description: "divide", + unicode_version: "6.0", + }, + { + emoji: "♾️", + aliases: ["infinity"], + tags: [], + category: "Symbols", + description: "infinity", + unicode_version: "11.0", + }, + { + emoji: "‼️", + aliases: ["bangbang"], + tags: [], + category: "Symbols", + description: "double exclamation mark", + unicode_version: "", + }, + { + emoji: "⁉️", + aliases: ["interrobang"], + tags: [], + category: "Symbols", + description: "exclamation question mark", + unicode_version: "3.0", + }, + { + emoji: "❓", + aliases: ["question"], + tags: ["confused"], + category: "Symbols", + description: "red question mark", + unicode_version: "6.0", + }, + { + emoji: "❔", + aliases: ["grey_question"], + tags: [], + category: "Symbols", + description: "white question mark", + unicode_version: "6.0", + }, + { + emoji: "❕", + aliases: ["grey_exclamation"], + tags: [], + category: "Symbols", + description: "white exclamation mark", + unicode_version: "6.0", + }, + { + emoji: "❗", + aliases: ["exclamation", "heavy_exclamation_mark"], + tags: ["bang"], + category: "Symbols", + description: "red exclamation mark", + unicode_version: "5.2", + }, + { + emoji: "〰️", + aliases: ["wavy_dash"], + tags: [], + category: "Symbols", + description: "wavy dash", + unicode_version: "", + }, + { + emoji: "💱", + aliases: ["currency_exchange"], + tags: [], + category: "Symbols", + description: "currency exchange", + unicode_version: "6.0", + }, + { + emoji: "💲", + aliases: ["heavy_dollar_sign"], + tags: [], + category: "Symbols", + description: "heavy dollar sign", + unicode_version: "6.0", + }, + { + emoji: "⚕️", + aliases: ["medical_symbol"], + tags: [], + category: "Symbols", + description: "medical symbol", + unicode_version: "11.0", + }, + { + emoji: "♻️", + aliases: ["recycle"], + tags: ["environment", "green"], + category: "Symbols", + description: "recycling symbol", + unicode_version: "3.2", + }, + { + emoji: "⚜️", + aliases: ["fleur_de_lis"], + tags: [], + category: "Symbols", + description: "fleur-de-lis", + unicode_version: "4.1", + }, + { + emoji: "🔱", + aliases: ["trident"], + tags: [], + category: "Symbols", + description: "trident emblem", + unicode_version: "6.0", + }, + { + emoji: "📛", + aliases: ["name_badge"], + tags: [], + category: "Symbols", + description: "name badge", + unicode_version: "6.0", + }, + { + emoji: "🔰", + aliases: ["beginner"], + tags: [], + category: "Symbols", + description: "Japanese symbol for beginner", + unicode_version: "6.0", + }, + { + emoji: "⭕", + aliases: ["o"], + tags: [], + category: "Symbols", + description: "hollow red circle", + unicode_version: "5.2", + }, + { + emoji: "✅", + aliases: ["white_check_mark"], + tags: [], + category: "Symbols", + description: "check mark button", + unicode_version: "6.0", + }, + { + emoji: "☑️", + aliases: ["ballot_box_with_check"], + tags: [], + category: "Symbols", + description: "check box with check", + unicode_version: "", + }, + { + emoji: "✔️", + aliases: ["heavy_check_mark"], + tags: [], + category: "Symbols", + description: "check mark", + unicode_version: "", + }, + { + emoji: "❌", + aliases: ["x"], + tags: [], + category: "Symbols", + description: "cross mark", + unicode_version: "6.0", + }, + { + emoji: "❎", + aliases: ["negative_squared_cross_mark"], + tags: [], + category: "Symbols", + description: "cross mark button", + unicode_version: "6.0", + }, + { + emoji: "➰", + aliases: ["curly_loop"], + tags: [], + category: "Symbols", + description: "curly loop", + unicode_version: "6.0", + }, + { + emoji: "➿", + aliases: ["loop"], + tags: [], + category: "Symbols", + description: "double curly loop", + unicode_version: "6.0", + }, + { + emoji: "〽️", + aliases: ["part_alternation_mark"], + tags: [], + category: "Symbols", + description: "part alternation mark", + unicode_version: "3.2", + }, + { + emoji: "✳️", + aliases: ["eight_spoked_asterisk"], + tags: [], + category: "Symbols", + description: "eight-spoked asterisk", + unicode_version: "", + }, + { + emoji: "✴️", + aliases: ["eight_pointed_black_star"], + tags: [], + category: "Symbols", + description: "eight-pointed star", + unicode_version: "", + }, + { + emoji: "❇️", + aliases: ["sparkle"], + tags: [], + category: "Symbols", + description: "sparkle", + unicode_version: "", + }, + { + emoji: "©️", + aliases: ["copyright"], + tags: [], + category: "Symbols", + description: "copyright", + unicode_version: "", + }, + { + emoji: "®️", + aliases: ["registered"], + tags: [], + category: "Symbols", + description: "registered", + unicode_version: "", + }, + { + emoji: "™️", + aliases: ["tm"], + tags: ["trademark"], + category: "Symbols", + description: "trade mark", + unicode_version: "", + }, + { + emoji: "#️⃣", + aliases: ["hash"], + tags: ["number"], + category: "Symbols", + description: "keycap: #", + unicode_version: "", + }, + { + emoji: "*️⃣", + aliases: ["asterisk"], + tags: [], + category: "Symbols", + description: "keycap: *", + unicode_version: "", + }, + { + emoji: "0️⃣", + aliases: ["zero"], + tags: [], + category: "Symbols", + description: "keycap: 0", + unicode_version: "", + }, + { + emoji: "1️⃣", + aliases: ["one"], + tags: [], + category: "Symbols", + description: "keycap: 1", + unicode_version: "", + }, + { + emoji: "2️⃣", + aliases: ["two"], + tags: [], + category: "Symbols", + description: "keycap: 2", + unicode_version: "", + }, + { + emoji: "3️⃣", + aliases: ["three"], + tags: [], + category: "Symbols", + description: "keycap: 3", + unicode_version: "", + }, + { + emoji: "4️⃣", + aliases: ["four"], + tags: [], + category: "Symbols", + description: "keycap: 4", + unicode_version: "", + }, + { + emoji: "5️⃣", + aliases: ["five"], + tags: [], + category: "Symbols", + description: "keycap: 5", + unicode_version: "", + }, + { + emoji: "6️⃣", + aliases: ["six"], + tags: [], + category: "Symbols", + description: "keycap: 6", + unicode_version: "", + }, + { + emoji: "7️⃣", + aliases: ["seven"], + tags: [], + category: "Symbols", + description: "keycap: 7", + unicode_version: "", + }, + { + emoji: "8️⃣", + aliases: ["eight"], + tags: [], + category: "Symbols", + description: "keycap: 8", + unicode_version: "", + }, + { + emoji: "9️⃣", + aliases: ["nine"], + tags: [], + category: "Symbols", + description: "keycap: 9", + unicode_version: "", + }, + { + emoji: "🔟", + aliases: ["keycap_ten"], + tags: [], + category: "Symbols", + description: "keycap: 10", + unicode_version: "6.0", + }, + { + emoji: "🔠", + aliases: ["capital_abcd"], + tags: ["letters"], + category: "Symbols", + description: "input latin uppercase", + unicode_version: "6.0", + }, + { + emoji: "🔡", + aliases: ["abcd"], + tags: [], + category: "Symbols", + description: "input latin lowercase", + unicode_version: "6.0", + }, + { + emoji: "🔢", + aliases: ["1234"], + tags: ["numbers"], + category: "Symbols", + description: "input numbers", + unicode_version: "6.0", + }, + { + emoji: "🔣", + aliases: ["symbols"], + tags: [], + category: "Symbols", + description: "input symbols", + unicode_version: "6.0", + }, + { + emoji: "🔤", + aliases: ["abc"], + tags: ["alphabet"], + category: "Symbols", + description: "input latin letters", + unicode_version: "6.0", + }, + { + emoji: "🅰️", + aliases: ["a"], + tags: [], + category: "Symbols", + description: "A button (blood type)", + unicode_version: "6.0", + }, + { + emoji: "🆎", + aliases: ["ab"], + tags: [], + category: "Symbols", + description: "AB button (blood type)", + unicode_version: "6.0", + }, + { + emoji: "🅱️", + aliases: ["b"], + tags: [], + category: "Symbols", + description: "B button (blood type)", + unicode_version: "6.0", + }, + { + emoji: "🆑", + aliases: ["cl"], + tags: [], + category: "Symbols", + description: "CL button", + unicode_version: "6.0", + }, + { + emoji: "🆒", + aliases: ["cool"], + tags: [], + category: "Symbols", + description: "COOL button", + unicode_version: "6.0", + }, + { + emoji: "🆓", + aliases: ["free"], + tags: [], + category: "Symbols", + description: "FREE button", + unicode_version: "6.0", + }, + { + emoji: "ℹ️", + aliases: ["information_source"], + tags: [], + category: "Symbols", + description: "information", + unicode_version: "3.0", + }, + { + emoji: "🆔", + aliases: ["id"], + tags: [], + category: "Symbols", + description: "ID button", + unicode_version: "6.0", + }, + { + emoji: "Ⓜ️", + aliases: ["m"], + tags: [], + category: "Symbols", + description: "circled M", + unicode_version: "", + }, + { + emoji: "🆕", + aliases: ["new"], + tags: ["fresh"], + category: "Symbols", + description: "NEW button", + unicode_version: "6.0", + }, + { + emoji: "🆖", + aliases: ["ng"], + tags: [], + category: "Symbols", + description: "NG button", + unicode_version: "6.0", + }, + { + emoji: "🅾️", + aliases: ["o2"], + tags: [], + category: "Symbols", + description: "O button (blood type)", + unicode_version: "6.0", + }, + { + emoji: "🆗", + aliases: ["ok"], + tags: ["yes"], + category: "Symbols", + description: "OK button", + unicode_version: "6.0", + }, + { + emoji: "🅿️", + aliases: ["parking"], + tags: [], + category: "Symbols", + description: "P button", + unicode_version: "5.2", + }, + { + emoji: "🆘", + aliases: ["sos"], + tags: ["help", "emergency"], + category: "Symbols", + description: "SOS button", + unicode_version: "6.0", + }, + { + emoji: "🆙", + aliases: ["up"], + tags: [], + category: "Symbols", + description: "UP! button", + unicode_version: "6.0", + }, + { + emoji: "🆚", + aliases: ["vs"], + tags: [], + category: "Symbols", + description: "VS button", + unicode_version: "6.0", + }, + { + emoji: "🈁", + aliases: ["koko"], + tags: [], + category: "Symbols", + description: "Japanese “here” button", + unicode_version: "6.0", + }, + { + emoji: "🈂️", + aliases: ["sa"], + tags: [], + category: "Symbols", + description: "Japanese “service charge” button", + unicode_version: "6.0", + }, + { + emoji: "🈷️", + aliases: ["u6708"], + tags: [], + category: "Symbols", + description: "Japanese “monthly amount” button", + unicode_version: "6.0", + }, + { + emoji: "🈶", + aliases: ["u6709"], + tags: [], + category: "Symbols", + description: "Japanese “not free of charge” button", + unicode_version: "6.0", + }, + { + emoji: "🈯", + aliases: ["u6307"], + tags: [], + category: "Symbols", + description: "Japanese “reserved” button", + unicode_version: "", + }, + { + emoji: "🉐", + aliases: ["ideograph_advantage"], + tags: [], + category: "Symbols", + description: "Japanese “bargain” button", + unicode_version: "6.0", + }, + { + emoji: "🈹", + aliases: ["u5272"], + tags: [], + category: "Symbols", + description: "Japanese “discount” button", + unicode_version: "6.0", + }, + { + emoji: "🈚", + aliases: ["u7121"], + tags: [], + category: "Symbols", + description: "Japanese “free of charge” button", + unicode_version: "", + }, + { + emoji: "🈲", + aliases: ["u7981"], + tags: [], + category: "Symbols", + description: "Japanese “prohibited” button", + unicode_version: "6.0", + }, + { + emoji: "🉑", + aliases: ["accept"], + tags: [], + category: "Symbols", + description: "Japanese “acceptable” button", + unicode_version: "6.0", + }, + { + emoji: "🈸", + aliases: ["u7533"], + tags: [], + category: "Symbols", + description: "Japanese “application” button", + unicode_version: "6.0", + }, + { + emoji: "🈴", + aliases: ["u5408"], + tags: [], + category: "Symbols", + description: "Japanese “passing grade” button", + unicode_version: "6.0", + }, + { + emoji: "🈳", + aliases: ["u7a7a"], + tags: [], + category: "Symbols", + description: "Japanese “vacancy” button", + unicode_version: "6.0", + }, + { + emoji: "㊗️", + aliases: ["congratulations"], + tags: [], + category: "Symbols", + description: "Japanese “congratulations” button", + unicode_version: "", + }, + { + emoji: "㊙️", + aliases: ["secret"], + tags: [], + category: "Symbols", + description: "Japanese “secret” button", + unicode_version: "", + }, + { + emoji: "🈺", + aliases: ["u55b6"], + tags: [], + category: "Symbols", + description: "Japanese “open for business” button", + unicode_version: "6.0", + }, + { + emoji: "🈵", + aliases: ["u6e80"], + tags: [], + category: "Symbols", + description: "Japanese “no vacancy” button", + unicode_version: "6.0", + }, + { + emoji: "🔴", + aliases: ["red_circle"], + tags: [], + category: "Symbols", + description: "red circle", + unicode_version: "6.0", + }, + { + emoji: "🟠", + aliases: ["orange_circle"], + tags: [], + category: "Symbols", + description: "orange circle", + unicode_version: "12.0", + }, + { + emoji: "🟡", + aliases: ["yellow_circle"], + tags: [], + category: "Symbols", + description: "yellow circle", + unicode_version: "12.0", + }, + { + emoji: "🟢", + aliases: ["green_circle"], + tags: [], + category: "Symbols", + description: "green circle", + unicode_version: "12.0", + }, + { + emoji: "🔵", + aliases: ["large_blue_circle"], + tags: [], + category: "Symbols", + description: "blue circle", + unicode_version: "6.0", + }, + { + emoji: "🟣", + aliases: ["purple_circle"], + tags: [], + category: "Symbols", + description: "purple circle", + unicode_version: "12.0", + }, + { + emoji: "🟤", + aliases: ["brown_circle"], + tags: [], + category: "Symbols", + description: "brown circle", + unicode_version: "12.0", + }, + { + emoji: "⚫", + aliases: ["black_circle"], + tags: [], + category: "Symbols", + description: "black circle", + unicode_version: "4.1", + }, + { + emoji: "⚪", + aliases: ["white_circle"], + tags: [], + category: "Symbols", + description: "white circle", + unicode_version: "4.1", + }, + { + emoji: "🟥", + aliases: ["red_square"], + tags: [], + category: "Symbols", + description: "red square", + unicode_version: "12.0", + }, + { + emoji: "🟧", + aliases: ["orange_square"], + tags: [], + category: "Symbols", + description: "orange square", + unicode_version: "12.0", + }, + { + emoji: "🟨", + aliases: ["yellow_square"], + tags: [], + category: "Symbols", + description: "yellow square", + unicode_version: "12.0", + }, + { + emoji: "🟩", + aliases: ["green_square"], + tags: [], + category: "Symbols", + description: "green square", + unicode_version: "12.0", + }, + { + emoji: "🟦", + aliases: ["blue_square"], + tags: [], + category: "Symbols", + description: "blue square", + unicode_version: "12.0", + }, + { + emoji: "🟪", + aliases: ["purple_square"], + tags: [], + category: "Symbols", + description: "purple square", + unicode_version: "12.0", + }, + { + emoji: "🟫", + aliases: ["brown_square"], + tags: [], + category: "Symbols", + description: "brown square", + unicode_version: "12.0", + }, + { + emoji: "⬛", + aliases: ["black_large_square"], + tags: [], + category: "Symbols", + description: "black large square", + unicode_version: "5.1", + }, + { + emoji: "⬜", + aliases: ["white_large_square"], + tags: [], + category: "Symbols", + description: "white large square", + unicode_version: "5.1", + }, + { + emoji: "◼️", + aliases: ["black_medium_square"], + tags: [], + category: "Symbols", + description: "black medium square", + unicode_version: "3.2", + }, + { + emoji: "◻️", + aliases: ["white_medium_square"], + tags: [], + category: "Symbols", + description: "white medium square", + unicode_version: "3.2", + }, + { + emoji: "◾", + aliases: ["black_medium_small_square"], + tags: [], + category: "Symbols", + description: "black medium-small square", + unicode_version: "3.2", + }, + { + emoji: "◽", + aliases: ["white_medium_small_square"], + tags: [], + category: "Symbols", + description: "white medium-small square", + unicode_version: "3.2", + }, + { + emoji: "▪️", + aliases: ["black_small_square"], + tags: [], + category: "Symbols", + description: "black small square", + unicode_version: "", + }, + { + emoji: "▫️", + aliases: ["white_small_square"], + tags: [], + category: "Symbols", + description: "white small square", + unicode_version: "", + }, + { + emoji: "🔶", + aliases: ["large_orange_diamond"], + tags: [], + category: "Symbols", + description: "large orange diamond", + unicode_version: "6.0", + }, + { + emoji: "🔷", + aliases: ["large_blue_diamond"], + tags: [], + category: "Symbols", + description: "large blue diamond", + unicode_version: "6.0", + }, + { + emoji: "🔸", + aliases: ["small_orange_diamond"], + tags: [], + category: "Symbols", + description: "small orange diamond", + unicode_version: "6.0", + }, + { + emoji: "🔹", + aliases: ["small_blue_diamond"], + tags: [], + category: "Symbols", + description: "small blue diamond", + unicode_version: "6.0", + }, + { + emoji: "🔺", + aliases: ["small_red_triangle"], + tags: [], + category: "Symbols", + description: "red triangle pointed up", + unicode_version: "6.0", + }, + { + emoji: "🔻", + aliases: ["small_red_triangle_down"], + tags: [], + category: "Symbols", + description: "red triangle pointed down", + unicode_version: "6.0", + }, + { + emoji: "💠", + aliases: ["diamond_shape_with_a_dot_inside"], + tags: [], + category: "Symbols", + description: "diamond with a dot", + unicode_version: "6.0", + }, + { + emoji: "🔘", + aliases: ["radio_button"], + tags: [], + category: "Symbols", + description: "radio button", + unicode_version: "6.0", + }, + { + emoji: "🔳", + aliases: ["white_square_button"], + tags: [], + category: "Symbols", + description: "white square button", + unicode_version: "6.0", + }, + { + emoji: "🔲", + aliases: ["black_square_button"], + tags: [], + category: "Symbols", + description: "black square button", + unicode_version: "6.0", + }, + { + emoji: "🏁", + aliases: ["checkered_flag"], + tags: ["milestone", "finish"], + category: "Flags", + description: "chequered flag", + unicode_version: "6.0", + }, + { + emoji: "🚩", + aliases: ["triangular_flag_on_post"], + tags: [], + category: "Flags", + description: "triangular flag", + unicode_version: "6.0", + }, + { + emoji: "🎌", + aliases: ["crossed_flags"], + tags: [], + category: "Flags", + description: "crossed flags", + unicode_version: "6.0", + }, + { + emoji: "🏴", + aliases: ["black_flag"], + tags: [], + category: "Flags", + description: "black flag", + unicode_version: "7.0", + }, + { + emoji: "🏳️", + aliases: ["white_flag"], + tags: [], + category: "Flags", + description: "white flag", + unicode_version: "7.0", + }, + { + emoji: "🏳️‍🌈", + aliases: ["rainbow_flag"], + tags: ["pride"], + category: "Flags", + description: "rainbow flag", + unicode_version: "6.0", + }, + { + emoji: "🏳️‍⚧️", + aliases: ["transgender_flag"], + tags: [], + category: "Flags", + description: "transgender flag", + unicode_version: "13.0", + }, + { + emoji: "🏴‍☠️", + aliases: ["pirate_flag"], + tags: [], + category: "Flags", + description: "pirate flag", + unicode_version: "11.0", + }, + { + emoji: "🇦🇨", + aliases: ["ascension_island"], + tags: [], + category: "Flags", + description: "flag: Ascension Island", + unicode_version: "11.0", + }, + { + emoji: "🇦🇩", + aliases: ["andorra"], + tags: [], + category: "Flags", + description: "flag: Andorra", + unicode_version: "6.0", + }, + { + emoji: "🇦🇪", + aliases: ["united_arab_emirates"], + tags: [], + category: "Flags", + description: "flag: United Arab Emirates", + unicode_version: "6.0", + }, + { + emoji: "🇦🇫", + aliases: ["afghanistan"], + tags: [], + category: "Flags", + description: "flag: Afghanistan", + unicode_version: "6.0", + }, + { + emoji: "🇦🇬", + aliases: ["antigua_barbuda"], + tags: [], + category: "Flags", + description: "flag: Antigua & Barbuda", + unicode_version: "6.0", + }, + { + emoji: "🇦🇮", + aliases: ["anguilla"], + tags: [], + category: "Flags", + description: "flag: Anguilla", + unicode_version: "6.0", + }, + { + emoji: "🇦🇱", + aliases: ["albania"], + tags: [], + category: "Flags", + description: "flag: Albania", + unicode_version: "6.0", + }, + { + emoji: "🇦🇲", + aliases: ["armenia"], + tags: [], + category: "Flags", + description: "flag: Armenia", + unicode_version: "6.0", + }, + { + emoji: "🇦🇴", + aliases: ["angola"], + tags: [], + category: "Flags", + description: "flag: Angola", + unicode_version: "6.0", + }, + { + emoji: "🇦🇶", + aliases: ["antarctica"], + tags: [], + category: "Flags", + description: "flag: Antarctica", + unicode_version: "6.0", + }, + { + emoji: "🇦🇷", + aliases: ["argentina"], + tags: [], + category: "Flags", + description: "flag: Argentina", + unicode_version: "6.0", + }, + { + emoji: "🇦🇸", + aliases: ["american_samoa"], + tags: [], + category: "Flags", + description: "flag: American Samoa", + unicode_version: "6.0", + }, + { + emoji: "🇦🇹", + aliases: ["austria"], + tags: [], + category: "Flags", + description: "flag: Austria", + unicode_version: "6.0", + }, + { + emoji: "🇦🇺", + aliases: ["australia"], + tags: [], + category: "Flags", + description: "flag: Australia", + unicode_version: "6.0", + }, + { + emoji: "🇦🇼", + aliases: ["aruba"], + tags: [], + category: "Flags", + description: "flag: Aruba", + unicode_version: "6.0", + }, + { + emoji: "🇦🇽", + aliases: ["aland_islands"], + tags: [], + category: "Flags", + description: "flag: Åland Islands", + unicode_version: "6.0", + }, + { + emoji: "🇦🇿", + aliases: ["azerbaijan"], + tags: [], + category: "Flags", + description: "flag: Azerbaijan", + unicode_version: "6.0", + }, + { + emoji: "🇧🇦", + aliases: ["bosnia_herzegovina"], + tags: [], + category: "Flags", + description: "flag: Bosnia & Herzegovina", + unicode_version: "6.0", + }, + { + emoji: "🇧🇧", + aliases: ["barbados"], + tags: [], + category: "Flags", + description: "flag: Barbados", + unicode_version: "6.0", + }, + { + emoji: "🇧🇩", + aliases: ["bangladesh"], + tags: [], + category: "Flags", + description: "flag: Bangladesh", + unicode_version: "6.0", + }, + { + emoji: "🇧🇪", + aliases: ["belgium"], + tags: [], + category: "Flags", + description: "flag: Belgium", + unicode_version: "6.0", + }, + { + emoji: "🇧🇫", + aliases: ["burkina_faso"], + tags: [], + category: "Flags", + description: "flag: Burkina Faso", + unicode_version: "6.0", + }, + { + emoji: "🇧🇬", + aliases: ["bulgaria"], + tags: [], + category: "Flags", + description: "flag: Bulgaria", + unicode_version: "6.0", + }, + { + emoji: "🇧🇭", + aliases: ["bahrain"], + tags: [], + category: "Flags", + description: "flag: Bahrain", + unicode_version: "6.0", + }, + { + emoji: "🇧🇮", + aliases: ["burundi"], + tags: [], + category: "Flags", + description: "flag: Burundi", + unicode_version: "6.0", + }, + { + emoji: "🇧🇯", + aliases: ["benin"], + tags: [], + category: "Flags", + description: "flag: Benin", + unicode_version: "6.0", + }, + { + emoji: "🇧🇱", + aliases: ["st_barthelemy"], + tags: [], + category: "Flags", + description: "flag: St. Barthélemy", + unicode_version: "6.0", + }, + { + emoji: "🇧🇲", + aliases: ["bermuda"], + tags: [], + category: "Flags", + description: "flag: Bermuda", + unicode_version: "6.0", + }, + { + emoji: "🇧🇳", + aliases: ["brunei"], + tags: [], + category: "Flags", + description: "flag: Brunei", + unicode_version: "6.0", + }, + { + emoji: "🇧🇴", + aliases: ["bolivia"], + tags: [], + category: "Flags", + description: "flag: Bolivia", + unicode_version: "6.0", + }, + { + emoji: "🇧🇶", + aliases: ["caribbean_netherlands"], + tags: [], + category: "Flags", + description: "flag: Caribbean Netherlands", + unicode_version: "6.0", + }, + { + emoji: "🇧🇷", + aliases: ["brazil"], + tags: [], + category: "Flags", + description: "flag: Brazil", + unicode_version: "6.0", + }, + { + emoji: "🇧🇸", + aliases: ["bahamas"], + tags: [], + category: "Flags", + description: "flag: Bahamas", + unicode_version: "6.0", + }, + { + emoji: "🇧🇹", + aliases: ["bhutan"], + tags: [], + category: "Flags", + description: "flag: Bhutan", + unicode_version: "6.0", + }, + { + emoji: "🇧🇻", + aliases: ["bouvet_island"], + tags: [], + category: "Flags", + description: "flag: Bouvet Island", + unicode_version: "11.0", + }, + { + emoji: "🇧🇼", + aliases: ["botswana"], + tags: [], + category: "Flags", + description: "flag: Botswana", + unicode_version: "6.0", + }, + { + emoji: "🇧🇾", + aliases: ["belarus"], + tags: [], + category: "Flags", + description: "flag: Belarus", + unicode_version: "6.0", + }, + { + emoji: "🇧🇿", + aliases: ["belize"], + tags: [], + category: "Flags", + description: "flag: Belize", + unicode_version: "6.0", + }, + { + emoji: "🇨🇦", + aliases: ["canada"], + tags: [], + category: "Flags", + description: "flag: Canada", + unicode_version: "6.0", + }, + { + emoji: "🇨🇨", + aliases: ["cocos_islands"], + tags: ["keeling"], + category: "Flags", + description: "flag: Cocos (Keeling) Islands", + unicode_version: "6.0", + }, + { + emoji: "🇨🇩", + aliases: ["congo_kinshasa"], + tags: [], + category: "Flags", + description: "flag: Congo - Kinshasa", + unicode_version: "6.0", + }, + { + emoji: "🇨🇫", + aliases: ["central_african_republic"], + tags: [], + category: "Flags", + description: "flag: Central African Republic", + unicode_version: "6.0", + }, + { + emoji: "🇨🇬", + aliases: ["congo_brazzaville"], + tags: [], + category: "Flags", + description: "flag: Congo - Brazzaville", + unicode_version: "6.0", + }, + { + emoji: "🇨🇭", + aliases: ["switzerland"], + tags: [], + category: "Flags", + description: "flag: Switzerland", + unicode_version: "6.0", + }, + { + emoji: "🇨🇮", + aliases: ["cote_divoire"], + tags: ["ivory"], + category: "Flags", + description: "flag: Côte d’Ivoire", + unicode_version: "6.0", + }, + { + emoji: "🇨🇰", + aliases: ["cook_islands"], + tags: [], + category: "Flags", + description: "flag: Cook Islands", + unicode_version: "6.0", + }, + { + emoji: "🇨🇱", + aliases: ["chile"], + tags: [], + category: "Flags", + description: "flag: Chile", + unicode_version: "6.0", + }, + { + emoji: "🇨🇲", + aliases: ["cameroon"], + tags: [], + category: "Flags", + description: "flag: Cameroon", + unicode_version: "6.0", + }, + { + emoji: "🇨🇳", + aliases: ["cn"], + tags: ["china"], + category: "Flags", + description: "flag: China", + unicode_version: "6.0", + }, + { + emoji: "🇨🇴", + aliases: ["colombia"], + tags: [], + category: "Flags", + description: "flag: Colombia", + unicode_version: "6.0", + }, + { + emoji: "🇨🇵", + aliases: ["clipperton_island"], + tags: [], + category: "Flags", + description: "flag: Clipperton Island", + unicode_version: "11.0", + }, + { + emoji: "🇨🇷", + aliases: ["costa_rica"], + tags: [], + category: "Flags", + description: "flag: Costa Rica", + unicode_version: "6.0", + }, + { + emoji: "🇨🇺", + aliases: ["cuba"], + tags: [], + category: "Flags", + description: "flag: Cuba", + unicode_version: "6.0", + }, + { + emoji: "🇨🇻", + aliases: ["cape_verde"], + tags: [], + category: "Flags", + description: "flag: Cape Verde", + unicode_version: "6.0", + }, + { + emoji: "🇨🇼", + aliases: ["curacao"], + tags: [], + category: "Flags", + description: "flag: Curaçao", + unicode_version: "6.0", + }, + { + emoji: "🇨🇽", + aliases: ["christmas_island"], + tags: [], + category: "Flags", + description: "flag: Christmas Island", + unicode_version: "6.0", + }, + { + emoji: "🇨🇾", + aliases: ["cyprus"], + tags: [], + category: "Flags", + description: "flag: Cyprus", + unicode_version: "6.0", + }, + { + emoji: "🇨🇿", + aliases: ["czech_republic"], + tags: [], + category: "Flags", + description: "flag: Czechia", + unicode_version: "6.0", + }, + { + emoji: "🇩🇪", + aliases: ["de"], + tags: ["flag", "germany"], + category: "Flags", + description: "flag: Germany", + unicode_version: "6.0", + }, + { + emoji: "🇩🇬", + aliases: ["diego_garcia"], + tags: [], + category: "Flags", + description: "flag: Diego Garcia", + unicode_version: "11.0", + }, + { + emoji: "🇩🇯", + aliases: ["djibouti"], + tags: [], + category: "Flags", + description: "flag: Djibouti", + unicode_version: "6.0", + }, + { + emoji: "🇩🇰", + aliases: ["denmark"], + tags: [], + category: "Flags", + description: "flag: Denmark", + unicode_version: "6.0", + }, + { + emoji: "🇩🇲", + aliases: ["dominica"], + tags: [], + category: "Flags", + description: "flag: Dominica", + unicode_version: "6.0", + }, + { + emoji: "🇩🇴", + aliases: ["dominican_republic"], + tags: [], + category: "Flags", + description: "flag: Dominican Republic", + unicode_version: "6.0", + }, + { + emoji: "🇩🇿", + aliases: ["algeria"], + tags: [], + category: "Flags", + description: "flag: Algeria", + unicode_version: "6.0", + }, + { + emoji: "🇪🇦", + aliases: ["ceuta_melilla"], + tags: [], + category: "Flags", + description: "flag: Ceuta & Melilla", + unicode_version: "11.0", + }, + { + emoji: "🇪🇨", + aliases: ["ecuador"], + tags: [], + category: "Flags", + description: "flag: Ecuador", + unicode_version: "6.0", + }, + { + emoji: "🇪🇪", + aliases: ["estonia"], + tags: [], + category: "Flags", + description: "flag: Estonia", + unicode_version: "6.0", + }, + { + emoji: "🇪🇬", + aliases: ["egypt"], + tags: [], + category: "Flags", + description: "flag: Egypt", + unicode_version: "6.0", + }, + { + emoji: "🇪🇭", + aliases: ["western_sahara"], + tags: [], + category: "Flags", + description: "flag: Western Sahara", + unicode_version: "6.0", + }, + { + emoji: "🇪🇷", + aliases: ["eritrea"], + tags: [], + category: "Flags", + description: "flag: Eritrea", + unicode_version: "6.0", + }, + { + emoji: "🇪🇸", + aliases: ["es"], + tags: ["spain"], + category: "Flags", + description: "flag: Spain", + unicode_version: "6.0", + }, + { + emoji: "🇪🇹", + aliases: ["ethiopia"], + tags: [], + category: "Flags", + description: "flag: Ethiopia", + unicode_version: "6.0", + }, + { + emoji: "🇪🇺", + aliases: ["eu", "european_union"], + tags: [], + category: "Flags", + description: "flag: European Union", + unicode_version: "6.0", + }, + { + emoji: "🇫🇮", + aliases: ["finland"], + tags: [], + category: "Flags", + description: "flag: Finland", + unicode_version: "6.0", + }, + { + emoji: "🇫🇯", + aliases: ["fiji"], + tags: [], + category: "Flags", + description: "flag: Fiji", + unicode_version: "6.0", + }, + { + emoji: "🇫🇰", + aliases: ["falkland_islands"], + tags: [], + category: "Flags", + description: "flag: Falkland Islands", + unicode_version: "6.0", + }, + { + emoji: "🇫🇲", + aliases: ["micronesia"], + tags: [], + category: "Flags", + description: "flag: Micronesia", + unicode_version: "6.0", + }, + { + emoji: "🇫🇴", + aliases: ["faroe_islands"], + tags: [], + category: "Flags", + description: "flag: Faroe Islands", + unicode_version: "6.0", + }, + { + emoji: "🇫🇷", + aliases: ["fr"], + tags: ["france", "french"], + category: "Flags", + description: "flag: France", + unicode_version: "6.0", + }, + { + emoji: "🇬🇦", + aliases: ["gabon"], + tags: [], + category: "Flags", + description: "flag: Gabon", + unicode_version: "6.0", + }, + { + emoji: "🇬🇧", + aliases: ["gb", "uk"], + tags: ["flag", "british"], + category: "Flags", + description: "flag: United Kingdom", + unicode_version: "6.0", + }, + { + emoji: "🇬🇩", + aliases: ["grenada"], + tags: [], + category: "Flags", + description: "flag: Grenada", + unicode_version: "6.0", + }, + { + emoji: "🇬🇪", + aliases: ["georgia"], + tags: [], + category: "Flags", + description: "flag: Georgia", + unicode_version: "6.0", + }, + { + emoji: "🇬🇫", + aliases: ["french_guiana"], + tags: [], + category: "Flags", + description: "flag: French Guiana", + unicode_version: "6.0", + }, + { + emoji: "🇬🇬", + aliases: ["guernsey"], + tags: [], + category: "Flags", + description: "flag: Guernsey", + unicode_version: "6.0", + }, + { + emoji: "🇬🇭", + aliases: ["ghana"], + tags: [], + category: "Flags", + description: "flag: Ghana", + unicode_version: "6.0", + }, + { + emoji: "🇬🇮", + aliases: ["gibraltar"], + tags: [], + category: "Flags", + description: "flag: Gibraltar", + unicode_version: "6.0", + }, + { + emoji: "🇬🇱", + aliases: ["greenland"], + tags: [], + category: "Flags", + description: "flag: Greenland", + unicode_version: "6.0", + }, + { + emoji: "🇬🇲", + aliases: ["gambia"], + tags: [], + category: "Flags", + description: "flag: Gambia", + unicode_version: "6.0", + }, + { + emoji: "🇬🇳", + aliases: ["guinea"], + tags: [], + category: "Flags", + description: "flag: Guinea", + unicode_version: "6.0", + }, + { + emoji: "🇬🇵", + aliases: ["guadeloupe"], + tags: [], + category: "Flags", + description: "flag: Guadeloupe", + unicode_version: "6.0", + }, + { + emoji: "🇬🇶", + aliases: ["equatorial_guinea"], + tags: [], + category: "Flags", + description: "flag: Equatorial Guinea", + unicode_version: "6.0", + }, + { + emoji: "🇬🇷", + aliases: ["greece"], + tags: [], + category: "Flags", + description: "flag: Greece", + unicode_version: "6.0", + }, + { + emoji: "🇬🇸", + aliases: ["south_georgia_south_sandwich_islands"], + tags: [], + category: "Flags", + description: "flag: South Georgia & South Sandwich Islands", + unicode_version: "6.0", + }, + { + emoji: "🇬🇹", + aliases: ["guatemala"], + tags: [], + category: "Flags", + description: "flag: Guatemala", + unicode_version: "6.0", + }, + { + emoji: "🇬🇺", + aliases: ["guam"], + tags: [], + category: "Flags", + description: "flag: Guam", + unicode_version: "6.0", + }, + { + emoji: "🇬🇼", + aliases: ["guinea_bissau"], + tags: [], + category: "Flags", + description: "flag: Guinea-Bissau", + unicode_version: "6.0", + }, + { + emoji: "🇬🇾", + aliases: ["guyana"], + tags: [], + category: "Flags", + description: "flag: Guyana", + unicode_version: "6.0", + }, + { + emoji: "🇭🇰", + aliases: ["hong_kong"], + tags: [], + category: "Flags", + description: "flag: Hong Kong SAR China", + unicode_version: "6.0", + }, + { + emoji: "🇭🇲", + aliases: ["heard_mcdonald_islands"], + tags: [], + category: "Flags", + description: "flag: Heard & McDonald Islands", + unicode_version: "11.0", + }, + { + emoji: "🇭🇳", + aliases: ["honduras"], + tags: [], + category: "Flags", + description: "flag: Honduras", + unicode_version: "6.0", + }, + { + emoji: "🇭🇷", + aliases: ["croatia"], + tags: [], + category: "Flags", + description: "flag: Croatia", + unicode_version: "6.0", + }, + { + emoji: "🇭🇹", + aliases: ["haiti"], + tags: [], + category: "Flags", + description: "flag: Haiti", + unicode_version: "6.0", + }, + { + emoji: "🇭🇺", + aliases: ["hungary"], + tags: [], + category: "Flags", + description: "flag: Hungary", + unicode_version: "6.0", + }, + { + emoji: "🇮🇨", + aliases: ["canary_islands"], + tags: [], + category: "Flags", + description: "flag: Canary Islands", + unicode_version: "6.0", + }, + { + emoji: "🇮🇩", + aliases: ["indonesia"], + tags: [], + category: "Flags", + description: "flag: Indonesia", + unicode_version: "6.0", + }, + { + emoji: "🇮🇪", + aliases: ["ireland"], + tags: [], + category: "Flags", + description: "flag: Ireland", + unicode_version: "6.0", + }, + { + emoji: "🇮🇱", + aliases: ["israel"], + tags: [], + category: "Flags", + description: "flag: Israel", + unicode_version: "6.0", + }, + { + emoji: "🇮🇲", + aliases: ["isle_of_man"], + tags: [], + category: "Flags", + description: "flag: Isle of Man", + unicode_version: "6.0", + }, + { + emoji: "🇮🇳", + aliases: ["india"], + tags: [], + category: "Flags", + description: "flag: India", + unicode_version: "6.0", + }, + { + emoji: "🇮🇴", + aliases: ["british_indian_ocean_territory"], + tags: [], + category: "Flags", + description: "flag: British Indian Ocean Territory", + unicode_version: "6.0", + }, + { + emoji: "🇮🇶", + aliases: ["iraq"], + tags: [], + category: "Flags", + description: "flag: Iraq", + unicode_version: "6.0", + }, + { + emoji: "🇮🇷", + aliases: ["iran"], + tags: [], + category: "Flags", + description: "flag: Iran", + unicode_version: "6.0", + }, + { + emoji: "🇮🇸", + aliases: ["iceland"], + tags: [], + category: "Flags", + description: "flag: Iceland", + unicode_version: "6.0", + }, + { + emoji: "🇮🇹", + aliases: ["it"], + tags: ["italy"], + category: "Flags", + description: "flag: Italy", + unicode_version: "6.0", + }, + { + emoji: "🇯🇪", + aliases: ["jersey"], + tags: [], + category: "Flags", + description: "flag: Jersey", + unicode_version: "6.0", + }, + { + emoji: "🇯🇲", + aliases: ["jamaica"], + tags: [], + category: "Flags", + description: "flag: Jamaica", + unicode_version: "6.0", + }, + { + emoji: "🇯🇴", + aliases: ["jordan"], + tags: [], + category: "Flags", + description: "flag: Jordan", + unicode_version: "6.0", + }, + { + emoji: "🇯🇵", + aliases: ["jp"], + tags: ["japan"], + category: "Flags", + description: "flag: Japan", + unicode_version: "6.0", + }, + { + emoji: "🇰🇪", + aliases: ["kenya"], + tags: [], + category: "Flags", + description: "flag: Kenya", + unicode_version: "6.0", + }, + { + emoji: "🇰🇬", + aliases: ["kyrgyzstan"], + tags: [], + category: "Flags", + description: "flag: Kyrgyzstan", + unicode_version: "6.0", + }, + { + emoji: "🇰🇭", + aliases: ["cambodia"], + tags: [], + category: "Flags", + description: "flag: Cambodia", + unicode_version: "6.0", + }, + { + emoji: "🇰🇮", + aliases: ["kiribati"], + tags: [], + category: "Flags", + description: "flag: Kiribati", + unicode_version: "6.0", + }, + { + emoji: "🇰🇲", + aliases: ["comoros"], + tags: [], + category: "Flags", + description: "flag: Comoros", + unicode_version: "6.0", + }, + { + emoji: "🇰🇳", + aliases: ["st_kitts_nevis"], + tags: [], + category: "Flags", + description: "flag: St. Kitts & Nevis", + unicode_version: "6.0", + }, + { + emoji: "🇰🇵", + aliases: ["north_korea"], + tags: [], + category: "Flags", + description: "flag: North Korea", + unicode_version: "6.0", + }, + { + emoji: "🇰🇷", + aliases: ["kr"], + tags: ["korea"], + category: "Flags", + description: "flag: South Korea", + unicode_version: "6.0", + }, + { + emoji: "🇰🇼", + aliases: ["kuwait"], + tags: [], + category: "Flags", + description: "flag: Kuwait", + unicode_version: "6.0", + }, + { + emoji: "🇰🇾", + aliases: ["cayman_islands"], + tags: [], + category: "Flags", + description: "flag: Cayman Islands", + unicode_version: "6.0", + }, + { + emoji: "🇰🇿", + aliases: ["kazakhstan"], + tags: [], + category: "Flags", + description: "flag: Kazakhstan", + unicode_version: "6.0", + }, + { + emoji: "🇱🇦", + aliases: ["laos"], + tags: [], + category: "Flags", + description: "flag: Laos", + unicode_version: "6.0", + }, + { + emoji: "🇱🇧", + aliases: ["lebanon"], + tags: [], + category: "Flags", + description: "flag: Lebanon", + unicode_version: "6.0", + }, + { + emoji: "🇱🇨", + aliases: ["st_lucia"], + tags: [], + category: "Flags", + description: "flag: St. Lucia", + unicode_version: "6.0", + }, + { + emoji: "🇱🇮", + aliases: ["liechtenstein"], + tags: [], + category: "Flags", + description: "flag: Liechtenstein", + unicode_version: "6.0", + }, + { + emoji: "🇱🇰", + aliases: ["sri_lanka"], + tags: [], + category: "Flags", + description: "flag: Sri Lanka", + unicode_version: "6.0", + }, + { + emoji: "🇱🇷", + aliases: ["liberia"], + tags: [], + category: "Flags", + description: "flag: Liberia", + unicode_version: "6.0", + }, + { + emoji: "🇱🇸", + aliases: ["lesotho"], + tags: [], + category: "Flags", + description: "flag: Lesotho", + unicode_version: "6.0", + }, + { + emoji: "🇱🇹", + aliases: ["lithuania"], + tags: [], + category: "Flags", + description: "flag: Lithuania", + unicode_version: "6.0", + }, + { + emoji: "🇱🇺", + aliases: ["luxembourg"], + tags: [], + category: "Flags", + description: "flag: Luxembourg", + unicode_version: "6.0", + }, + { + emoji: "🇱🇻", + aliases: ["latvia"], + tags: [], + category: "Flags", + description: "flag: Latvia", + unicode_version: "6.0", + }, + { + emoji: "🇱🇾", + aliases: ["libya"], + tags: [], + category: "Flags", + description: "flag: Libya", + unicode_version: "6.0", + }, + { + emoji: "🇲🇦", + aliases: ["morocco"], + tags: [], + category: "Flags", + description: "flag: Morocco", + unicode_version: "6.0", + }, + { + emoji: "🇲🇨", + aliases: ["monaco"], + tags: [], + category: "Flags", + description: "flag: Monaco", + unicode_version: "6.0", + }, + { + emoji: "🇲🇩", + aliases: ["moldova"], + tags: [], + category: "Flags", + description: "flag: Moldova", + unicode_version: "6.0", + }, + { + emoji: "🇲🇪", + aliases: ["montenegro"], + tags: [], + category: "Flags", + description: "flag: Montenegro", + unicode_version: "6.0", + }, + { + emoji: "🇲🇫", + aliases: ["st_martin"], + tags: [], + category: "Flags", + description: "flag: St. Martin", + unicode_version: "11.0", + }, + { + emoji: "🇲🇬", + aliases: ["madagascar"], + tags: [], + category: "Flags", + description: "flag: Madagascar", + unicode_version: "6.0", + }, + { + emoji: "🇲🇭", + aliases: ["marshall_islands"], + tags: [], + category: "Flags", + description: "flag: Marshall Islands", + unicode_version: "6.0", + }, + { + emoji: "🇲🇰", + aliases: ["macedonia"], + tags: [], + category: "Flags", + description: "flag: North Macedonia", + unicode_version: "6.0", + }, + { + emoji: "🇲🇱", + aliases: ["mali"], + tags: [], + category: "Flags", + description: "flag: Mali", + unicode_version: "6.0", + }, + { + emoji: "🇲🇲", + aliases: ["myanmar"], + tags: ["burma"], + category: "Flags", + description: "flag: Myanmar (Burma)", + unicode_version: "6.0", + }, + { + emoji: "🇲🇳", + aliases: ["mongolia"], + tags: [], + category: "Flags", + description: "flag: Mongolia", + unicode_version: "6.0", + }, + { + emoji: "🇲🇴", + aliases: ["macau"], + tags: [], + category: "Flags", + description: "flag: Macao SAR China", + unicode_version: "6.0", + }, + { + emoji: "🇲🇵", + aliases: ["northern_mariana_islands"], + tags: [], + category: "Flags", + description: "flag: Northern Mariana Islands", + unicode_version: "6.0", + }, + { + emoji: "🇲🇶", + aliases: ["martinique"], + tags: [], + category: "Flags", + description: "flag: Martinique", + unicode_version: "6.0", + }, + { + emoji: "🇲🇷", + aliases: ["mauritania"], + tags: [], + category: "Flags", + description: "flag: Mauritania", + unicode_version: "6.0", + }, + { + emoji: "🇲🇸", + aliases: ["montserrat"], + tags: [], + category: "Flags", + description: "flag: Montserrat", + unicode_version: "6.0", + }, + { + emoji: "🇲🇹", + aliases: ["malta"], + tags: [], + category: "Flags", + description: "flag: Malta", + unicode_version: "6.0", + }, + { + emoji: "🇲🇺", + aliases: ["mauritius"], + tags: [], + category: "Flags", + description: "flag: Mauritius", + unicode_version: "6.0", + }, + { + emoji: "🇲🇻", + aliases: ["maldives"], + tags: [], + category: "Flags", + description: "flag: Maldives", + unicode_version: "6.0", + }, + { + emoji: "🇲🇼", + aliases: ["malawi"], + tags: [], + category: "Flags", + description: "flag: Malawi", + unicode_version: "6.0", + }, + { + emoji: "🇲🇽", + aliases: ["mexico"], + tags: [], + category: "Flags", + description: "flag: Mexico", + unicode_version: "6.0", + }, + { + emoji: "🇲🇾", + aliases: ["malaysia"], + tags: [], + category: "Flags", + description: "flag: Malaysia", + unicode_version: "6.0", + }, + { + emoji: "🇲🇿", + aliases: ["mozambique"], + tags: [], + category: "Flags", + description: "flag: Mozambique", + unicode_version: "6.0", + }, + { + emoji: "🇳🇦", + aliases: ["namibia"], + tags: [], + category: "Flags", + description: "flag: Namibia", + unicode_version: "6.0", + }, + { + emoji: "🇳🇨", + aliases: ["new_caledonia"], + tags: [], + category: "Flags", + description: "flag: New Caledonia", + unicode_version: "6.0", + }, + { + emoji: "🇳🇪", + aliases: ["niger"], + tags: [], + category: "Flags", + description: "flag: Niger", + unicode_version: "6.0", + }, + { + emoji: "🇳🇫", + aliases: ["norfolk_island"], + tags: [], + category: "Flags", + description: "flag: Norfolk Island", + unicode_version: "6.0", + }, + { + emoji: "🇳🇬", + aliases: ["nigeria"], + tags: [], + category: "Flags", + description: "flag: Nigeria", + unicode_version: "6.0", + }, + { + emoji: "🇳🇮", + aliases: ["nicaragua"], + tags: [], + category: "Flags", + description: "flag: Nicaragua", + unicode_version: "6.0", + }, + { + emoji: "🇳🇱", + aliases: ["netherlands"], + tags: [], + category: "Flags", + description: "flag: Netherlands", + unicode_version: "6.0", + }, + { + emoji: "🇳🇴", + aliases: ["norway"], + tags: [], + category: "Flags", + description: "flag: Norway", + unicode_version: "6.0", + }, + { + emoji: "🇳🇵", + aliases: ["nepal"], + tags: [], + category: "Flags", + description: "flag: Nepal", + unicode_version: "6.0", + }, + { + emoji: "🇳🇷", + aliases: ["nauru"], + tags: [], + category: "Flags", + description: "flag: Nauru", + unicode_version: "6.0", + }, + { + emoji: "🇳🇺", + aliases: ["niue"], + tags: [], + category: "Flags", + description: "flag: Niue", + unicode_version: "6.0", + }, + { + emoji: "🇳🇿", + aliases: ["new_zealand"], + tags: [], + category: "Flags", + description: "flag: New Zealand", + unicode_version: "6.0", + }, + { + emoji: "🇴🇲", + aliases: ["oman"], + tags: [], + category: "Flags", + description: "flag: Oman", + unicode_version: "6.0", + }, + { + emoji: "🇵🇦", + aliases: ["panama"], + tags: [], + category: "Flags", + description: "flag: Panama", + unicode_version: "6.0", + }, + { + emoji: "🇵🇪", + aliases: ["peru"], + tags: [], + category: "Flags", + description: "flag: Peru", + unicode_version: "6.0", + }, + { + emoji: "🇵🇫", + aliases: ["french_polynesia"], + tags: [], + category: "Flags", + description: "flag: French Polynesia", + unicode_version: "6.0", + }, + { + emoji: "🇵🇬", + aliases: ["papua_new_guinea"], + tags: [], + category: "Flags", + description: "flag: Papua New Guinea", + unicode_version: "6.0", + }, + { + emoji: "🇵🇭", + aliases: ["philippines"], + tags: [], + category: "Flags", + description: "flag: Philippines", + unicode_version: "6.0", + }, + { + emoji: "🇵🇰", + aliases: ["pakistan"], + tags: [], + category: "Flags", + description: "flag: Pakistan", + unicode_version: "6.0", + }, + { + emoji: "🇵🇱", + aliases: ["poland"], + tags: [], + category: "Flags", + description: "flag: Poland", + unicode_version: "6.0", + }, + { + emoji: "🇵🇲", + aliases: ["st_pierre_miquelon"], + tags: [], + category: "Flags", + description: "flag: St. Pierre & Miquelon", + unicode_version: "6.0", + }, + { + emoji: "🇵🇳", + aliases: ["pitcairn_islands"], + tags: [], + category: "Flags", + description: "flag: Pitcairn Islands", + unicode_version: "6.0", + }, + { + emoji: "🇵🇷", + aliases: ["puerto_rico"], + tags: [], + category: "Flags", + description: "flag: Puerto Rico", + unicode_version: "6.0", + }, + { + emoji: "🇵🇸", + aliases: ["palestinian_territories"], + tags: [], + category: "Flags", + description: "flag: Palestinian Territories", + unicode_version: "6.0", + }, + { + emoji: "🇵🇹", + aliases: ["portugal"], + tags: [], + category: "Flags", + description: "flag: Portugal", + unicode_version: "6.0", + }, + { + emoji: "🇵🇼", + aliases: ["palau"], + tags: [], + category: "Flags", + description: "flag: Palau", + unicode_version: "6.0", + }, + { + emoji: "🇵🇾", + aliases: ["paraguay"], + tags: [], + category: "Flags", + description: "flag: Paraguay", + unicode_version: "6.0", + }, + { + emoji: "🇶🇦", + aliases: ["qatar"], + tags: [], + category: "Flags", + description: "flag: Qatar", + unicode_version: "6.0", + }, + { + emoji: "🇷🇪", + aliases: ["reunion"], + tags: [], + category: "Flags", + description: "flag: Réunion", + unicode_version: "6.0", + }, + { + emoji: "🇷🇴", + aliases: ["romania"], + tags: [], + category: "Flags", + description: "flag: Romania", + unicode_version: "6.0", + }, + { + emoji: "🇷🇸", + aliases: ["serbia"], + tags: [], + category: "Flags", + description: "flag: Serbia", + unicode_version: "6.0", + }, + { + emoji: "🇷🇺", + aliases: ["ru"], + tags: ["russia"], + category: "Flags", + description: "flag: Russia", + unicode_version: "6.0", + }, + { + emoji: "🇷🇼", + aliases: ["rwanda"], + tags: [], + category: "Flags", + description: "flag: Rwanda", + unicode_version: "6.0", + }, + { + emoji: "🇸🇦", + aliases: ["saudi_arabia"], + tags: [], + category: "Flags", + description: "flag: Saudi Arabia", + unicode_version: "6.0", + }, + { + emoji: "🇸🇧", + aliases: ["solomon_islands"], + tags: [], + category: "Flags", + description: "flag: Solomon Islands", + unicode_version: "6.0", + }, + { + emoji: "🇸🇨", + aliases: ["seychelles"], + tags: [], + category: "Flags", + description: "flag: Seychelles", + unicode_version: "6.0", + }, + { + emoji: "🇸🇩", + aliases: ["sudan"], + tags: [], + category: "Flags", + description: "flag: Sudan", + unicode_version: "6.0", + }, + { + emoji: "🇸🇪", + aliases: ["sweden"], + tags: [], + category: "Flags", + description: "flag: Sweden", + unicode_version: "6.0", + }, + { + emoji: "🇸🇬", + aliases: ["singapore"], + tags: [], + category: "Flags", + description: "flag: Singapore", + unicode_version: "6.0", + }, + { + emoji: "🇸🇭", + aliases: ["st_helena"], + tags: [], + category: "Flags", + description: "flag: St. Helena", + unicode_version: "6.0", + }, + { + emoji: "🇸🇮", + aliases: ["slovenia"], + tags: [], + category: "Flags", + description: "flag: Slovenia", + unicode_version: "6.0", + }, + { + emoji: "🇸🇯", + aliases: ["svalbard_jan_mayen"], + tags: [], + category: "Flags", + description: "flag: Svalbard & Jan Mayen", + unicode_version: "11.0", + }, + { + emoji: "🇸🇰", + aliases: ["slovakia"], + tags: [], + category: "Flags", + description: "flag: Slovakia", + unicode_version: "6.0", + }, + { + emoji: "🇸🇱", + aliases: ["sierra_leone"], + tags: [], + category: "Flags", + description: "flag: Sierra Leone", + unicode_version: "6.0", + }, + { + emoji: "🇸🇲", + aliases: ["san_marino"], + tags: [], + category: "Flags", + description: "flag: San Marino", + unicode_version: "6.0", + }, + { + emoji: "🇸🇳", + aliases: ["senegal"], + tags: [], + category: "Flags", + description: "flag: Senegal", + unicode_version: "6.0", + }, + { + emoji: "🇸🇴", + aliases: ["somalia"], + tags: [], + category: "Flags", + description: "flag: Somalia", + unicode_version: "6.0", + }, + { + emoji: "🇸🇷", + aliases: ["suriname"], + tags: [], + category: "Flags", + description: "flag: Suriname", + unicode_version: "6.0", + }, + { + emoji: "🇸🇸", + aliases: ["south_sudan"], + tags: [], + category: "Flags", + description: "flag: South Sudan", + unicode_version: "6.0", + }, + { + emoji: "🇸🇹", + aliases: ["sao_tome_principe"], + tags: [], + category: "Flags", + description: "flag: São Tomé & Príncipe", + unicode_version: "6.0", + }, + { + emoji: "🇸🇻", + aliases: ["el_salvador"], + tags: [], + category: "Flags", + description: "flag: El Salvador", + unicode_version: "6.0", + }, + { + emoji: "🇸🇽", + aliases: ["sint_maarten"], + tags: [], + category: "Flags", + description: "flag: Sint Maarten", + unicode_version: "6.0", + }, + { + emoji: "🇸🇾", + aliases: ["syria"], + tags: [], + category: "Flags", + description: "flag: Syria", + unicode_version: "6.0", + }, + { + emoji: "🇸🇿", + aliases: ["swaziland"], + tags: [], + category: "Flags", + description: "flag: Eswatini", + unicode_version: "6.0", + }, + { + emoji: "🇹🇦", + aliases: ["tristan_da_cunha"], + tags: [], + category: "Flags", + description: "flag: Tristan da Cunha", + unicode_version: "11.0", + }, + { + emoji: "🇹🇨", + aliases: ["turks_caicos_islands"], + tags: [], + category: "Flags", + description: "flag: Turks & Caicos Islands", + unicode_version: "6.0", + }, + { + emoji: "🇹🇩", + aliases: ["chad"], + tags: [], + category: "Flags", + description: "flag: Chad", + unicode_version: "6.0", + }, + { + emoji: "🇹🇫", + aliases: ["french_southern_territories"], + tags: [], + category: "Flags", + description: "flag: French Southern Territories", + unicode_version: "6.0", + }, + { + emoji: "🇹🇬", + aliases: ["togo"], + tags: [], + category: "Flags", + description: "flag: Togo", + unicode_version: "6.0", + }, + { + emoji: "🇹🇭", + aliases: ["thailand"], + tags: [], + category: "Flags", + description: "flag: Thailand", + unicode_version: "6.0", + }, + { + emoji: "🇹🇯", + aliases: ["tajikistan"], + tags: [], + category: "Flags", + description: "flag: Tajikistan", + unicode_version: "6.0", + }, + { + emoji: "🇹🇰", + aliases: ["tokelau"], + tags: [], + category: "Flags", + description: "flag: Tokelau", + unicode_version: "6.0", + }, + { + emoji: "🇹🇱", + aliases: ["timor_leste"], + tags: [], + category: "Flags", + description: "flag: Timor-Leste", + unicode_version: "6.0", + }, + { + emoji: "🇹🇲", + aliases: ["turkmenistan"], + tags: [], + category: "Flags", + description: "flag: Turkmenistan", + unicode_version: "6.0", + }, + { + emoji: "🇹🇳", + aliases: ["tunisia"], + tags: [], + category: "Flags", + description: "flag: Tunisia", + unicode_version: "6.0", + }, + { + emoji: "🇹🇴", + aliases: ["tonga"], + tags: [], + category: "Flags", + description: "flag: Tonga", + unicode_version: "6.0", + }, + { + emoji: "🇹🇷", + aliases: ["tr"], + tags: ["turkey"], + category: "Flags", + description: "flag: Turkey", + unicode_version: "8.0", + }, + { + emoji: "🇹🇹", + aliases: ["trinidad_tobago"], + tags: [], + category: "Flags", + description: "flag: Trinidad & Tobago", + unicode_version: "6.0", + }, + { + emoji: "🇹🇻", + aliases: ["tuvalu"], + tags: [], + category: "Flags", + description: "flag: Tuvalu", + unicode_version: "6.0", + }, + { + emoji: "🇹🇼", + aliases: ["taiwan"], + tags: [], + category: "Flags", + description: "flag: Taiwan", + unicode_version: "6.0", + }, + { + emoji: "🇹🇿", + aliases: ["tanzania"], + tags: [], + category: "Flags", + description: "flag: Tanzania", + unicode_version: "6.0", + }, + { + emoji: "🇺🇦", + aliases: ["ukraine"], + tags: [], + category: "Flags", + description: "flag: Ukraine", + unicode_version: "6.0", + }, + { + emoji: "🇺🇬", + aliases: ["uganda"], + tags: [], + category: "Flags", + description: "flag: Uganda", + unicode_version: "6.0", + }, + { + emoji: "🇺🇲", + aliases: ["us_outlying_islands"], + tags: [], + category: "Flags", + description: "flag: U.S. Outlying Islands", + unicode_version: "11.0", + }, + { + emoji: "🇺🇳", + aliases: ["united_nations"], + tags: [], + category: "Flags", + description: "flag: United Nations", + unicode_version: "11.0", + }, + { + emoji: "🇺🇸", + aliases: ["us"], + tags: ["flag", "united", "america"], + category: "Flags", + description: "flag: United States", + unicode_version: "6.0", + }, + { + emoji: "🇺🇾", + aliases: ["uruguay"], + tags: [], + category: "Flags", + description: "flag: Uruguay", + unicode_version: "6.0", + }, + { + emoji: "🇺🇿", + aliases: ["uzbekistan"], + tags: [], + category: "Flags", + description: "flag: Uzbekistan", + unicode_version: "6.0", + }, + { + emoji: "🇻🇦", + aliases: ["vatican_city"], + tags: [], + category: "Flags", + description: "flag: Vatican City", + unicode_version: "6.0", + }, + { + emoji: "🇻🇨", + aliases: ["st_vincent_grenadines"], + tags: [], + category: "Flags", + description: "flag: St. Vincent & Grenadines", + unicode_version: "6.0", + }, + { + emoji: "🇻🇪", + aliases: ["venezuela"], + tags: [], + category: "Flags", + description: "flag: Venezuela", + unicode_version: "6.0", + }, + { + emoji: "🇻🇬", + aliases: ["british_virgin_islands"], + tags: [], + category: "Flags", + description: "flag: British Virgin Islands", + unicode_version: "6.0", + }, + { + emoji: "🇻🇮", + aliases: ["us_virgin_islands"], + tags: [], + category: "Flags", + description: "flag: U.S. Virgin Islands", + unicode_version: "6.0", + }, + { + emoji: "🇻🇳", + aliases: ["vietnam"], + tags: [], + category: "Flags", + description: "flag: Vietnam", + unicode_version: "6.0", + }, + { + emoji: "🇻🇺", + aliases: ["vanuatu"], + tags: [], + category: "Flags", + description: "flag: Vanuatu", + unicode_version: "6.0", + }, + { + emoji: "🇼🇫", + aliases: ["wallis_futuna"], + tags: [], + category: "Flags", + description: "flag: Wallis & Futuna", + unicode_version: "6.0", + }, + { + emoji: "🇼🇸", + aliases: ["samoa"], + tags: [], + category: "Flags", + description: "flag: Samoa", + unicode_version: "6.0", + }, + { + emoji: "🇽🇰", + aliases: ["kosovo"], + tags: [], + category: "Flags", + description: "flag: Kosovo", + unicode_version: "6.0", + }, + { + emoji: "🇾🇪", + aliases: ["yemen"], + tags: [], + category: "Flags", + description: "flag: Yemen", + unicode_version: "6.0", + }, + { + emoji: "🇾🇹", + aliases: ["mayotte"], + tags: [], + category: "Flags", + description: "flag: Mayotte", + unicode_version: "6.0", + }, + { + emoji: "🇿🇦", + aliases: ["south_africa"], + tags: [], + category: "Flags", + description: "flag: South Africa", + unicode_version: "6.0", + }, + { + emoji: "🇿🇲", + aliases: ["zambia"], + tags: [], + category: "Flags", + description: "flag: Zambia", + unicode_version: "6.0", + }, + { + emoji: "🇿🇼", + aliases: ["zimbabwe"], + tags: [], + category: "Flags", + description: "flag: Zimbabwe", + unicode_version: "6.0", + }, + { + emoji: "🏴󠁧󠁢󠁥󠁮󠁧󠁿", + aliases: ["england"], + tags: [], + category: "Flags", + description: "flag: England", + unicode_version: "11.0", + }, + { + emoji: "🏴󠁧󠁢󠁳󠁣󠁴󠁿", + aliases: ["scotland"], + tags: [], + category: "Flags", + description: "flag: Scotland", + unicode_version: "11.0", + }, + { + emoji: "🏴󠁧󠁢󠁷󠁬󠁳󠁿", + aliases: ["wales"], + tags: [], + category: "Flags", + description: "flag: Wales", + unicode_version: "11.0", + }, +]; diff --git a/web/src/app/emojisMapped.js b/web/src/app/emojisMapped.js new file mode 100644 index 00000000..d823bbe0 --- /dev/null +++ b/web/src/app/emojisMapped.js @@ -0,0 +1,4 @@ +import { rawEmojis } from "./emojis"; + +// Format emojis (see emoji.js) +export default Object.fromEntries(rawEmojis.flatMap((emoji) => emoji.aliases.map((alias) => [alias, emoji.emoji]))); diff --git a/web/src/app/errors.js b/web/src/app/errors.js index 38165a24..28f49af1 100644 --- a/web/src/app/errors.js +++ b/web/src/app/errors.js @@ -1,66 +1,80 @@ +/* eslint-disable max-classes-per-file */ // This is a subset of, and the counterpart to errors.go -export const fetchOrThrow = async (url, options) => { - const response = await fetch(url, options); - if (response.status !== 200) { - await throwAppError(response); - } - return response; // Promise! -}; - -export const throwAppError = async (response) => { - if (response.status === 401 || response.status === 403) { - console.log(`[Error] HTTP ${response.status}`, response); - throw new UnauthorizedError(); - } - const error = await maybeToJson(response); - if (error?.code) { - console.log(`[Error] HTTP ${response.status}, ntfy error ${error.code}: ${error.error || ""}`, response); - if (error.code === UserExistsError.CODE) { - throw new UserExistsError(); - } else if (error.code === TopicReservedError.CODE) { - throw new TopicReservedError(); - } else if (error.code === AccountCreateLimitReachedError.CODE) { - throw new AccountCreateLimitReachedError(); - } else if (error.code === IncorrectPasswordError.CODE) { - throw new IncorrectPasswordError(); - } else if (error?.error) { - throw new Error(`Error ${error.code}: ${error.error}`); - } - } - console.log(`[Error] HTTP ${response.status}, not a ntfy error`, response); - throw new Error(`Unexpected response ${response.status}`); -}; - const maybeToJson = async (response) => { - try { - return await response.json(); - } catch (e) { - return null; - } -} + try { + return await response.json(); + } catch (e) { + return null; + } +}; export class UnauthorizedError extends Error { - constructor() { super("Unauthorized"); } + constructor() { + super("Unauthorized"); + } } export class UserExistsError extends Error { - static CODE = 40901; // errHTTPConflictUserExists - constructor() { super("Username already exists"); } + static CODE = 40901; // errHTTPConflictUserExists + + constructor() { + super("Username already exists"); + } } export class TopicReservedError extends Error { - static CODE = 40902; // errHTTPConflictTopicReserved - constructor() { super("Topic already reserved"); } + static CODE = 40902; // errHTTPConflictTopicReserved + + constructor() { + super("Topic already reserved"); + } } export class AccountCreateLimitReachedError extends Error { - static CODE = 42906; // errHTTPTooManyRequestsLimitAccountCreation - constructor() { super("Account creation limit reached"); } + static CODE = 42906; // errHTTPTooManyRequestsLimitAccountCreation + + constructor() { + super("Account creation limit reached"); + } } export class IncorrectPasswordError extends Error { - static CODE = 40026; // errHTTPBadRequestIncorrectPasswordConfirmation - constructor() { super("Password incorrect"); } + static CODE = 40026; // errHTTPBadRequestIncorrectPasswordConfirmation + + constructor() { + super("Password incorrect"); + } } +export const throwAppError = async (response) => { + if (response.status === 401 || response.status === 403) { + console.log(`[Error] HTTP ${response.status}`, response); + throw new UnauthorizedError(); + } + const error = await maybeToJson(response); + if (error?.code) { + console.log(`[Error] HTTP ${response.status}, ntfy error ${error.code}: ${error.error || ""}`, response); + if (error.code === UserExistsError.CODE) { + throw new UserExistsError(); + } else if (error.code === TopicReservedError.CODE) { + throw new TopicReservedError(); + } else if (error.code === AccountCreateLimitReachedError.CODE) { + throw new AccountCreateLimitReachedError(); + } else if (error.code === IncorrectPasswordError.CODE) { + throw new IncorrectPasswordError(); + } else if (error?.error) { + throw new Error(`Error ${error.code}: ${error.error}`); + } + } + console.log(`[Error] HTTP ${response.status}, not a ntfy error`, response); + throw new Error(`Unexpected response ${response.status}`); +}; + +export const fetchOrThrow = async (url, options) => { + const response = await fetch(url, options); + if (response.status !== 200) { + await throwAppError(response); + } + return response; // Promise! +}; diff --git a/web/src/components/i18n.js b/web/src/app/i18n.js similarity index 50% rename from web/src/components/i18n.js rename to web/src/app/i18n.js index 42eb5721..298f595c 100644 --- a/web/src/components/i18n.js +++ b/web/src/app/i18n.js @@ -1,7 +1,7 @@ -import i18n from 'i18next'; -import Backend from 'i18next-http-backend'; -import LanguageDetector from 'i18next-browser-languagedetector'; -import { initReactI18next } from 'react-i18next'; +import i18next from "i18next"; +import Backend from "i18next-http-backend"; +import LanguageDetector from "i18next-browser-languagedetector"; +import { initReactI18next } from "react-i18next"; // Translations using i18next // - Options: https://www.i18next.com/overview/configuration-options @@ -11,19 +11,20 @@ import { initReactI18next } from 'react-i18next'; // See example project here: // https://github.com/i18next/react-i18next/tree/master/example/react -i18n +const initI18n = () => + i18next .use(Backend) .use(LanguageDetector) .use(initReactI18next) .init({ - fallbackLng: 'en', - debug: true, - interpolation: { - escapeValue: false, // not needed for react as it escapes by default - }, - backend: { - loadPath: '/static/langs/{{lng}}.json', - } + fallbackLng: "en", + debug: true, + interpolation: { + escapeValue: false, // not needed for react as it escapes by default + }, + backend: { + loadPath: "/static/langs/{{lng}}.json", + }, }); -export default i18n; +export default initI18n; diff --git a/web/src/app/notificationUtils.js b/web/src/app/notificationUtils.js new file mode 100644 index 00000000..0bd5136d --- /dev/null +++ b/web/src/app/notificationUtils.js @@ -0,0 +1,81 @@ +// This is a separate file since the other utils import `config.js`, which depends on `window` +// and cannot be used in the service worker + +import emojisMapped from "./emojisMapped"; + +const toEmojis = (tags) => { + if (!tags) return []; + return tags.filter((tag) => tag in emojisMapped).map((tag) => emojisMapped[tag]); +}; + +export const formatTitle = (m) => { + const emojiList = toEmojis(m.tags); + if (emojiList.length > 0) { + return `${emojiList.join(" ")} ${m.title}`; + } + return m.title; +}; + +const formatTitleWithDefault = (m, fallback) => { + if (m.title) { + return formatTitle(m); + } + return fallback; +}; + +export const formatMessage = (m) => { + if (m.title) { + return m.message; + } + const emojiList = toEmojis(m.tags); + if (emojiList.length > 0) { + return `${emojiList.join(" ")} ${m.message}`; + } + return m.message; +}; + +const imageRegex = /\.(png|jpe?g|gif|webp)$/i; +export const isImage = (attachment) => { + if (!attachment) return false; + + // if there's a type, only take that into account + if (attachment.type) { + return attachment.type.startsWith("image/"); + } + + // otherwise, check the extension + return attachment.name?.match(imageRegex) || attachment.url?.match(imageRegex); +}; + +export const icon = "/static/images/ntfy.png"; +export const badge = "/static/images/mask-icon.svg"; + +export const toNotificationParams = ({ subscriptionId, message, defaultTitle, topicRoute }) => { + const image = isImage(message.attachment) ? message.attachment.url : undefined; + + // https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API + return [ + formatTitleWithDefault(message, defaultTitle), + { + body: formatMessage(message), + badge, + icon, + image, + timestamp: message.time * 1_000, + tag: subscriptionId, + renotify: true, + silent: false, + // This is used by the notification onclick event + data: { + message, + topicRoute, + }, + actions: message.actions + ?.filter(({ action }) => action === "view" || action === "http") + .map(({ label }) => ({ + action: label, + title: label, + })), + }, + ]; +}; diff --git a/web/src/app/utils.js b/web/src/app/utils.js index 6eb4ac54..08710c1f 100644 --- a/web/src/app/utils.js +++ b/web/src/app/utils.js @@ -1,4 +1,4 @@ -import {rawEmojis} from "./emojis"; +import { Base64 } from "js-base64"; import beep from "../sounds/beep.mp3"; import juntos from "../sounds/juntos.mp3"; import pristine from "../sounds/pristine.mp3"; @@ -7,17 +7,21 @@ import dadum from "../sounds/dadum.mp3"; import pop from "../sounds/pop.mp3"; import popSwoosh from "../sounds/pop-swoosh.mp3"; import config from "./config"; -import {Base64} from 'js-base64'; +import emojisMapped from "./emojisMapped"; +export const tiersUrl = (baseUrl) => `${baseUrl}/v1/tiers`; +export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, ""); +export const expandUrl = (url) => [`https://${url}`, `http://${url}`]; +export const expandSecureUrl = (url) => `https://${url}`; export const topicUrl = (baseUrl, topic) => `${baseUrl}/${topic}`; -export const topicUrlWs = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/ws` - .replaceAll("https://", "wss://") - .replaceAll("http://", "ws://"); +export const topicUrlWs = (baseUrl, topic) => + `${topicUrl(baseUrl, topic)}/ws`.replaceAll("https://", "wss://").replaceAll("http://", "ws://"); export const topicUrlJson = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/json`; export const topicUrlJsonPoll = (baseUrl, topic) => `${topicUrlJson(baseUrl, topic)}?poll=1`; export const topicUrlJsonPollWithSince = (baseUrl, topic, since) => `${topicUrlJson(baseUrl, topic)}?poll=1&since=${since}`; export const topicUrlAuth = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/auth`; export const topicShortUrl = (baseUrl, topic) => shortUrl(topicUrl(baseUrl, topic)); +export const webPushUrl = (baseUrl) => `${baseUrl}/v1/webpush`; export const accountUrl = (baseUrl) => `${baseUrl}/v1/account`; export const accountPasswordUrl = (baseUrl) => `${baseUrl}/v1/account/password`; export const accountTokenUrl = (baseUrl) => `${baseUrl}/v1/account/token`; @@ -27,276 +31,242 @@ export const accountReservationUrl = (baseUrl) => `${baseUrl}/v1/account/reserva export const accountReservationSingleUrl = (baseUrl, topic) => `${baseUrl}/v1/account/reservation/${topic}`; export const accountBillingSubscriptionUrl = (baseUrl) => `${baseUrl}/v1/account/billing/subscription`; export const accountBillingPortalUrl = (baseUrl) => `${baseUrl}/v1/account/billing/portal`; -export const tiersUrl = (baseUrl) => `${baseUrl}/v1/tiers`; -export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, ""); -export const expandUrl = (url) => [`https://${url}`, `http://${url}`]; -export const expandSecureUrl = (url) => `https://${url}`; +export const accountPhoneUrl = (baseUrl) => `${baseUrl}/v1/account/phone`; +export const accountPhoneVerifyUrl = (baseUrl) => `${baseUrl}/v1/account/phone/verify`; -export const validUrl = (url) => { - return url.match(/^https?:\/\/.+/); -} +export const validUrl = (url) => url.match(/^https?:\/\/.+/); + +export const disallowedTopic = (topic) => config.disallowed_topics.includes(topic); export const validTopic = (topic) => { - if (disallowedTopic(topic)) { - return false; - } - return topic.match(/^([-_a-zA-Z0-9]{1,64})$/); // Regex must match Go & Android app! -} - -export const disallowedTopic = (topic) => { - return config.disallowed_topics.includes(topic); -} + if (disallowedTopic(topic)) { + return false; + } + return topic.match(/^([-_a-zA-Z0-9]{1,64})$/); // Regex must match Go & Android app! +}; export const topicDisplayName = (subscription) => { - if (subscription.displayName) { - return subscription.displayName; - } else if (subscription.baseUrl === config.base_url) { - return subscription.topic; - } - return topicShortUrl(subscription.baseUrl, subscription.topic); -}; - -// Format emojis (see emoji.js) -const emojis = {}; -rawEmojis.forEach(emoji => { - emoji.aliases.forEach(alias => { - emojis[alias] = emoji.emoji; - }); -}); - -const toEmojis = (tags) => { - if (!tags) return []; - else return tags.filter(tag => tag in emojis).map(tag => emojis[tag]); -} - -export const formatTitleWithDefault = (m, fallback) => { - if (m.title) { - return formatTitle(m); - } - return fallback; -}; - -export const formatTitle = (m) => { - const emojiList = toEmojis(m.tags); - if (emojiList.length > 0) { - return `${emojiList.join(" ")} ${m.title}`; - } else { - return m.title; - } -}; - -export const formatMessage = (m) => { - if (m.title) { - return m.message; - } else { - const emojiList = toEmojis(m.tags); - if (emojiList.length > 0) { - return `${emojiList.join(" ")} ${m.message}`; - } else { - return m.message; - } - } + if (subscription.displayName) { + return subscription.displayName; + } + if (subscription.baseUrl === config.base_url) { + return subscription.topic; + } + return topicShortUrl(subscription.baseUrl, subscription.topic); }; export const unmatchedTags = (tags) => { - if (!tags) return []; - else return tags.filter(tag => !(tag in emojis)); -} + if (!tags) return []; + return tags.filter((tag) => !(tag in emojisMapped)); +}; -export const maybeWithAuth = (headers, user) => { - if (user && user.password) { - return withBasicAuth(headers, user.username, user.password); - } else if (user && user.token) { - return withBearerAuth(headers, user.token); - } - return headers; -} +export const encodeBase64 = (s) => Base64.encode(s); + +export const encodeBase64Url = (s) => Base64.encodeURI(s); + +export const bearerAuth = (token) => `Bearer ${token}`; + +export const basicAuth = (username, password) => `Basic ${encodeBase64(`${username}:${password}`)}`; + +export const withBearerAuth = (headers, token) => ({ ...headers, Authorization: bearerAuth(token) }); export const maybeWithBearerAuth = (headers, token) => { - if (token) { - return withBearerAuth(headers, token); - } - return headers; -} + if (token) { + return withBearerAuth(headers, token); + } + return headers; +}; -export const withBasicAuth = (headers, username, password) => { - headers['Authorization'] = basicAuth(username, password); - return headers; -} +export const withBasicAuth = (headers, username, password) => ({ ...headers, Authorization: basicAuth(username, password) }); -export const basicAuth = (username, password) => { - return `Basic ${encodeBase64(`${username}:${password}`)}`; -} +export const maybeWithAuth = (headers, user) => { + if (user?.password) { + return withBasicAuth(headers, user.username, user.password); + } + if (user?.token) { + return withBearerAuth(headers, user.token); + } + return headers; +}; -export const withBearerAuth = (headers, token) => { - headers['Authorization'] = bearerAuth(token); - return headers; -} - -export const bearerAuth = (token) => { - return `Bearer ${token}`; -} - -export const encodeBase64 = (s) => { - return Base64.encode(s); -} - -export const encodeBase64Url = (s) => { - return Base64.encodeURI(s); -} - -export const maybeAppendActionErrors = (message, notification) => { - const actionErrors = (notification.actions ?? []) - .map(action => action.error) - .filter(action => !!action) - .join("\n") - if (actionErrors.length === 0) { - return message; - } else { - return `${message}\n\n${actionErrors}`; - } -} +export const maybeActionErrors = (notification) => { + const actionErrors = (notification.actions ?? []) + .map((action) => action.error) + .filter((action) => !!action) + .join("\n"); + if (actionErrors.length === 0) { + return undefined; + } + return actionErrors; +}; export const shuffle = (arr) => { - let j, x; - for (let index = arr.length - 1; index > 0; index--) { - j = Math.floor(Math.random() * (index + 1)); - x = arr[index]; - arr[index] = arr[j]; - arr[j] = x; - } - return arr; -} + const returnArr = [...arr]; -export const splitNoEmpty = (s, delimiter) => { - return s - .split(delimiter) - .map(x => x.trim()) - .filter(x => x !== ""); -} + for (let index = returnArr.length - 1; index > 0; index -= 1) { + const j = Math.floor(Math.random() * (index + 1)); + [returnArr[index], returnArr[j]] = [returnArr[j], returnArr[index]]; + } + + return returnArr; +}; + +export const splitNoEmpty = (s, delimiter) => + s + .split(delimiter) + .map((x) => x.trim()) + .filter((x) => x !== ""); /** Non-cryptographic hash function, see https://stackoverflow.com/a/8831937/1440785 */ -export const hashCode = async (s) => { - let hash = 0; - for (let i = 0; i < s.length; i++) { - const char = s.charCodeAt(i); - hash = ((hash<<5)-hash)+char; - hash = hash & hash; // Convert to 32bit integer - } - return hash; -} +export const hashCode = (s) => { + let hash = 0; + for (let i = 0; i < s.length; i += 1) { + const char = s.charCodeAt(i); + // eslint-disable-next-line no-bitwise + hash = (hash << 5) - hash + char; + // eslint-disable-next-line no-bitwise + hash &= hash; // Convert to 32bit integer + } + return hash; +}; -export const formatShortDateTime = (timestamp) => { - return new Intl.DateTimeFormat('default', {dateStyle: 'short', timeStyle: 'short'}) - .format(new Date(timestamp * 1000)); -} +/** + * convert `i18n.language` style str (e.g.: `en_US`) to kebab-case (e.g.: `en-US`), + * which is expected by `` and `Intl.DateTimeFormat` + */ +export const getKebabCaseLangStr = (language) => language.replace(/_/g, "-"); -export const formatShortDate = (timestamp) => { - return new Intl.DateTimeFormat('default', {dateStyle: 'short'}) - .format(new Date(timestamp * 1000)); -} +export const formatShortDateTime = (timestamp, language) => + new Intl.DateTimeFormat(getKebabCaseLangStr(language), { + dateStyle: "short", + timeStyle: "short", + }).format(new Date(timestamp * 1000)); + +export const formatShortDate = (timestamp, language) => + new Intl.DateTimeFormat(getKebabCaseLangStr(language), { dateStyle: "short" }).format(new Date(timestamp * 1000)); export const formatBytes = (bytes, decimals = 2) => { - if (bytes === 0) return '0 bytes'; - const k = 1024; - const dm = decimals < 0 ? 0 : decimals; - const sizes = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; -} + if (bytes === 0) return "0 bytes"; + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`; +}; export const formatNumber = (n) => { - if (n % 1000 === 0) { - return `${n/1000}k`; - } + if (n === 0) { return n; -} + } + if (n % 1000 === 0) { + return `${n / 1000}k`; + } + return n.toLocaleString(); +}; export const formatPrice = (n) => { - if (n % 100 === 0) { - return `$${n/100}`; - } - return `$${(n/100).toPrecision(2)}`; -} + if (n % 100 === 0) { + return `$${n / 100}`; + } + return `$${(n / 100).toPrecision(2)}`; +}; export const openUrl = (url) => { - window.open(url, "_blank", "noopener,noreferrer"); + window.open(url, "_blank", "noopener,noreferrer"); }; export const sounds = { - "ding": { - file: ding, - label: "Ding" - }, - "juntos": { - file: juntos, - label: "Juntos" - }, - "pristine": { - file: pristine, - label: "Pristine" - }, - "dadum": { - file: dadum, - label: "Dadum" - }, - "pop": { - file: pop, - label: "Pop" - }, - "pop-swoosh": { - file: popSwoosh, - label: "Pop swoosh" - }, - "beep": { - file: beep, - label: "Beep" - } + ding: { + file: ding, + label: "Ding", + }, + juntos: { + file: juntos, + label: "Juntos", + }, + pristine: { + file: pristine, + label: "Pristine", + }, + dadum: { + file: dadum, + label: "Dadum", + }, + pop: { + file: pop, + label: "Pop", + }, + "pop-swoosh": { + file: popSwoosh, + label: "Pop swoosh", + }, + beep: { + file: beep, + label: "Beep", + }, }; export const playSound = async (id) => { - const audio = new Audio(sounds[id].file); - return audio.play(); + const audio = new Audio(sounds[id].file); + return audio.play(); }; // From: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch +// eslint-disable-next-line func-style export async function* fetchLinesIterator(fileURL, headers) { - const utf8Decoder = new TextDecoder('utf-8'); - const response = await fetch(fileURL, { - headers: headers - }); - const reader = response.body.getReader(); - let { value: chunk, done: readerDone } = await reader.read(); - chunk = chunk ? utf8Decoder.decode(chunk) : ''; + const utf8Decoder = new TextDecoder("utf-8"); + const response = await fetch(fileURL, { + headers, + }); + const reader = response.body.getReader(); + let { value: chunk, done: readerDone } = await reader.read(); + chunk = chunk ? utf8Decoder.decode(chunk) : ""; - const re = /\n|\r|\r\n/gm; - let startIndex = 0; + const re = /\n|\r|\r\n/gm; + let startIndex = 0; - for (;;) { - let result = re.exec(chunk); - if (!result) { - if (readerDone) { - break; - } - let remainder = chunk.substr(startIndex); - ({ value: chunk, done: readerDone } = await reader.read()); - chunk = remainder + (chunk ? utf8Decoder.decode(chunk) : ''); - startIndex = re.lastIndex = 0; - continue; - } - yield chunk.substring(startIndex, result.index); - startIndex = re.lastIndex; - } - if (startIndex < chunk.length) { - yield chunk.substr(startIndex); // last line didn't end in a newline char + for (;;) { + const result = re.exec(chunk); + if (!result) { + if (readerDone) { + break; + } + const remainder = chunk.substr(startIndex); + // eslint-disable-next-line no-await-in-loop + ({ value: chunk, done: readerDone } = await reader.read()); + chunk = remainder + (chunk ? utf8Decoder.decode(chunk) : ""); + startIndex = 0; + re.lastIndex = 0; + // eslint-disable-next-line no-continue + continue; } + yield chunk.substring(startIndex, result.index); + startIndex = re.lastIndex; + } + if (startIndex < chunk.length) { + yield chunk.substr(startIndex); // last line didn't end in a newline char + } } export const randomAlphanumericString = (len) => { - const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - let id = ""; - for (let i = 0; i < len; i++) { - id += alphabet[(Math.random() * alphabet.length) | 0]; - } - return id; -} + const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + let id = ""; + for (let i = 0; i < len; i += 1) { + // eslint-disable-next-line no-bitwise + id += alphabet[(Math.random() * alphabet.length) | 0]; + } + return id; +}; + +export const urlB64ToUint8Array = (base64String) => { + const padding = "=".repeat((4 - (base64String.length % 4)) % 4); + const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/"); + + const rawData = window.atob(base64); + const outputArray = new Uint8Array(rawData.length); + + for (let i = 0; i < rawData.length; i += 1) { + outputArray[i] = rawData.charCodeAt(i); + } + return outputArray; +}; diff --git a/web/src/components/Account.js b/web/src/components/Account.js deleted file mode 100644 index e5b60077..00000000 --- a/web/src/components/Account.js +++ /dev/null @@ -1,803 +0,0 @@ -import * as React from 'react'; -import {useContext, useState} from 'react'; -import { - Alert, - CardActions, - CardContent, - FormControl, - LinearProgress, - Link, - Portal, - Select, - Snackbar, - Stack, - Table, - TableBody, - TableCell, - TableHead, - TableRow, - useMediaQuery -} from "@mui/material"; -import Tooltip from '@mui/material/Tooltip'; -import Typography from "@mui/material/Typography"; -import EditIcon from '@mui/icons-material/Edit'; -import Container from "@mui/material/Container"; -import Card from "@mui/material/Card"; -import Button from "@mui/material/Button"; -import {Trans, useTranslation} from "react-i18next"; -import session from "../app/Session"; -import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; -import theme from "./theme"; -import Dialog from "@mui/material/Dialog"; -import DialogTitle from "@mui/material/DialogTitle"; -import DialogContent from "@mui/material/DialogContent"; -import TextField from "@mui/material/TextField"; -import routes from "./routes"; -import IconButton from "@mui/material/IconButton"; -import {formatBytes, formatShortDate, formatShortDateTime, openUrl} from "../app/utils"; -import accountApi, {LimitBasis, Role, SubscriptionInterval, SubscriptionStatus} from "../app/AccountApi"; -import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; -import {Pref, PrefGroup} from "./Pref"; -import db from "../app/db"; -import i18n from "i18next"; -import humanizeDuration from "humanize-duration"; -import UpgradeDialog from "./UpgradeDialog"; -import CelebrationIcon from "@mui/icons-material/Celebration"; -import {AccountContext} from "./App"; -import DialogFooter from "./DialogFooter"; -import {Paragraph} from "./styles"; -import CloseIcon from "@mui/icons-material/Close"; -import {ContentCopy, Public} from "@mui/icons-material"; -import MenuItem from "@mui/material/MenuItem"; -import DialogContentText from "@mui/material/DialogContentText"; -import {IncorrectPasswordError, UnauthorizedError} from "../app/errors"; - -const Account = () => { - if (!session.exists()) { - window.location.href = routes.app; - return <>; - } - return ( - - - - - - - - - ); -}; - -const Basics = () => { - const { t } = useTranslation(); - return ( - - - {t("account_basics_title")} - - - - - - - - ); -}; - -const Username = () => { - const { t } = useTranslation(); - const { account } = useContext(AccountContext); - const labelId = "prefUsername"; - - return ( - -
- {session.username()} - {account?.role === Role.ADMIN - ? <>{" "}👑 - : ""} -
-
- ) -}; - -const ChangePassword = () => { - const { t } = useTranslation(); - const [dialogKey, setDialogKey] = useState(0); - const [dialogOpen, setDialogOpen] = useState(false); - const labelId = "prefChangePassword"; - - const handleDialogOpen = () => { - setDialogKey(prev => prev+1); - setDialogOpen(true); - }; - - const handleDialogClose = () => { - setDialogOpen(false); - }; - - return ( - -
- ⬤⬤⬤⬤⬤⬤⬤⬤⬤⬤ - - - -
- -
- ) -}; - -const ChangePasswordDialog = (props) => { - const { t } = useTranslation(); - const [error, setError] = useState(""); - const [currentPassword, setCurrentPassword] = useState(""); - const [newPassword, setNewPassword] = useState(""); - const [confirmPassword, setConfirmPassword] = useState(""); - const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); - - const handleDialogSubmit = async () => { - try { - console.debug(`[Account] Changing password`); - await accountApi.changePassword(currentPassword, newPassword); - props.onClose(); - } catch (e) { - console.log(`[Account] Error changing password`, e); - if (e instanceof IncorrectPasswordError) { - setError(t("account_basics_password_dialog_current_password_incorrect")); - } else if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } else { - setError(e.message); - } - } - }; - - return ( - - {t("account_basics_password_dialog_title")} - - setCurrentPassword(ev.target.value)} - fullWidth - variant="standard" - /> - setNewPassword(ev.target.value)} - fullWidth - variant="standard" - /> - setConfirmPassword(ev.target.value)} - fullWidth - variant="standard" - /> - - - - - - - ); -}; - -const AccountType = () => { - const { t } = useTranslation(); - const { account } = useContext(AccountContext); - const [upgradeDialogKey, setUpgradeDialogKey] = useState(0); - const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false); - const [showPortalError, setShowPortalError] = useState(false); - - if (!account) { - return <>; - } - - const handleUpgradeClick = () => { - setUpgradeDialogKey(k => k + 1); - setUpgradeDialogOpen(true); - } - - const handleManageBilling = async () => { - try { - const response = await accountApi.createBillingPortalSession(); - window.open(response.redirect_url, "billing_portal"); - } catch (e) { - console.log(`[Account] Error opening billing portal`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } else { - setShowPortalError(true); - } - } - }; - - let accountType; - if (account.role === Role.ADMIN) { - const tierSuffix = (account.tier) ? t("account_basics_tier_admin_suffix_with_tier", { tier: account.tier.name }) : t("account_basics_tier_admin_suffix_no_tier"); - accountType = `${t("account_basics_tier_admin")} ${tierSuffix}`; - } else if (!account.tier) { - accountType = (config.enable_payments) ? t("account_basics_tier_free") : t("account_basics_tier_basic"); - } else { - accountType = account.tier.name; - if (account.billing?.interval === SubscriptionInterval.MONTH) { - accountType += ` (${t("account_basics_tier_interval_monthly")})`; - } else if (account.billing?.interval === SubscriptionInterval.YEAR) { - accountType += ` (${t("account_basics_tier_interval_yearly")})`; - } - } - - return ( - 0} - title={t("account_basics_tier_title")} - description={t("account_basics_tier_description")} - > -
- {accountType} - {account.billing?.paid_until && !account.billing?.cancel_at && - - - - } - {config.enable_payments && account.role === Role.USER && !account.billing?.subscription && - - } - {config.enable_payments && account.role === Role.USER && account.billing?.subscription && - - } - {config.enable_payments && account.role === Role.USER && account.billing?.customer && - - } - {config.enable_payments && - setUpgradeDialogOpen(false)} - /> - } -
- {account.billing?.status === SubscriptionStatus.PAST_DUE && - {t("account_basics_tier_payment_overdue")} - } - {account.billing?.cancel_at > 0 && - {t("account_basics_tier_canceled_subscription", { date: formatShortDate(account.billing.cancel_at) })} - } - - setShowPortalError(false)} - message={t("account_usage_cannot_create_portal_session")} - /> - -
- ) -}; - -const Stats = () => { - const { t } = useTranslation(); - const { account } = useContext(AccountContext); - - if (!account) { - return <>; - } - - const normalize = (value, max) => { - return Math.min(value / max * 100, 100); - }; - - return ( - - - {t("account_usage_title")} - - - - {(account.role === Role.ADMIN || account.limits.reservations > 0) && - <> -
- {account.stats.reservations} - {account.role === Role.USER ? t("account_usage_of_limit", {limit: account.limits.reservations}) : t("account_usage_unlimited")} -
- 0 ? normalize(account.stats.reservations, account.limits.reservations) : 100} - /> - - } - {account.role === Role.USER && account.limits.reservations === 0 && - {t("account_usage_reservations_none")} - } -
- - {t("account_usage_messages_title")} - - - }> -
- {account.stats.messages} - {account.role === Role.USER ? t("account_usage_of_limit", { limit: account.limits.messages }) : t("account_usage_unlimited")} -
- -
- - {t("account_usage_emails_title")} - - - }> -
- {account.stats.emails} - {account.role === Role.USER ? t("account_usage_of_limit", { limit: account.limits.emails }) : t("account_usage_unlimited")} -
- -
- -
- {formatBytes(account.stats.attachment_total_size)} - {account.role === Role.USER ? t("account_usage_of_limit", { limit: formatBytes(account.limits.attachment_total_size) }) : t("account_usage_unlimited")} -
- -
-
- {account.role === Role.USER && account.limits.basis === LimitBasis.IP && - - {t("account_usage_basis_ip_description")} - - } -
- ); -}; - -const InfoIcon = () => { - return ( - - ); -} - - -const Tokens = () => { - const { t } = useTranslation(); - const { account } = useContext(AccountContext); - const [dialogKey, setDialogKey] = useState(0); - const [dialogOpen, setDialogOpen] = useState(false); - const tokens = account?.tokens || []; - - const handleCreateClick = () => { - setDialogKey(prev => prev+1); - setDialogOpen(true); - }; - - const handleDialogClose = () => { - setDialogOpen(false); - }; - - const handleDialogSubmit = async (user) => { - setDialogOpen(false); - // - }; - return ( - - - - {t("account_tokens_title")} - - - - }} - /> - - {tokens?.length > 0 && } - - - - - - - ); -}; - -const TokensTable = (props) => { - const { t } = useTranslation(); - const [snackOpen, setSnackOpen] = useState(false); - const [upsertDialogKey, setUpsertDialogKey] = useState(0); - const [upsertDialogOpen, setUpsertDialogOpen] = useState(false); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [selectedToken, setSelectedToken] = useState(null); - - const tokens = (props.tokens || []) - .sort( (a, b) => { - if (a.token === session.token()) { - return -1; - } else if (b.token === session.token()) { - return 1; - } - return a.token.localeCompare(b.token); - }); - - const handleEditClick = (token) => { - setUpsertDialogKey(prev => prev+1); - setSelectedToken(token); - setUpsertDialogOpen(true); - }; - - const handleDialogClose = () => { - setUpsertDialogOpen(false); - setDeleteDialogOpen(false); - setSelectedToken(null); - }; - - const handleDeleteClick = async (token) => { - setSelectedToken(token); - setDeleteDialogOpen(true); - }; - - const handleCopy = async (token) => { - await navigator.clipboard.writeText(token); - setSnackOpen(true); - }; - - return ( -
TagEmoji
" >> "$1" cat "$SCRIPTDIR/emoji.json" \ - | jq -r '.[] | ""' \ + | jq -r '.[] | ""' \ | sed -n "${from},${to}p" >> "$1" echo "
TagEmoji
" + .aliases[0] + "" + .emoji + "
" + .aliases[0] + "" + .emoji + "
- - - {t("account_tokens_table_token_header")} - {t("account_tokens_table_label_header")} - {t("account_tokens_table_expires_header")} - {t("account_tokens_table_last_access_header")} - - - - - {tokens.map(token => ( - - - - {token.token.slice(0, 12)} - ... - - handleCopy(token.token)}> - - - - - {token.token === session.token() && {t("account_tokens_table_current_session")}} - {token.token !== session.token() && (token.label || "-")} - - - {token.expires ? formatShortDateTime(token.expires) : {t("account_tokens_table_never_expires")}} - - -
- {formatShortDateTime(token.last_access)} - - openUrl(`https://whatismyipaddress.com/ip/${token.last_origin}`)}> - - - -
-
- - {token.token !== session.token() && - <> - handleEditClick(token)} aria-label={t("account_tokens_dialog_title_edit")}> - - - handleDeleteClick(token)} aria-label={t("account_tokens_dialog_title_delete")}> - - - - } - {token.token === session.token() && - - - - - - - } - -
- ))} -
- - setSnackOpen(false)} - message={t("account_tokens_table_copied_to_clipboard")} - /> - - - -
- ); -}; - -const TokenDialog = (props) => { - const { t } = useTranslation(); - const [error, setError] = useState(""); - const [label, setLabel] = useState(props.token?.label || ""); - const [expires, setExpires] = useState(props.token ? -1 : 0); - const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); - const editMode = !!props.token; - - const handleSubmit = async () => { - try { - if (editMode) { - await accountApi.updateToken(props.token.token, label, expires); - } else { - await accountApi.createToken(label, expires); - } - props.onClose(); - } catch (e) { - console.log(`[Account] Error creating token`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } else { - setError(e.message); - } - } - }; - - return ( - - {editMode ? t("account_tokens_dialog_title_edit") : t("account_tokens_dialog_title_create")} - - setLabel(ev.target.value)} - fullWidth - variant="standard" - /> - - - - - - - - - - ); -}; - -const TokenDeleteDialog = (props) => { - const { t } = useTranslation(); - const [error, setError] = useState(""); - - const handleSubmit = async () => { - try { - await accountApi.deleteToken(props.token.token); - props.onClose(); - } catch (e) { - console.log(`[Account] Error deleting token`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } else { - setError(e.message); - } - } - }; - - return ( - - {t("account_tokens_delete_dialog_title")} - - - - - - - - - - - ); -} - - -const Delete = () => { - const { t } = useTranslation(); - return ( - - - {t("account_delete_title")} - - - - - - ); -}; - -const DeleteAccount = () => { - const { t } = useTranslation(); - const [dialogKey, setDialogKey] = useState(0); - const [dialogOpen, setDialogOpen] = useState(false); - - const handleDialogOpen = () => { - setDialogKey(prev => prev+1); - setDialogOpen(true); - }; - - const handleDialogClose = () => { - setDialogOpen(false); - }; - - return ( - -
- -
- -
- ) -}; - -const DeleteAccountDialog = (props) => { - const { t } = useTranslation(); - const { account } = useContext(AccountContext); - const [error, setError] = useState(""); - const [password, setPassword] = useState(""); - const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); - - const handleSubmit = async () => { - try { - await accountApi.delete(password); - await db.delete(); - console.debug(`[Account] Account deleted`); - session.resetAndRedirect(routes.app); - } catch (e) { - console.log(`[Account] Error deleting account`, e); - if (e instanceof IncorrectPasswordError) { - setError(t("account_basics_password_dialog_current_password_incorrect")); - } else if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } else { - setError(e.message); - } - } - }; - - return ( - - {t("account_delete_title")} - - - {t("account_delete_dialog_description")} - - setPassword(ev.target.value)} - fullWidth - variant="standard" - /> - {account?.billing?.subscription && - {t("account_delete_dialog_billing_warning")} - } - - - - - - - ); -}; - -export default Account; diff --git a/web/src/components/Account.jsx b/web/src/components/Account.jsx new file mode 100644 index 00000000..319353df --- /dev/null +++ b/web/src/components/Account.jsx @@ -0,0 +1,1131 @@ +import * as React from "react"; +import { useContext, useState } from "react"; +import { + Alert, + CardActions, + CardContent, + Chip, + FormControl, + FormControlLabel, + LinearProgress, + Link, + Portal, + Radio, + RadioGroup, + Select, + Snackbar, + Stack, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + useMediaQuery, + Tooltip, + Typography, + Container, + Card, + Button, + Dialog, + DialogTitle, + DialogContent, + TextField, + IconButton, + MenuItem, + DialogContentText, + useTheme, +} from "@mui/material"; +import EditIcon from "@mui/icons-material/Edit"; +import { Trans, useTranslation } from "react-i18next"; +import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; +import humanizeDuration from "humanize-duration"; +import CelebrationIcon from "@mui/icons-material/Celebration"; +import CloseIcon from "@mui/icons-material/Close"; +import { ContentCopy, Public } from "@mui/icons-material"; +import AddIcon from "@mui/icons-material/Add"; +import routes from "./routes"; +import { formatBytes, formatShortDate, formatShortDateTime, openUrl } from "../app/utils"; +import accountApi, { LimitBasis, Role, SubscriptionInterval, SubscriptionStatus } from "../app/AccountApi"; +import { Pref, PrefGroup } from "./Pref"; +import db from "../app/db"; +import UpgradeDialog from "./UpgradeDialog"; +import { AccountContext } from "./App"; +import DialogFooter from "./DialogFooter"; +import { Paragraph } from "./styles"; +import { IncorrectPasswordError, UnauthorizedError } from "../app/errors"; +import { ProChip } from "./SubscriptionPopup"; +import session from "../app/Session"; + +const Account = () => { + if (!session.exists()) { + window.location.href = routes.app; + return <>; + } + return ( + + + + + + + + + ); +}; + +const Basics = () => { + const { t } = useTranslation(); + return ( + + + {t("account_basics_title")} + + + + + + + + + ); +}; + +const Username = () => { + const { t } = useTranslation(); + const { account } = useContext(AccountContext); + const labelId = "prefUsername"; + + return ( + +
+ {session.username()} + {account?.role === Role.ADMIN ? ( + <> + {" "} + + 👑 + + + ) : ( + "" + )} +
+
+ ); +}; + +const ChangePassword = () => { + const { t } = useTranslation(); + const [dialogKey, setDialogKey] = useState(0); + const [dialogOpen, setDialogOpen] = useState(false); + const labelId = "prefChangePassword"; + + const handleDialogOpen = () => { + setDialogKey((prev) => prev + 1); + setDialogOpen(true); + }; + + const handleDialogClose = () => { + setDialogOpen(false); + }; + + return ( + +
+ + ⬤⬤⬤⬤⬤⬤⬤⬤⬤⬤ + + + + +
+ +
+ ); +}; + +const ChangePasswordDialog = (props) => { + const theme = useTheme(); + const { t } = useTranslation(); + const [error, setError] = useState(""); + const [currentPassword, setCurrentPassword] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); + + const handleDialogSubmit = async () => { + try { + console.debug(`[Account] Changing password`); + await accountApi.changePassword(currentPassword, newPassword); + props.onClose(); + } catch (e) { + console.log(`[Account] Error changing password`, e); + if (e instanceof IncorrectPasswordError) { + setError(t("account_basics_password_dialog_current_password_incorrect")); + } else if (e instanceof UnauthorizedError) { + await session.resetAndRedirect(routes.login); + } else { + setError(e.message); + } + } + }; + + return ( + + {t("account_basics_password_dialog_title")} + + setCurrentPassword(ev.target.value)} + fullWidth + variant="standard" + /> + setNewPassword(ev.target.value)} + fullWidth + variant="standard" + /> + setConfirmPassword(ev.target.value)} + fullWidth + variant="standard" + /> + + + + + + + ); +}; + +const AccountType = () => { + const { t, i18n } = useTranslation(); + const { account } = useContext(AccountContext); + const [upgradeDialogKey, setUpgradeDialogKey] = useState(0); + const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false); + const [showPortalError, setShowPortalError] = useState(false); + + if (!account) { + return <>; + } + + const handleUpgradeClick = () => { + setUpgradeDialogKey((k) => k + 1); + setUpgradeDialogOpen(true); + }; + + const handleManageBilling = async () => { + try { + const response = await accountApi.createBillingPortalSession(); + window.open(response.redirect_url, "billing_portal"); + } catch (e) { + console.log(`[Account] Error opening billing portal`, e); + if (e instanceof UnauthorizedError) { + await session.resetAndRedirect(routes.login); + } else { + setShowPortalError(true); + } + } + }; + + let accountType; + if (account.role === Role.ADMIN) { + const tierSuffix = account.tier + ? t("account_basics_tier_admin_suffix_with_tier", { + tier: account.tier.name, + }) + : t("account_basics_tier_admin_suffix_no_tier"); + accountType = `${t("account_basics_tier_admin")} ${tierSuffix}`; + } else if (!account.tier) { + accountType = config.enable_payments ? t("account_basics_tier_free") : t("account_basics_tier_basic"); + } else { + accountType = account.tier.name; + if (account.billing?.interval === SubscriptionInterval.MONTH) { + accountType += ` (${t("account_basics_tier_interval_monthly")})`; + } else if (account.billing?.interval === SubscriptionInterval.YEAR) { + accountType += ` (${t("account_basics_tier_interval_yearly")})`; + } + } + + return ( + 0} + title={t("account_basics_tier_title")} + description={t("account_basics_tier_description")} + > +
+ {accountType} + {account.billing?.paid_until && !account.billing?.cancel_at && ( + + + + + + )} + {config.enable_payments && account.role === Role.USER && !account.billing?.subscription && ( + + )} + {config.enable_payments && account.role === Role.USER && account.billing?.subscription && ( + + )} + {config.enable_payments && account.role === Role.USER && account.billing?.customer && ( + + )} + {config.enable_payments && ( + setUpgradeDialogOpen(false)} + /> + )} +
+ {account.billing?.status === SubscriptionStatus.PAST_DUE && ( + + {t("account_basics_tier_payment_overdue")} + + )} + {account.billing?.cancel_at > 0 && ( + + {t("account_basics_tier_canceled_subscription", { + date: formatShortDate(account.billing.cancel_at, i18n.language), + })} + + )} + + setShowPortalError(false)} + message={t("account_usage_cannot_create_portal_session")} + /> + +
+ ); +}; + +const PhoneNumbers = () => { + const { t } = useTranslation(); + const { account } = useContext(AccountContext); + const [dialogKey, setDialogKey] = useState(0); + const [dialogOpen, setDialogOpen] = useState(false); + const [snackOpen, setSnackOpen] = useState(false); + const labelId = "prefPhoneNumbers"; + + const handleDialogOpen = () => { + setDialogKey((prev) => prev + 1); + setDialogOpen(true); + }; + + const handleDialogClose = () => { + setDialogOpen(false); + }; + + const handleCopy = (phoneNumber) => { + navigator.clipboard.writeText(phoneNumber); + setSnackOpen(true); + }; + + const handleDelete = async (phoneNumber) => { + try { + await accountApi.deletePhoneNumber(phoneNumber); + } catch (e) { + console.log(`[Account] Error deleting phone number`, e); + if (e instanceof UnauthorizedError) { + await session.resetAndRedirect(routes.login); + } + } + }; + + if (!config.enable_calls) { + return null; + } + + if (account?.limits.calls === 0) { + return ( + + {t("account_basics_phone_numbers_title")} + {config.enable_payments && } + + } + description={t("account_basics_phone_numbers_description")} + > + {t("account_usage_calls_none")} + + ); + } + + return ( + +
+ {account?.phone_numbers?.map((phoneNumber) => ( + + {phoneNumber} + + } + variant="outlined" + onClick={() => handleCopy(phoneNumber)} + onDelete={() => handleDelete(phoneNumber)} + /> + ))} + {!account?.phone_numbers && {t("account_basics_phone_numbers_no_phone_numbers_yet")}} + + + +
+ + + setSnackOpen(false)} + message={t("account_basics_phone_numbers_copied_to_clipboard")} + /> + +
+ ); +}; + +const AddPhoneNumberDialog = (props) => { + const theme = useTheme(); + const { t } = useTranslation(); + const [error, setError] = useState(""); + const [phoneNumber, setPhoneNumber] = useState(""); + const [channel, setChannel] = useState("sms"); + const [code, setCode] = useState(""); + const [sending, setSending] = useState(false); + const [verificationCodeSent, setVerificationCodeSent] = useState(false); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); + + const verifyPhone = async () => { + try { + setSending(true); + await accountApi.verifyPhoneNumber(phoneNumber, channel); + setVerificationCodeSent(true); + } catch (e) { + console.log(`[Account] Error sending verification`, e); + if (e instanceof UnauthorizedError) { + await session.resetAndRedirect(routes.login); + } else { + setError(e.message); + } + } finally { + setSending(false); + } + }; + + const checkVerifyPhone = async () => { + try { + setSending(true); + await accountApi.addPhoneNumber(phoneNumber, code); + props.onClose(); + } catch (e) { + console.log(`[Account] Error confirming verification`, e); + if (e instanceof UnauthorizedError) { + await session.resetAndRedirect(routes.login); + } else { + setError(e.message); + } + } finally { + setSending(false); + } + }; + + const handleDialogSubmit = async () => { + if (!verificationCodeSent) { + await verifyPhone(); + } else { + await checkVerifyPhone(); + } + }; + + const handleCancel = () => { + if (verificationCodeSent) { + setVerificationCodeSent(false); + setCode(""); + } else { + props.onClose(); + } + }; + + return ( + + {t("account_basics_phone_numbers_dialog_title")} + + {t("account_basics_phone_numbers_dialog_description")} + {!verificationCodeSent && ( +
+ setPhoneNumber(ev.target.value)} + inputProps={{ inputMode: "tel", pattern: "+[0-9]*" }} + variant="standard" + sx={{ flexGrow: 1 }} + /> + + + setChannel(e.target.value)} />} + label={t("account_basics_phone_numbers_dialog_channel_sms")} + /> + setChannel(e.target.value)} />} + label={t("account_basics_phone_numbers_dialog_channel_call")} + sx={{ marginRight: 0 }} + /> + + +
+ )} + {verificationCodeSent && ( + setCode(ev.target.value)} + fullWidth + inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} + variant="standard" + /> + )} +
+ + + + +
+ ); +}; + +const Stats = () => { + const { t, i18n } = useTranslation(); + const { account } = useContext(AccountContext); + + if (!account) { + return <>; + } + + const normalize = (value, max) => Math.min((value / max) * 100, 100); + + return ( + + + {t("account_usage_title")} + + + {(account.role === Role.ADMIN || account.limits.reservations > 0) && ( + +
+ + {account.stats.reservations.toLocaleString()} + + + {account.role === Role.USER + ? t("account_usage_of_limit", { + limit: account.limits.reservations.toLocaleString(), + }) + : t("account_usage_unlimited")} + +
+ 0 + ? normalize(account.stats.reservations, account.limits.reservations) + : 100 + } + /> +
+ )} + + {t("account_usage_messages_title")} + + + + + + + } + > +
+ + {account.stats.messages.toLocaleString()} + + + {account.role === Role.USER + ? t("account_usage_of_limit", { + limit: account.limits.messages.toLocaleString(), + }) + : t("account_usage_unlimited")} + +
+ +
+ {config.enable_emails && ( + + {t("account_usage_emails_title")} + + + + + + + } + > +
+ + {account.stats.emails.toLocaleString()} + + + {account.role === Role.USER + ? t("account_usage_of_limit", { + limit: account.limits.emails.toLocaleString(), + }) + : t("account_usage_unlimited")} + +
+ +
+ )} + {config.enable_calls && (account.role === Role.ADMIN || account.limits.calls > 0) && ( + + {t("account_usage_calls_title")} + + + + + + + } + > +
+ + {account.stats.calls.toLocaleString()} + + + {account.role === Role.USER + ? t("account_usage_of_limit", { + limit: account.limits.calls.toLocaleString(), + }) + : t("account_usage_unlimited")} + +
+ 0 ? normalize(account.stats.calls, account.limits.calls) : 100} + /> +
+ )} + +
+ + {formatBytes(account.stats.attachment_total_size)} + + + {account.role === Role.USER + ? t("account_usage_of_limit", { + limit: formatBytes(account.limits.attachment_total_size), + }) + : t("account_usage_unlimited")} + +
+ +
+ {config.enable_reservations && account.role === Role.USER && account.limits.reservations === 0 && ( + + {t("account_usage_reservations_title")} + {config.enable_payments && } + + } + > + {t("account_usage_reservations_none")} + + )} + {config.enable_calls && account.role === Role.USER && account.limits.calls === 0 && ( + + {t("account_usage_calls_title")} + {config.enable_payments && } + + } + > + {t("account_usage_calls_none")} + + )} +
+ {account.role === Role.USER && account.limits.basis === LimitBasis.IP && ( + {t("account_usage_basis_ip_description")} + )} +
+ ); +}; + +const InfoIcon = () => ( + +); + +const Tokens = () => { + const { t } = useTranslation(); + const { account } = useContext(AccountContext); + const [dialogKey, setDialogKey] = useState(0); + const [dialogOpen, setDialogOpen] = useState(false); + const tokens = account?.tokens || []; + + const handleCreateClick = () => { + setDialogKey((prev) => prev + 1); + setDialogOpen(true); + }; + + const handleDialogClose = () => { + setDialogOpen(false); + }; + + return ( + + + + {t("account_tokens_title")} + + + , + }} + /> + +
{tokens?.length > 0 && }
+
+ + + + +
+ ); +}; + +const TokensTable = (props) => { + const { t, i18n } = useTranslation(); + const [snackOpen, setSnackOpen] = useState(false); + const [upsertDialogKey, setUpsertDialogKey] = useState(0); + const [upsertDialogOpen, setUpsertDialogOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [selectedToken, setSelectedToken] = useState(null); + + const tokens = (props.tokens || []).sort((a, b) => { + if (a.token === session.token()) { + return -1; + } + if (b.token === session.token()) { + return 1; + } + return a.token.localeCompare(b.token); + }); + + const handleEditClick = (token) => { + setUpsertDialogKey((prev) => prev + 1); + setSelectedToken(token); + setUpsertDialogOpen(true); + }; + + const handleDialogClose = () => { + setUpsertDialogOpen(false); + setDeleteDialogOpen(false); + setSelectedToken(null); + }; + + const handleDeleteClick = async (token) => { + setSelectedToken(token); + setDeleteDialogOpen(true); + }; + + const handleCopy = async (token) => { + await navigator.clipboard.writeText(token); + setSnackOpen(true); + }; + + return ( + + + + {t("account_tokens_table_token_header")} + {t("account_tokens_table_label_header")} + {t("account_tokens_table_expires_header")} + {t("account_tokens_table_last_access_header")} + + + + + {tokens.map((token) => ( + + + + {token.token.slice(0, 12)} + ... + + handleCopy(token.token)}> + + + + + + + {token.token === session.token() && {t("account_tokens_table_current_session")}} + {token.token !== session.token() && (token.label || "-")} + + + {token.expires ? formatShortDateTime(token.expires, i18n.language) : {t("account_tokens_table_never_expires")}} + + +
+ {formatShortDateTime(token.last_access, i18n.language)} + + openUrl(`https://whatismyipaddress.com/ip/${token.last_origin}`)}> + + + +
+
+ + {token.token !== session.token() && ( + <> + handleEditClick(token)} aria-label={t("account_tokens_dialog_title_edit")}> + + + handleDeleteClick(token)} aria-label={t("account_tokens_dialog_title_delete")}> + + + + )} + {token.token === session.token() && ( + + + + + + + + + + + )} + +
+ ))} +
+ + setSnackOpen(false)} + message={t("account_tokens_table_copied_to_clipboard")} + /> + + + +
+ ); +}; + +const TokenDialog = (props) => { + const theme = useTheme(); + const { t } = useTranslation(); + const [error, setError] = useState(""); + const [label, setLabel] = useState(props.token?.label || ""); + const [expires, setExpires] = useState(props.token ? -1 : 0); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); + const editMode = !!props.token; + + const handleSubmit = async () => { + try { + if (editMode) { + await accountApi.updateToken(props.token.token, label, expires); + } else { + await accountApi.createToken(label, expires); + } + props.onClose(); + } catch (e) { + console.log(`[Account] Error creating token`, e); + if (e instanceof UnauthorizedError) { + await session.resetAndRedirect(routes.login); + } else { + setError(e.message); + } + } + }; + + return ( + + {editMode ? t("account_tokens_dialog_title_edit") : t("account_tokens_dialog_title_create")} + + setLabel(ev.target.value)} + fullWidth + variant="standard" + /> + + + + + + + + + + ); +}; + +const TokenDeleteDialog = (props) => { + const { t } = useTranslation(); + const [error, setError] = useState(""); + + const handleSubmit = async () => { + try { + await accountApi.deleteToken(props.token.token); + props.onClose(); + } catch (e) { + console.log(`[Account] Error deleting token`, e); + if (e instanceof UnauthorizedError) { + await session.resetAndRedirect(routes.login); + } else { + setError(e.message); + } + } + }; + + return ( + + {t("account_tokens_delete_dialog_title")} + + + + + + + + + + + ); +}; + +const Delete = () => { + const { t } = useTranslation(); + return ( + + + {t("account_delete_title")} + + + + + + ); +}; + +const DeleteAccount = () => { + const { t } = useTranslation(); + const [dialogKey, setDialogKey] = useState(0); + const [dialogOpen, setDialogOpen] = useState(false); + + const handleDialogOpen = () => { + setDialogKey((prev) => prev + 1); + setDialogOpen(true); + }; + + const handleDialogClose = () => { + setDialogOpen(false); + }; + + return ( + +
+ +
+ +
+ ); +}; + +const DeleteAccountDialog = (props) => { + const theme = useTheme(); + const { t } = useTranslation(); + const { account } = useContext(AccountContext); + const [error, setError] = useState(""); + const [password, setPassword] = useState(""); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); + + const handleSubmit = async () => { + try { + await accountApi.delete(password); + await db().delete(); + console.debug(`[Account] Account deleted`); + await session.resetAndRedirect(routes.app); + } catch (e) { + console.log(`[Account] Error deleting account`, e); + if (e instanceof IncorrectPasswordError) { + setError(t("account_basics_password_dialog_current_password_incorrect")); + } else if (e instanceof UnauthorizedError) { + await session.resetAndRedirect(routes.login); + } else { + setError(e.message); + } + } + }; + + return ( + + {t("account_delete_title")} + + {t("account_delete_dialog_description")} + setPassword(ev.target.value)} + fullWidth + variant="standard" + /> + {account?.billing?.subscription && ( + + {t("account_delete_dialog_billing_warning")} + + )} + + + + + + + ); +}; + +export default Account; diff --git a/web/src/components/ActionBar.js b/web/src/components/ActionBar.js deleted file mode 100644 index 189ae1cb..00000000 --- a/web/src/components/ActionBar.js +++ /dev/null @@ -1,183 +0,0 @@ -import AppBar from "@mui/material/AppBar"; -import Navigation from "./Navigation"; -import Toolbar from "@mui/material/Toolbar"; -import IconButton from "@mui/material/IconButton"; -import MenuIcon from "@mui/icons-material/Menu"; -import Typography from "@mui/material/Typography"; -import * as React from "react"; -import {useState} from "react"; -import Box from "@mui/material/Box"; -import {topicDisplayName} from "../app/utils"; -import db from "../app/db"; -import {useLocation, useNavigate} from "react-router-dom"; -import MenuItem from '@mui/material/MenuItem'; -import MoreVertIcon from "@mui/icons-material/MoreVert"; -import NotificationsIcon from '@mui/icons-material/Notifications'; -import NotificationsOffIcon from '@mui/icons-material/NotificationsOff'; -import routes from "./routes"; -import subscriptionManager from "../app/SubscriptionManager"; -import logo from "../img/ntfy.svg"; -import {useTranslation} from "react-i18next"; -import session from "../app/Session"; -import AccountCircleIcon from '@mui/icons-material/AccountCircle'; -import Button from "@mui/material/Button"; -import Divider from "@mui/material/Divider"; -import {Logout, Person, Settings} from "@mui/icons-material"; -import ListItemIcon from "@mui/material/ListItemIcon"; -import accountApi from "../app/AccountApi"; -import PopupMenu from "./PopupMenu"; -import { SubscriptionPopup } from "./SubscriptionPopup"; - -const ActionBar = (props) => { - const { t } = useTranslation(); - const location = useLocation(); - let title = "ntfy"; - if (props.selected) { - title = topicDisplayName(props.selected); - } else if (location.pathname === routes.settings) { - title = t("action_bar_settings"); - } else if (location.pathname === routes.account) { - title = t("action_bar_account"); - } - return ( - Navigation (1200), but < Dialog (1300) - ml: { sm: `${Navigation.width}px` } - }}> - - - - - - - {title} - - {props.selected && - } - - - - ); -}; - -const SettingsIcons = (props) => { - const { t } = useTranslation(); - const [anchorEl, setAnchorEl] = useState(null); - const subscription = props.subscription; - - const handleToggleMute = async () => { - const mutedUntil = (subscription.mutedUntil) ? 0 : 1; // Make this a timestamp in the future - await subscriptionManager.setMutedUntil(subscription.id, mutedUntil); - } - - return ( - <> - - {subscription.mutedUntil ? : } - - setAnchorEl(ev.currentTarget)} aria-label={t("action_bar_toggle_action_menu")}> - - - setAnchorEl(null)} - /> - - ); -}; - -const ProfileIcon = () => { - const { t } = useTranslation(); - const [anchorEl, setAnchorEl] = useState(null); - const open = Boolean(anchorEl); - const navigate = useNavigate(); - - const handleClick = (event) => { - setAnchorEl(event.currentTarget); - }; - - const handleClose = () => { - setAnchorEl(null); - }; - - const handleLogout = async () => { - try { - await accountApi.logout(); - await db.delete(); - } finally { - session.resetAndRedirect(routes.app); - } - }; - - return ( - <> - {session.exists() && - - - - } - {!session.exists() && config.enable_login && - - } - {!session.exists() && config.enable_signup && - - } - - navigate(routes.account)}> - - - - {session.username()} - - - navigate(routes.settings)}> - - - - {t("action_bar_profile_settings")} - - - - - - {t("action_bar_profile_logout")} - - - - ); -}; - -export default ActionBar; diff --git a/web/src/components/ActionBar.jsx b/web/src/components/ActionBar.jsx new file mode 100644 index 00000000..1f41aac0 --- /dev/null +++ b/web/src/components/ActionBar.jsx @@ -0,0 +1,192 @@ +import { AppBar, Toolbar, IconButton, Typography, Box, MenuItem, Button, Divider, ListItemIcon, useTheme } from "@mui/material"; +import MenuIcon from "@mui/icons-material/Menu"; +import * as React from "react"; +import { useState } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import MoreVertIcon from "@mui/icons-material/MoreVert"; +import NotificationsIcon from "@mui/icons-material/Notifications"; +import NotificationsOffIcon from "@mui/icons-material/NotificationsOff"; +import { useTranslation } from "react-i18next"; +import AccountCircleIcon from "@mui/icons-material/AccountCircle"; +import { Logout, Person, Settings } from "@mui/icons-material"; +import session from "../app/Session"; +import logo from "../img/ntfy.svg"; +import subscriptionManager from "../app/SubscriptionManager"; +import routes from "./routes"; +import db from "../app/db"; +import { topicDisplayName } from "../app/utils"; +import Navigation from "./Navigation"; +import accountApi from "../app/AccountApi"; +import PopupMenu from "./PopupMenu"; +import { SubscriptionPopup } from "./SubscriptionPopup"; +import { useIsLaunchedPWA } from "./hooks"; + +const ActionBar = (props) => { + const theme = useTheme(); + const { t } = useTranslation(); + const location = useLocation(); + const isLaunchedPWA = useIsLaunchedPWA(); + + let title = "ntfy"; + if (props.selected) { + title = topicDisplayName(props.selected); + } else if (location.pathname === routes.settings) { + title = t("action_bar_settings"); + } else if (location.pathname === routes.account) { + title = t("action_bar_account"); + } + + const getActionBarBackground = () => { + if (isLaunchedPWA) { + return "#317f6f"; + } + + switch (theme.palette.mode) { + case "dark": + return "linear-gradient(150deg, #203631 0%, #2a6e60 100%)"; + + case "light": + default: + return "linear-gradient(150deg, #338574 0%, #56bda8 100%)"; + } + }; + + return ( + Navigation (1200), but < Dialog (1300) + ml: { sm: `${Navigation.width}px` }, + }} + > + + + + + + + {title} + + {props.selected && } + + + + ); +}; + +const SettingsIcons = (props) => { + const { t } = useTranslation(); + const [anchorEl, setAnchorEl] = useState(null); + const { subscription } = props; + + const handleToggleMute = async () => { + const mutedUntil = subscription.mutedUntil ? 0 : 1; // Make this a timestamp in the future + await subscriptionManager.setMutedUntil(subscription.id, mutedUntil); + }; + + return ( + <> + + {subscription.mutedUntil ? : } + + setAnchorEl(ev.currentTarget)} + aria-label={t("action_bar_toggle_action_menu")} + > + + + setAnchorEl(null)} /> + + ); +}; + +const ProfileIcon = () => { + const { t } = useTranslation(); + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + const navigate = useNavigate(); + + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleLogout = async () => { + try { + await accountApi.logout(); + await db().delete(); + } finally { + await session.resetAndRedirect(routes.app); + } + }; + + return ( + <> + {session.exists() && ( + + + + )} + {!session.exists() && config.enable_login && ( + + )} + {!session.exists() && config.enable_signup && ( + + )} + + navigate(routes.account)}> + + + + {session.username()} + + + navigate(routes.settings)}> + + + + {t("action_bar_profile_settings")} + + + + + + {t("action_bar_profile_logout")} + + + + ); +}; + +export default ActionBar; diff --git a/web/src/components/App.js b/web/src/components/App.js deleted file mode 100644 index 861a3709..00000000 --- a/web/src/components/App.js +++ /dev/null @@ -1,147 +0,0 @@ -import * as React from 'react'; -import {createContext, Suspense, useContext, useEffect, useState} from 'react'; -import Box from '@mui/material/Box'; -import {ThemeProvider} from '@mui/material/styles'; -import CssBaseline from '@mui/material/CssBaseline'; -import Toolbar from '@mui/material/Toolbar'; -import {AllSubscriptions, SingleSubscription} from "./Notifications"; -import theme from "./theme"; -import Navigation from "./Navigation"; -import ActionBar from "./ActionBar"; -import notifier from "../app/Notifier"; -import Preferences from "./Preferences"; -import {useLiveQuery} from "dexie-react-hooks"; -import subscriptionManager from "../app/SubscriptionManager"; -import userManager from "../app/UserManager"; -import {BrowserRouter, Outlet, Route, Routes, useParams} from "react-router-dom"; -import {expandUrl} from "../app/utils"; -import ErrorBoundary from "./ErrorBoundary"; -import routes from "./routes"; -import {useAccountListener, useBackgroundProcesses, useConnectionListeners} from "./hooks"; -import PublishDialog from "./PublishDialog"; -import Messaging from "./Messaging"; -import "./i18n"; // Translations! -import {Backdrop, CircularProgress} from "@mui/material"; -import Login from "./Login"; -import Signup from "./Signup"; -import Account from "./Account"; - -export const AccountContext = createContext(null); - -const App = () => { - const [account, setAccount] = useState(null); - return ( - }> - - - - - - - }/> - }/> - }> - }/> - }/> - }/> - }/> - }/> - - - - - - - - ); -} - -const Layout = () => { - const params = useParams(); - const { account, setAccount } = useContext(AccountContext); - const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false); - const [notificationsGranted, setNotificationsGranted] = useState(notifier.granted()); - const [sendDialogOpenMode, setSendDialogOpenMode] = useState(""); - const users = useLiveQuery(() => userManager.all()); - const subscriptions = useLiveQuery(() => subscriptionManager.all()); - const subscriptionsWithoutInternal = subscriptions?.filter(s => !s.internal); - const newNotificationsCount = subscriptionsWithoutInternal?.reduce((prev, cur) => prev + cur.new, 0) || 0; - const [selected] = (subscriptionsWithoutInternal || []).filter(s => { - return (params.baseUrl && expandUrl(params.baseUrl).includes(s.baseUrl) && params.topic === s.topic) - || (config.base_url === s.baseUrl && params.topic === s.topic) - }); - - useConnectionListeners(account, subscriptions, users); - useAccountListener(setAccount) - useBackgroundProcesses(); - useEffect(() => updateTitle(newNotificationsCount), [newNotificationsCount]); - - return ( - - setMobileDrawerOpen(!mobileDrawerOpen)} - /> - setMobileDrawerOpen(!mobileDrawerOpen)} - onNotificationGranted={setNotificationsGranted} - onPublishMessageClick={() => setSendDialogOpenMode(PublishDialog.OPEN_MODE_DEFAULT)} - /> -
- - -
- -
- ); -} - -const Main = (props) => { - return ( - theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[900] - }} - > - {props.children} - - ); -}; - -const Loader = () => ( - theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[900] - }} - > - - -); - -const updateTitle = (newNotificationsCount) => { - document.title = (newNotificationsCount > 0) ? `(${newNotificationsCount}) ntfy` : "ntfy"; -} - -export default App; diff --git a/web/src/components/App.jsx b/web/src/components/App.jsx new file mode 100644 index 00000000..7f84b7de --- /dev/null +++ b/web/src/components/App.jsx @@ -0,0 +1,172 @@ +import * as React from "react"; +import { createContext, Suspense, useContext, useEffect, useState, useMemo } from "react"; +import { Box, Toolbar, CssBaseline, Backdrop, CircularProgress, useMediaQuery, ThemeProvider, createTheme } from "@mui/material"; +import { useLiveQuery } from "dexie-react-hooks"; +import { BrowserRouter, Outlet, Route, Routes, useParams } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { AllSubscriptions, SingleSubscription } from "./Notifications"; +import { darkTheme, lightTheme } from "./theme"; +import Navigation from "./Navigation"; +import ActionBar from "./ActionBar"; +import Preferences from "./Preferences"; +import subscriptionManager from "../app/SubscriptionManager"; +import userManager from "../app/UserManager"; +import { expandUrl, getKebabCaseLangStr } from "../app/utils"; +import ErrorBoundary from "./ErrorBoundary"; +import routes from "./routes"; +import { useAccountListener, useBackgroundProcesses, useConnectionListeners, useWebPushTopics } from "./hooks"; +import PublishDialog from "./PublishDialog"; +import Messaging from "./Messaging"; +import Login from "./Login"; +import Signup from "./Signup"; +import Account from "./Account"; +import initI18n from "../app/i18n"; // Translations! +import prefs, { THEME } from "../app/Prefs"; +import RTLCacheProvider from "./RTLCacheProvider"; + +initI18n(); + +export const AccountContext = createContext(null); + +const darkModeEnabled = (prefersDarkMode, themePreference) => { + switch (themePreference) { + case THEME.DARK: + return true; + + case THEME.LIGHT: + return false; + + case THEME.SYSTEM: + default: + return prefersDarkMode; + } +}; + +const App = () => { + const { i18n } = useTranslation(); + const languageDir = i18n.dir(); + + const [account, setAccount] = useState(null); + const accountMemo = useMemo(() => ({ account, setAccount }), [account, setAccount]); + const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)"); + const themePreference = useLiveQuery(() => prefs.theme()); + const theme = React.useMemo( + () => createTheme({ ...(darkModeEnabled(prefersDarkMode, themePreference) ? darkTheme : lightTheme), direction: languageDir }), + [prefersDarkMode, themePreference, languageDir] + ); + + useEffect(() => { + document.documentElement.setAttribute("lang", getKebabCaseLangStr(i18n.language)); + document.dir = languageDir; + }, [i18n.language, languageDir]); + + return ( + }> + + + + + + + + } /> + } /> + }> + } /> + } /> + } /> + } /> + } /> + + + + + + + + + ); +}; + +const updateTitle = (newNotificationsCount) => { + document.title = newNotificationsCount > 0 ? `(${newNotificationsCount}) ntfy` : "ntfy"; + window.navigator.setAppBadge?.(newNotificationsCount); +}; + +const Layout = () => { + const params = useParams(); + const { account, setAccount } = useContext(AccountContext); + const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false); + const [sendDialogOpenMode, setSendDialogOpenMode] = useState(""); + const users = useLiveQuery(() => userManager.all()); + const subscriptions = useLiveQuery(() => subscriptionManager.all()); + const webPushTopics = useWebPushTopics(); + const subscriptionsWithoutInternal = subscriptions?.filter((s) => !s.internal); + const newNotificationsCount = subscriptionsWithoutInternal?.reduce((prev, cur) => prev + cur.new, 0) || 0; + const [selected] = (subscriptionsWithoutInternal || []).filter( + (s) => + (params.baseUrl && expandUrl(params.baseUrl).includes(s.baseUrl) && params.topic === s.topic) || + (config.base_url === s.baseUrl && params.topic === s.topic) + ); + + useConnectionListeners(account, subscriptions, users, webPushTopics); + useAccountListener(setAccount); + useBackgroundProcesses(); + useEffect(() => updateTitle(newNotificationsCount), [newNotificationsCount]); + + return ( + + setMobileDrawerOpen(!mobileDrawerOpen)} /> + setMobileDrawerOpen(!mobileDrawerOpen)} + onPublishMessageClick={() => setSendDialogOpenMode(PublishDialog.OPEN_MODE_DEFAULT)} + /> +
+ + +
+ +
+ ); +}; + +const Main = (props) => ( + (palette.mode === "light" ? palette.grey[100] : palette.grey[900]), + }} + > + {props.children} + +); + +const Loader = () => ( + (palette.mode === "light" ? palette.grey[100] : palette.grey[900]), + }} + > + + +); + +export default App; diff --git a/web/src/components/AttachmentIcon.js b/web/src/components/AttachmentIcon.js deleted file mode 100644 index 337760b7..00000000 --- a/web/src/components/AttachmentIcon.js +++ /dev/null @@ -1,47 +0,0 @@ -import * as React from "react"; -import Box from "@mui/material/Box"; -import fileDocument from "../img/file-document.svg"; -import fileImage from "../img/file-image.svg"; -import fileVideo from "../img/file-video.svg"; -import fileAudio from "../img/file-audio.svg"; -import fileApp from "../img/file-app.svg"; -import {useTranslation} from "react-i18next"; - -const AttachmentIcon = (props) => { - const { t } = useTranslation(); - const type = props.type; - let imageFile, imageLabel; - if (!type) { - imageFile = fileDocument; - imageLabel = t("notifications_attachment_file_image"); - } else if (type.startsWith('image/')) { - imageFile = fileImage; - imageLabel = t("notifications_attachment_file_video"); - } else if (type.startsWith('video/')) { - imageFile = fileVideo; - imageLabel = t("notifications_attachment_file_video"); - } else if (type.startsWith('audio/')) { - imageFile = fileAudio; - imageLabel = t("notifications_attachment_file_audio"); - } else if (type === "application/vnd.android.package-archive") { - imageFile = fileApp; - imageLabel = t("notifications_attachment_file_app"); - } else { - imageFile = fileDocument; - imageLabel = t("notifications_attachment_file_document"); - } - return ( - - ); -} - -export default AttachmentIcon; diff --git a/web/src/components/AttachmentIcon.jsx b/web/src/components/AttachmentIcon.jsx new file mode 100644 index 00000000..9a2581e9 --- /dev/null +++ b/web/src/components/AttachmentIcon.jsx @@ -0,0 +1,48 @@ +import * as React from "react"; +import { Box } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import fileDocument from "../img/file-document.svg"; +import fileImage from "../img/file-image.svg"; +import fileVideo from "../img/file-video.svg"; +import fileAudio from "../img/file-audio.svg"; +import fileApp from "../img/file-app.svg"; + +const AttachmentIcon = (props) => { + const { t } = useTranslation(); + const { type } = props; + let imageFile; + let imageLabel; + if (!type) { + imageFile = fileDocument; + imageLabel = t("notifications_attachment_file_image"); + } else if (type.startsWith("image/")) { + imageFile = fileImage; + imageLabel = t("notifications_attachment_file_video"); + } else if (type.startsWith("video/")) { + imageFile = fileVideo; + imageLabel = t("notifications_attachment_file_video"); + } else if (type.startsWith("audio/")) { + imageFile = fileAudio; + imageLabel = t("notifications_attachment_file_audio"); + } else if (type === "application/vnd.android.package-archive") { + imageFile = fileApp; + imageLabel = t("notifications_attachment_file_app"); + } else { + imageFile = fileDocument; + imageLabel = t("notifications_attachment_file_document"); + } + return ( + + ); +}; + +export default AttachmentIcon; diff --git a/web/src/components/AvatarBox.js b/web/src/components/AvatarBox.js deleted file mode 100644 index 2278f605..00000000 --- a/web/src/components/AvatarBox.js +++ /dev/null @@ -1,29 +0,0 @@ -import * as React from 'react'; -import {Avatar} from "@mui/material"; -import Box from "@mui/material/Box"; -import logo from "../img/ntfy-filled.svg"; - -const AvatarBox = (props) => { - return ( - - - {props.children} - - ); -} - -export default AvatarBox; diff --git a/web/src/components/AvatarBox.jsx b/web/src/components/AvatarBox.jsx new file mode 100644 index 00000000..37c85d4e --- /dev/null +++ b/web/src/components/AvatarBox.jsx @@ -0,0 +1,23 @@ +import * as React from "react"; +import { Avatar, Box, styled } from "@mui/material"; +import logo from "../img/ntfy-filled.svg"; + +const AvatarBoxContainer = styled(Box)` + display: flex; + flex-grow: 1; + justify-content: center; + flex-direction: column; + align-content: center; + align-items: center; + height: 100dvh; + max-width: min(400px, 90dvw); + margin: auto; +`; +const AvatarBox = (props) => ( + + + {props.children} + +); + +export default AvatarBox; diff --git a/web/src/components/DialogFooter.js b/web/src/components/DialogFooter.js deleted file mode 100644 index 68d17c73..00000000 --- a/web/src/components/DialogFooter.js +++ /dev/null @@ -1,33 +0,0 @@ -import * as React from "react"; -import Box from "@mui/material/Box"; -import DialogContentText from "@mui/material/DialogContentText"; -import DialogActions from "@mui/material/DialogActions"; - -const DialogFooter = (props) => { - return ( - - - {props.status} - - - {props.children} - - - ); -}; - -export default DialogFooter; diff --git a/web/src/components/DialogFooter.jsx b/web/src/components/DialogFooter.jsx new file mode 100644 index 00000000..bcaf4cfc --- /dev/null +++ b/web/src/components/DialogFooter.jsx @@ -0,0 +1,29 @@ +import * as React from "react"; +import { Box, DialogContentText, DialogActions } from "@mui/material"; + +const DialogFooter = (props) => ( + + + {props.status} + + {props.children} + +); + +export default DialogFooter; diff --git a/web/src/components/EmojiPicker.js b/web/src/components/EmojiPicker.js deleted file mode 100644 index 9b29e8f0..00000000 --- a/web/src/components/EmojiPicker.js +++ /dev/null @@ -1,179 +0,0 @@ -import * as React from 'react'; -import {useRef, useState} from 'react'; -import Typography from '@mui/material/Typography'; -import {rawEmojis} from '../app/emojis'; -import Box from "@mui/material/Box"; -import TextField from "@mui/material/TextField"; -import {ClickAwayListener, Fade, InputAdornment, styled} from "@mui/material"; -import IconButton from "@mui/material/IconButton"; -import {Close} from "@mui/icons-material"; -import Popper from "@mui/material/Popper"; -import {splitNoEmpty} from "../app/utils"; -import {useTranslation} from "react-i18next"; - -// Create emoji list by category and create a search base (string with all search words) -// -// This also filters emojis that are not supported by Desktop Chrome. -// This is a hack, but on Ubuntu 18.04, with Chrome 99, only Emoji <= 11 are supported. - -const emojisByCategory = {}; -const isDesktopChrome = /Chrome/.test(navigator.userAgent) && !/Mobile/.test(navigator.userAgent); -const maxSupportedVersionForDesktopChrome = 11; -rawEmojis.forEach(emoji => { - if (!emojisByCategory[emoji.category]) { - emojisByCategory[emoji.category] = []; - } - try { - const unicodeVersion = parseFloat(emoji.unicode_version); - const supportedEmoji = unicodeVersion <= maxSupportedVersionForDesktopChrome || !isDesktopChrome; - if (supportedEmoji) { - const searchBase = `${emoji.description.toLowerCase()} ${emoji.aliases.join(" ")} ${emoji.tags.join(" ")}`; - const emojiWithSearchBase = { ...emoji, searchBase: searchBase }; - emojisByCategory[emoji.category].push(emojiWithSearchBase); - } - } catch (e) { - // Nothing. Ignore. - } -}); - -const EmojiPicker = (props) => { - const { t } = useTranslation(); - const open = Boolean(props.anchorEl); - const [search, setSearch] = useState(""); - const searchRef = useRef(null); - const searchFields = splitNoEmpty(search.toLowerCase(), " "); - - const handleSearchClear = () => { - setSearch(""); - searchRef.current?.focus(); - }; - - return ( - - {({ TransitionProps }) => ( - - - - setSearch(ev.target.value)} - type="text" - variant="standard" - fullWidth - sx={{ marginTop: 0, marginBottom: "12px", paddingRight: 2 }} - inputProps={{ - role: "searchbox", - "aria-label": t("emoji_picker_search_placeholder") - }} - InputProps={{ - endAdornment: - - - - - - }} - /> - - {Object.keys(emojisByCategory).map(category => - - )} - - - - - )} - - ); -}; - -const Category = (props) => { - const showTitle = props.search.length === 0; - return ( - <> - {showTitle && - - {props.title} - - } - {props.emojis.map(emoji => - props.onPick(emoji.aliases[0])} - /> - )} - - ); -}; - -const Emoji = (props) => { - const emoji = props.emoji; - const matches = emojiMatches(emoji, props.search); - const title = `${emoji.description} (${emoji.aliases[0]})`; - return ( - - {props.emoji.emoji} - - ); -}; - -const EmojiDiv = styled("div")({ - fontSize: "30px", - width: "30px", - height: "30px", - marginTop: "8px", - marginBottom: "8px", - marginRight: "8px", - lineHeight: "30px", - cursor: "pointer", - opacity: 0.85, - "&:hover": { - opacity: 1 - } -}); - -const emojiMatches = (emoji, words) => { - if (words.length === 0) { - return true; - } - for (const word of words) { - if (emoji.searchBase.indexOf(word) === -1) { - return false; - } - } - return true; -} - -export default EmojiPicker; diff --git a/web/src/components/EmojiPicker.jsx b/web/src/components/EmojiPicker.jsx new file mode 100644 index 00000000..d1fb1706 --- /dev/null +++ b/web/src/components/EmojiPicker.jsx @@ -0,0 +1,158 @@ +import * as React from "react"; +import { useRef, useState } from "react"; +import { Typography, Box, TextField, ClickAwayListener, Fade, InputAdornment, styled, IconButton, Popper } from "@mui/material"; +import { Close } from "@mui/icons-material"; +import { useTranslation } from "react-i18next"; +import { splitNoEmpty } from "../app/utils"; +import { rawEmojis } from "../app/emojis"; + +// Create emoji list by category and create a search base (string with all search words) +// +// This also filters emojis that are not supported by Desktop Chrome. +// This is a hack, but on Ubuntu 18.04, with Chrome 99, only Emoji <= 11 are supported. + +const emojisByCategory = {}; +const isDesktopChrome = /Chrome/.test(navigator.userAgent) && !/Mobile/.test(navigator.userAgent); +const maxSupportedVersionForDesktopChrome = 11; +rawEmojis.forEach((emoji) => { + if (!emojisByCategory[emoji.category]) { + emojisByCategory[emoji.category] = []; + } + try { + const unicodeVersion = parseFloat(emoji.unicode_version); + const supportedEmoji = unicodeVersion <= maxSupportedVersionForDesktopChrome || !isDesktopChrome; + if (supportedEmoji) { + const searchBase = `${emoji.description.toLowerCase()} ${emoji.aliases.join(" ")} ${emoji.tags.join(" ")}`; + const emojiWithSearchBase = { ...emoji, searchBase }; + emojisByCategory[emoji.category].push(emojiWithSearchBase); + } + } catch (e) { + // Nothing. Ignore. + } +}); + +const EmojiPicker = (props) => { + const { t } = useTranslation(); + const open = Boolean(props.anchorEl); + const [search, setSearch] = useState(""); + const searchRef = useRef(null); + const searchFields = splitNoEmpty(search.toLowerCase(), " "); + + const handleSearchClear = () => { + setSearch(""); + searchRef.current?.focus(); + }; + + return ( + + {({ TransitionProps }) => ( + + + + setSearch(ev.target.value)} + type="text" + variant="standard" + fullWidth + sx={{ marginTop: 0, marginBottom: "12px", paddingRight: 2 }} + inputProps={{ + role: "searchbox", + "aria-label": t("emoji_picker_search_placeholder"), + }} + InputProps={{ + endAdornment: ( + + + + + + ), + }} + /> + + {Object.keys(emojisByCategory).map((category) => ( + + ))} + + + + + )} + + ); +}; + +const Category = (props) => { + const showTitle = props.search.length === 0; + return ( + <> + {showTitle && ( + + {props.title} + + )} + {props.emojis.map((emoji) => ( + props.onPick(emoji.aliases[0])} /> + ))} + + ); +}; + +const emojiMatches = (emoji, words) => words.length === 0 || words.some((word) => emoji.searchBase.includes(word)); + +const Emoji = (props) => { + const { emoji } = props; + const matches = emojiMatches(emoji, props.search); + const title = `${emoji.description} (${emoji.aliases[0]})`; + return ( + + {props.emoji.emoji} + + ); +}; + +const EmojiDiv = styled("div")({ + fontSize: "30px", + width: "30px", + height: "30px", + marginTop: "8px", + marginBottom: "8px", + marginRight: "8px", + lineHeight: "30px", + cursor: "pointer", + opacity: 0.85, + "&:hover": { + opacity: 1, + }, +}); + +export default EmojiPicker; diff --git a/web/src/components/ErrorBoundary.js b/web/src/components/ErrorBoundary.js deleted file mode 100644 index c6d789a3..00000000 --- a/web/src/components/ErrorBoundary.js +++ /dev/null @@ -1,129 +0,0 @@ -import * as React from "react"; -import StackTrace from "stacktrace-js"; -import {CircularProgress, Link} from "@mui/material"; -import Button from "@mui/material/Button"; -import {Trans, withTranslation} from "react-i18next"; - -class ErrorBoundaryImpl extends React.Component { - constructor(props) { - super(props); - this.state = { - error: false, - originalStack: null, - niceStack: null, - unsupportedIndexedDB: false - }; - } - - componentDidCatch(error, info) { - console.error("[ErrorBoundary] Error caught", error, info); - - // Special case for unsupported IndexedDB in Private Browsing mode (Firefox, Safari), see - // - https://github.com/dexie/Dexie.js/issues/312 - // - https://bugzilla.mozilla.org/show_bug.cgi?id=781982 - const isUnsupportedIndexedDB = error?.name === "InvalidStateError" || - (error?.name === "DatabaseClosedError" && error?.message?.indexOf("InvalidStateError") !== -1); - - if (isUnsupportedIndexedDB) { - this.handleUnsupportedIndexedDB(); - } else { - this.handleError(error, info); - } - } - - handleError(error, info) { - // Immediately render original stack trace - const prettierOriginalStack = info.componentStack - .trim() - .split("\n") - .map(line => ` at ${line}`) - .join("\n"); - this.setState({ - error: true, - originalStack: `${error.toString()}\n${prettierOriginalStack}` - }); - - // Fetch additional info and a better stack trace - StackTrace.fromError(error).then(stack => { - console.error("[ErrorBoundary] Stacktrace fetched", stack); - const niceStack = `${error.toString()}\n` + stack.map( el => ` at ${el.functionName} (${el.fileName}:${el.columnNumber}:${el.lineNumber})`).join("\n"); - this.setState({ niceStack }); - }); - } - - handleUnsupportedIndexedDB() { - this.setState({ - error: true, - unsupportedIndexedDB: true - }); - } - - copyStack() { - let stack = ""; - if (this.state.niceStack) { - stack += `${this.state.niceStack}\n\n`; - } - stack += `${this.state.originalStack}\n`; - navigator.clipboard.writeText(stack); - } - - render() { - if (this.state.error) { - if (this.state.unsupportedIndexedDB) { - return this.renderUnsupportedIndexedDB(); - } else { - return this.renderError(); - } - } - return this.props.children; - } - - renderUnsupportedIndexedDB() { - const { t } = this.props; - return ( -
-

{t("error_boundary_unsupported_indexeddb_title")} 😮

-

- , - discordLink: , - matrixLink: - }} - /> -

-
- ); - } - - renderError() { - const { t } = this.props; - return ( -
-

{t("error_boundary_title")} 😮

-

- , - discordLink: , - matrixLink: - }} - /> -

-

- -

-

{t("error_boundary_stack_trace")}

- {this.state.niceStack - ?
{this.state.niceStack}
- : <> {t("error_boundary_gathering_info")}} -
{this.state.originalStack}
-
- ); - } -} - -const ErrorBoundary = withTranslation()(ErrorBoundaryImpl); // Adds props.t -export default ErrorBoundary; diff --git a/web/src/components/ErrorBoundary.jsx b/web/src/components/ErrorBoundary.jsx new file mode 100644 index 00000000..adb177c6 --- /dev/null +++ b/web/src/components/ErrorBoundary.jsx @@ -0,0 +1,138 @@ +import * as React from "react"; +import StackTrace from "stacktrace-js"; +import { CircularProgress, Link, Button } from "@mui/material"; +import { Trans, withTranslation } from "react-i18next"; + +class ErrorBoundaryImpl extends React.Component { + constructor(props) { + super(props); + this.state = { + error: false, + originalStack: null, + niceStack: null, + unsupportedIndexedDB: false, + }; + } + + componentDidCatch(error, info) { + console.error("[ErrorBoundary] Error caught", error, info); + + // Special case for unsupported IndexedDB in Private Browsing mode (Firefox, Safari), see + // - https://github.com/dexie/Dexie.js/issues/312 + // - https://bugzilla.mozilla.org/show_bug.cgi?id=781982 + const isUnsupportedIndexedDB = + error?.name === "InvalidStateError" || (error?.name === "DatabaseClosedError" && error?.message?.indexOf("InvalidStateError") !== -1); + + if (isUnsupportedIndexedDB) { + this.handleUnsupportedIndexedDB(); + } else { + this.handleError(error, info); + } + } + + handleError(error, info) { + // Immediately render original stack trace + const prettierOriginalStack = info.componentStack + .trim() + .split("\n") + .map((line) => ` at ${line}`) + .join("\n"); + this.setState({ + error: true, + originalStack: `${error.toString()}\n${prettierOriginalStack}`, + }); + + // Fetch additional info and a better stack trace + StackTrace.fromError(error).then((stack) => { + console.error("[ErrorBoundary] Stacktrace fetched", stack); + const stackString = stack.map((el) => ` at ${el.functionName} (${el.fileName}:${el.columnNumber}:${el.lineNumber})`).join("\n"); + const niceStack = `${error.toString()}\n${stackString}`; + this.setState({ niceStack }); + }); + } + + handleUnsupportedIndexedDB() { + this.setState({ + error: true, + unsupportedIndexedDB: true, + }); + } + + copyStack() { + let stack = ""; + if (this.state.niceStack) { + stack += `${this.state.niceStack}\n\n`; + } + stack += `${this.state.originalStack}\n`; + navigator.clipboard.writeText(stack); + } + + renderUnsupportedIndexedDB() { + const { t } = this.props; + return ( +
+

{t("error_boundary_unsupported_indexeddb_title")} 😮

+

+ , + discordLink: , + matrixLink: , + }} + /> +

+
+ ); + } + + renderError() { + const { t } = this.props; + return ( +
+

{t("error_boundary_title")} 😮

+

+ , + discordLink: , + matrixLink: , + }} + /> +

+
+ + + +
+

{t("error_boundary_stack_trace")}

+ {this.state.niceStack ? ( +
{this.state.niceStack}
+ ) : ( + <> + {t("error_boundary_gathering_info")} + + )} +
{this.state.originalStack}
+
+ ); + } + + render() { + if (this.state.error) { + if (this.state.unsupportedIndexedDB) { + return this.renderUnsupportedIndexedDB(); + } + return this.renderError(); + } + return this.props.children; + } +} + +const ErrorBoundary = withTranslation()(ErrorBoundaryImpl); // Adds props.t +export default ErrorBoundary; diff --git a/web/src/components/Login.js b/web/src/components/Login.js deleted file mode 100644 index 8b14c53d..00000000 --- a/web/src/components/Login.js +++ /dev/null @@ -1,122 +0,0 @@ -import * as React from 'react'; -import {useState} from 'react'; -import Typography from "@mui/material/Typography"; -import WarningAmberIcon from '@mui/icons-material/WarningAmber'; -import TextField from "@mui/material/TextField"; -import Button from "@mui/material/Button"; -import Box from "@mui/material/Box"; -import routes from "./routes"; -import session from "../app/Session"; -import {NavLink} from "react-router-dom"; -import AvatarBox from "./AvatarBox"; -import {useTranslation} from "react-i18next"; -import accountApi from "../app/AccountApi"; -import IconButton from "@mui/material/IconButton"; -import {InputAdornment} from "@mui/material"; -import {Visibility, VisibilityOff} from "@mui/icons-material"; -import {UnauthorizedError} from "../app/errors"; - -const Login = () => { - const { t } = useTranslation(); - const [error, setError] = useState(""); - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - const [showPassword, setShowPassword] = useState(false); - - const handleSubmit = async (event) => { - event.preventDefault(); - const user = { username, password }; - try { - const token = await accountApi.login(user); - console.log(`[Login] User auth for user ${user.username} successful, token is ${token}`); - session.store(user.username, token); - window.location.href = routes.app; - } catch (e) { - console.log(`[Login] User auth for user ${user.username} failed`, e); - if (e instanceof UnauthorizedError) { - setError(t("Login failed: Invalid username or password")); - } else { - setError(e.message); - } - } - }; - if (!config.enable_login) { - return ( - - {t("login_disabled")} - - ); - } - return ( - - - {t("login_title")} - - - setUsername(ev.target.value.trim())} - autoFocus - /> - setPassword(ev.target.value.trim())} - autoComplete="current-password" - InputProps={{ - endAdornment: ( - - setShowPassword(!showPassword)} - onMouseDown={(ev) => ev.preventDefault()} - edge="end" - > - {showPassword ? : } - - - ) - }} - /> - - {error && - - - {error} - - } - - {/* This is where the password reset link would go */} - {config.enable_signup &&
{t("login_link_signup")}
} -
-
-
- ); -} - -export default Login; diff --git a/web/src/components/Login.jsx b/web/src/components/Login.jsx new file mode 100644 index 00000000..5c1af249 --- /dev/null +++ b/web/src/components/Login.jsx @@ -0,0 +1,117 @@ +import * as React from "react"; +import { useState } from "react"; +import { Typography, TextField, Button, Box, IconButton, InputAdornment } from "@mui/material"; +import WarningAmberIcon from "@mui/icons-material/WarningAmber"; +import { NavLink } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { Visibility, VisibilityOff } from "@mui/icons-material"; +import accountApi from "../app/AccountApi"; +import AvatarBox from "./AvatarBox"; +import session from "../app/Session"; +import routes from "./routes"; +import { UnauthorizedError } from "../app/errors"; + +const Login = () => { + const { t } = useTranslation(); + const [error, setError] = useState(""); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [showPassword, setShowPassword] = useState(false); + + const handleSubmit = async (event) => { + event.preventDefault(); + const user = { username, password }; + try { + const token = await accountApi.login(user); + console.log(`[Login] User auth for user ${user.username} successful, token is ${token}`); + await session.store(user.username, token); + window.location.href = routes.app; + } catch (e) { + console.log(`[Login] User auth for user ${user.username} failed`, e); + if (e instanceof UnauthorizedError) { + setError(t("Login failed: Invalid username or password")); + } else { + setError(e.message); + } + } + }; + if (!config.enable_login) { + return ( + + {t("login_disabled")} + + ); + } + return ( + + {t("login_title")} + + setUsername(ev.target.value.trim())} + autoFocus + /> + setPassword(ev.target.value.trim())} + autoComplete="current-password" + InputProps={{ + endAdornment: ( + + setShowPassword(!showPassword)} + onMouseDown={(ev) => ev.preventDefault()} + edge="end" + > + {showPassword ? : } + + + ), + }} + /> + + {error && ( + + + {error} + + )} + + {/* This is where the password reset link would go */} + {config.enable_signup && ( +
+ + {t("login_link_signup")} + +
+ )} +
+
+
+ ); +}; + +export default Login; diff --git a/web/src/components/Messaging.js b/web/src/components/Messaging.js deleted file mode 100644 index b1f11a96..00000000 --- a/web/src/components/Messaging.js +++ /dev/null @@ -1,114 +0,0 @@ -import * as React from 'react'; -import {useState} from 'react'; -import Navigation from "./Navigation"; -import Paper from "@mui/material/Paper"; -import IconButton from "@mui/material/IconButton"; -import TextField from "@mui/material/TextField"; -import SendIcon from "@mui/icons-material/Send"; -import api from "../app/Api"; -import PublishDialog from "./PublishDialog"; -import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; -import {Portal, Snackbar} from "@mui/material"; -import {useTranslation} from "react-i18next"; - -const Messaging = (props) => { - const [message, setMessage] = useState(""); - const [dialogKey, setDialogKey] = useState(0); - - const dialogOpenMode = props.dialogOpenMode; - const subscription = props.selected; - - const handleOpenDialogClick = () => { - props.onDialogOpenModeChange(PublishDialog.OPEN_MODE_DEFAULT); - }; - - const handleDialogClose = () => { - props.onDialogOpenModeChange(""); - setDialogKey(prev => prev+1); - }; - - return ( - <> - {subscription && } - props.onDialogOpenModeChange(prev => (prev) ? prev : PublishDialog.OPEN_MODE_DRAG)} // Only update if not already open - onResetOpenMode={() => props.onDialogOpenModeChange(PublishDialog.OPEN_MODE_DEFAULT)} - /> - - ); -} - -const MessageBar = (props) => { - const { t } = useTranslation(); - const subscription = props.subscription; - const [snackOpen, setSnackOpen] = useState(false); - const handleSendClick = async () => { - try { - await api.publish(subscription.baseUrl, subscription.topic, props.message); - } catch (e) { - console.log(`[MessageBar] Error publishing message`, e); - setSnackOpen(true); - } - props.onMessageChange(""); - }; - return ( - theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[900] - }} - > - - - - props.onMessageChange(ev.target.value)} - onKeyPress={(ev) => { - if (ev.key === 'Enter') { - ev.preventDefault(); - handleSendClick(); - } - }} - /> - - - - - setSnackOpen(false)} - message={t("message_bar_error_publishing")} - /> - - - ); -}; - -export default Messaging; diff --git a/web/src/components/Messaging.jsx b/web/src/components/Messaging.jsx new file mode 100644 index 00000000..27e08dc9 --- /dev/null +++ b/web/src/components/Messaging.jsx @@ -0,0 +1,108 @@ +import * as React from "react"; +import { useState } from "react"; +import { Paper, IconButton, TextField, Portal, Snackbar } from "@mui/material"; +import SendIcon from "@mui/icons-material/Send"; +import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; +import { useTranslation } from "react-i18next"; +import PublishDialog from "./PublishDialog"; +import api from "../app/Api"; +import Navigation from "./Navigation"; + +const Messaging = (props) => { + const [message, setMessage] = useState(""); + const [dialogKey, setDialogKey] = useState(0); + + const { dialogOpenMode } = props; + const subscription = props.selected; + + const handleOpenDialogClick = () => { + props.onDialogOpenModeChange(PublishDialog.OPEN_MODE_DEFAULT); + }; + + const handleDialogClose = () => { + props.onDialogOpenModeChange(""); + setDialogKey((prev) => prev + 1); + }; + + return ( + <> + {subscription && ( + + )} + props.onDialogOpenModeChange((prev) => prev || PublishDialog.OPEN_MODE_DRAG)} // Only update if not already open + onResetOpenMode={() => props.onDialogOpenModeChange(PublishDialog.OPEN_MODE_DEFAULT)} + /> + + ); +}; + +const MessageBar = (props) => { + const { t } = useTranslation(); + const { subscription } = props; + const [snackOpen, setSnackOpen] = useState(false); + const handleSendClick = async () => { + try { + await api.publish(subscription.baseUrl, subscription.topic, props.message); + } catch (e) { + console.log(`[MessageBar] Error publishing message`, e); + setSnackOpen(true); + } + props.onMessageChange(""); + }; + return ( + (theme.palette.mode === "light" ? theme.palette.grey[100] : theme.palette.grey[900]), + }} + > + + + + props.onMessageChange(ev.target.value)} + onKeyPress={(ev) => { + if (ev.key === "Enter") { + ev.preventDefault(); + handleSendClick(); + } + }} + /> + + + + + setSnackOpen(false)} + message={t("message_bar_error_publishing")} + /> + + + ); +}; + +export default Messaging; diff --git a/web/src/components/Navigation.js b/web/src/components/Navigation.js deleted file mode 100644 index a7d0da0e..00000000 --- a/web/src/components/Navigation.js +++ /dev/null @@ -1,371 +0,0 @@ -import Drawer from "@mui/material/Drawer"; -import * as React from "react"; -import {useContext, useState} from "react"; -import ListItemButton from "@mui/material/ListItemButton"; -import ListItemIcon from "@mui/material/ListItemIcon"; -import ChatBubbleOutlineIcon from "@mui/icons-material/ChatBubbleOutline"; -import Person from "@mui/icons-material/Person"; -import ListItemText from "@mui/material/ListItemText"; -import Toolbar from "@mui/material/Toolbar"; -import Divider from "@mui/material/Divider"; -import List from "@mui/material/List"; -import SettingsIcon from "@mui/icons-material/Settings"; -import AddIcon from "@mui/icons-material/Add"; -import SubscribeDialog from "./SubscribeDialog"; -import {Alert, AlertTitle, Badge, CircularProgress, Link, ListSubheader, Portal, Tooltip} from "@mui/material"; -import Button from "@mui/material/Button"; -import Typography from "@mui/material/Typography"; -import {openUrl, topicDisplayName, topicUrl} from "../app/utils"; -import routes from "./routes"; -import {ConnectionState} from "../app/Connection"; -import {useLocation, useNavigate} from "react-router-dom"; -import subscriptionManager from "../app/SubscriptionManager"; -import {ChatBubble, MoreVert, NotificationsOffOutlined, Send} from "@mui/icons-material"; -import Box from "@mui/material/Box"; -import notifier from "../app/Notifier"; -import config from "../app/config"; -import ArticleIcon from '@mui/icons-material/Article'; -import {Trans, useTranslation} from "react-i18next"; -import session from "../app/Session"; -import accountApi, {Permission, Role} from "../app/AccountApi"; -import CelebrationIcon from '@mui/icons-material/Celebration'; -import UpgradeDialog from "./UpgradeDialog"; -import {AccountContext} from "./App"; -import {PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite} from "./ReserveIcons"; -import IconButton from "@mui/material/IconButton"; -import { SubscriptionPopup } from "./SubscriptionPopup"; - -const navWidth = 280; - -const Navigation = (props) => { - const navigationList = ; - return ( - - {/* Mobile drawer; only shown if menu icon clicked (mobile open) and display is small */} - - {navigationList} - - {/* Big screen drawer; persistent, shown if screen is big */} - - {navigationList} - - - ); -}; -Navigation.width = navWidth; - -const NavList = (props) => { - const { t } = useTranslation(); - const navigate = useNavigate(); - const location = useLocation(); - const { account } = useContext(AccountContext); - const [subscribeDialogKey, setSubscribeDialogKey] = useState(0); - const [subscribeDialogOpen, setSubscribeDialogOpen] = useState(false); - - const handleSubscribeReset = () => { - setSubscribeDialogOpen(false); - setSubscribeDialogKey(prev => prev+1); - } - - const handleSubscribeSubmit = (subscription) => { - console.log(`[Navigation] New subscription: ${subscription.id}`, subscription); - handleSubscribeReset(); - navigate(routes.forSubscription(subscription)); - handleRequestNotificationPermission(); - } - - const handleRequestNotificationPermission = () => { - notifier.maybeRequestPermission(granted => props.onNotificationGranted(granted)) - }; - - const handleAccountClick = () => { - accountApi.sync(); // Dangle! - navigate(routes.account); - }; - - const isAdmin = account?.role === Role.ADMIN; - const isPaid = account?.billing?.subscription; - const showUpgradeBanner = config.enable_payments && !isAdmin && !isPaid; - const showSubscriptionsList = props.subscriptions?.length > 0; - const showNotificationBrowserNotSupportedBox = !notifier.browserSupported(); - const showNotificationContextNotSupportedBox = notifier.browserSupported() && !notifier.contextSupported(); // Only show if notifications are generally supported in the browser - const showNotificationGrantBox = notifier.supported() && props.subscriptions?.length > 0 && !props.notificationsGranted; - const navListPadding = (showNotificationGrantBox || showNotificationBrowserNotSupportedBox || showNotificationContextNotSupportedBox) ? '0' : ''; - - return ( - <> - - - {showNotificationBrowserNotSupportedBox && } - {showNotificationContextNotSupportedBox && } - {showNotificationGrantBox && } - {!showSubscriptionsList && - navigate(routes.app)} selected={location.pathname === config.app_root}> - - - } - {showSubscriptionsList && - <> - {t("nav_topics_title")} - navigate(routes.app)} selected={location.pathname === config.app_root}> - - - - - - } - {session.exists() && - - - - - } - navigate(routes.settings)} selected={location.pathname === routes.settings}> - - - - openUrl("/docs")}> - - - - props.onPublishMessageClick()}> - - - - setSubscribeDialogOpen(true)}> - - - - {showUpgradeBanner && - - } - - - - ); -}; - -const UpgradeBanner = () => { - const { t } = useTranslation(); - const [dialogKey, setDialogKey] = useState(0); - const [dialogOpen, setDialogOpen] = useState(false); - - const handleClick = () => { - setDialogKey(k => k + 1); - setDialogOpen(true); - }; - - return ( - - - - - - - setDialogOpen(false)} - /> - - ); -}; - -const SubscriptionList = (props) => { - const sortedSubscriptions = props.subscriptions - .filter(s => !s.internal) - .sort((a, b) => { - return (topicUrl(a.baseUrl, a.topic) < topicUrl(b.baseUrl, b.topic)) ? -1 : 1; - }); - return ( - <> - {sortedSubscriptions.map(subscription => - )} - - ); -} - -const SubscriptionItem = (props) => { - const { t } = useTranslation(); - const navigate = useNavigate(); - const [menuAnchorEl, setMenuAnchorEl] = useState(null); - - const subscription = props.subscription; - const iconBadge = (subscription.new <= 99) ? subscription.new : "99+"; - const displayName = topicDisplayName(subscription); - const ariaLabel = (subscription.state === ConnectionState.Connecting) - ? `${displayName} (${t("nav_button_connecting")})` - : displayName; - const icon = (subscription.state === ConnectionState.Connecting) - ? - : ; - - const handleClick = async () => { - navigate(routes.forSubscription(subscription)); - await subscriptionManager.markNotificationsRead(subscription.id); - }; - - return ( - <> - - {icon} - - {subscription.reservation?.everyone && - - {subscription.reservation?.everyone === Permission.READ_WRITE && - - } - {subscription.reservation?.everyone === Permission.READ_ONLY && - - } - {subscription.reservation?.everyone === Permission.WRITE_ONLY && - - } - {subscription.reservation?.everyone === Permission.DENY_ALL && - - } - - } - {subscription.mutedUntil > 0 && - - - - } - - e.stopPropagation()} - onClick={(e) => { - e.stopPropagation(); - setMenuAnchorEl(e.currentTarget); - }} - > - - - - - - setMenuAnchorEl(null)} - /> - - - ); -}; - -const NotificationGrantAlert = (props) => { - const { t } = useTranslation(); - return ( - <> - - {t("alert_grant_title")} - {t("alert_grant_description")} - - - - - ); -}; - -const NotificationBrowserNotSupportedAlert = () => { - const { t } = useTranslation(); - return ( - <> - - {t("alert_not_supported_title")} - {t("alert_not_supported_description")} - - - - ); -}; - -const NotificationContextNotSupportedAlert = () => { - const { t } = useTranslation(); - return ( - <> - - {t("alert_not_supported_title")} - - - }} - /> - - - - - ); -}; - -export default Navigation; diff --git a/web/src/components/Navigation.jsx b/web/src/components/Navigation.jsx new file mode 100644 index 00000000..7e30931a --- /dev/null +++ b/web/src/components/Navigation.jsx @@ -0,0 +1,428 @@ +import { + Drawer, + ListItemButton, + ListItemIcon, + ListItemText, + Toolbar, + Divider, + List, + Alert, + AlertTitle, + Badge, + CircularProgress, + Link, + ListSubheader, + Portal, + Tooltip, + Typography, + Box, + IconButton, + Button, + useTheme, +} from "@mui/material"; +import * as React from "react"; +import { useContext, useState } from "react"; +import ChatBubbleOutlineIcon from "@mui/icons-material/ChatBubbleOutline"; +import Person from "@mui/icons-material/Person"; +import SettingsIcon from "@mui/icons-material/Settings"; +import AddIcon from "@mui/icons-material/Add"; +import { useLocation, useNavigate } from "react-router-dom"; +import { ChatBubble, MoreVert, NotificationsOffOutlined, Send } from "@mui/icons-material"; +import ArticleIcon from "@mui/icons-material/Article"; +import { Trans, useTranslation } from "react-i18next"; +import CelebrationIcon from "@mui/icons-material/Celebration"; +import SubscribeDialog from "./SubscribeDialog"; +import { openUrl, topicDisplayName, topicUrl } from "../app/utils"; +import routes from "./routes"; +import { ConnectionState } from "../app/Connection"; +import subscriptionManager from "../app/SubscriptionManager"; +import notifier from "../app/Notifier"; +import config from "../app/config"; +import session from "../app/Session"; +import accountApi, { Permission, Role } from "../app/AccountApi"; +import UpgradeDialog from "./UpgradeDialog"; +import { AccountContext } from "./App"; +import { PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite } from "./ReserveIcons"; +import { SubscriptionPopup } from "./SubscriptionPopup"; +import { useNotificationPermissionListener } from "./hooks"; + +const navWidth = 280; + +const Navigation = (props) => { + const navigationList = ; + return ( + + {/* Mobile drawer; only shown if menu icon clicked (mobile open) and display is small */} + + {navigationList} + + {/* Big screen drawer; persistent, shown if screen is big */} + + {navigationList} + + + ); +}; +Navigation.width = navWidth; + +const NavList = (props) => { + const theme = useTheme(); + const { t } = useTranslation(); + const navigate = useNavigate(); + const location = useLocation(); + const { account } = useContext(AccountContext); + const [subscribeDialogKey, setSubscribeDialogKey] = useState(0); + const [subscribeDialogOpen, setSubscribeDialogOpen] = useState(false); + + const handleSubscribeReset = () => { + setSubscribeDialogOpen(false); + setSubscribeDialogKey((prev) => prev + 1); + }; + + const handleSubscribeSubmit = (subscription) => { + console.log(`[Navigation] New subscription: ${subscription.id}`, subscription); + handleSubscribeReset(); + navigate(routes.forSubscription(subscription)); + }; + + const handleAccountClick = () => { + accountApi.sync(); // Dangle! + navigate(routes.account); + }; + + const isAdmin = account?.role === Role.ADMIN; + const isPaid = account?.billing?.subscription; + const showUpgradeBanner = config.enable_payments && !isAdmin && !isPaid; + const showSubscriptionsList = props.subscriptions?.length > 0; + const showNotificationPermissionRequired = useNotificationPermissionListener(() => notifier.notRequested()); + const showNotificationPermissionDenied = useNotificationPermissionListener(() => notifier.denied()); + const showNotificationIOSInstallRequired = notifier.iosSupportedButInstallRequired(); + const showNotificationBrowserNotSupportedBox = !showNotificationIOSInstallRequired && !notifier.browserSupported(); + const showNotificationContextNotSupportedBox = notifier.browserSupported() && !notifier.contextSupported(); // Only show if notifications are generally supported in the browser + + const alertVisible = + showNotificationPermissionRequired || + showNotificationPermissionDenied || + showNotificationIOSInstallRequired || + showNotificationBrowserNotSupportedBox || + showNotificationContextNotSupportedBox; + + return ( + <> + + + {showNotificationPermissionRequired && } + {showNotificationPermissionDenied && } + {showNotificationBrowserNotSupportedBox && } + {showNotificationContextNotSupportedBox && } + {showNotificationIOSInstallRequired && } + {alertVisible && } + {!showSubscriptionsList && ( + navigate(routes.app)} selected={location.pathname === config.app_root}> + + + + + + )} + {showSubscriptionsList && ( + <> + {t("nav_topics_title")} + navigate(routes.app)} selected={location.pathname === config.app_root}> + + + + + + + + + )} + {session.exists() && ( + + + + + + + )} + navigate(routes.settings)} selected={location.pathname === routes.settings}> + + + + + + openUrl("/docs")}> + + + + + + props.onPublishMessageClick()}> + + + + + + setSubscribeDialogOpen(true)}> + + + + + + {showUpgradeBanner && ( + // The text background gradient didn't seem to do well with switching between light/dark mode, + // So adding a `key` forces React to replace the entire component when the theme changes + + )} + + + + ); +}; + +const UpgradeBanner = ({ mode }) => { + const { t } = useTranslation(); + const [dialogKey, setDialogKey] = useState(0); + const [dialogOpen, setDialogOpen] = useState(false); + + const handleClick = () => { + setDialogKey((k) => k + 1); + setDialogOpen(true); + }; + + return ( + + + + + + + + + setDialogOpen(false)} /> + + ); +}; + +const SubscriptionList = (props) => { + const sortedSubscriptions = props.subscriptions + .filter((s) => !s.internal) + .sort((a, b) => (topicUrl(a.baseUrl, a.topic) < topicUrl(b.baseUrl, b.topic) ? -1 : 1)); + return ( + <> + {sortedSubscriptions.map((subscription) => ( + + ))} + + ); +}; + +const SubscriptionItem = (props) => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const [menuAnchorEl, setMenuAnchorEl] = useState(null); + + const { subscription } = props; + const iconBadge = subscription.new <= 99 ? subscription.new : "99+"; + const displayName = topicDisplayName(subscription); + const ariaLabel = subscription.state === ConnectionState.Connecting ? `${displayName} (${t("nav_button_connecting")})` : displayName; + const icon = + subscription.state === ConnectionState.Connecting ? ( + + ) : ( + + + + ); + + const handleClick = async () => { + navigate(routes.forSubscription(subscription)); + await subscriptionManager.markNotificationsRead(subscription.id); + }; + + return ( + <> + + {icon} + + {subscription.reservation?.everyone && ( + + {subscription.reservation?.everyone === Permission.READ_WRITE && ( + + + + )} + {subscription.reservation?.everyone === Permission.READ_ONLY && ( + + + + )} + {subscription.reservation?.everyone === Permission.WRITE_ONLY && ( + + + + )} + {subscription.reservation?.everyone === Permission.DENY_ALL && ( + + + + )} + + )} + {subscription.mutedUntil > 0 && ( + + + + + + )} + + e.stopPropagation()} + onClick={(e) => { + e.stopPropagation(); + setMenuAnchorEl(e.currentTarget); + }} + > + + + + + + setMenuAnchorEl(null)} /> + + + ); +}; + +const NotificationPermissionRequired = () => { + const { t } = useTranslation(); + const requestPermission = async () => { + await notifier.maybeRequestPermission(); + }; + return ( + + {t("alert_notification_permission_required_title")} + {t("alert_notification_permission_required_description")} + + + ); +}; + +const NotificationPermissionDeniedAlert = () => { + const { t } = useTranslation(); + return ( + + {t("alert_notification_permission_denied_title")} + {t("alert_notification_permission_denied_description")} + + ); +}; + +const NotificationIOSInstallRequiredAlert = () => { + const { t } = useTranslation(); + return ( + + {t("alert_notification_ios_install_required_title")} + {t("alert_notification_ios_install_required_description")} + + ); +}; + +const NotificationBrowserNotSupportedAlert = () => { + const { t } = useTranslation(); + return ( + + {t("alert_not_supported_title")} + {t("alert_not_supported_description")} + + ); +}; + +const NotificationContextNotSupportedAlert = () => { + const { t } = useTranslation(); + return ( + + {t("alert_not_supported_title")} + + , + }} + /> + + + ); +}; + +export default Navigation; diff --git a/web/src/components/Notifications.js b/web/src/components/Notifications.js deleted file mode 100644 index 10bcad81..00000000 --- a/web/src/components/Notifications.js +++ /dev/null @@ -1,548 +0,0 @@ -import Container from "@mui/material/Container"; -import { - ButtonBase, - CardActions, - CardContent, - CircularProgress, - Fade, - Link, - Modal, - Snackbar, - Stack, - Tooltip -} from "@mui/material"; -import Card from "@mui/material/Card"; -import Typography from "@mui/material/Typography"; -import * as React from "react"; -import {useEffect, useState} from "react"; -import { - formatBytes, - formatMessage, - formatShortDateTime, - formatTitle, - maybeAppendActionErrors, - openUrl, - shortUrl, - topicShortUrl, - unmatchedTags -} from "../app/utils"; -import IconButton from "@mui/material/IconButton"; -import CheckIcon from '@mui/icons-material/Check'; -import CloseIcon from '@mui/icons-material/Close'; -import {LightboxBackdrop, Paragraph, VerticallyCenteredContainer} from "./styles"; -import {useLiveQuery} from "dexie-react-hooks"; -import Box from "@mui/material/Box"; -import Button from "@mui/material/Button"; -import subscriptionManager from "../app/SubscriptionManager"; -import InfiniteScroll from "react-infinite-scroll-component"; -import priority1 from "../img/priority-1.svg"; -import priority2 from "../img/priority-2.svg"; -import priority4 from "../img/priority-4.svg"; -import priority5 from "../img/priority-5.svg"; -import logoOutline from "../img/ntfy-outline.svg"; -import AttachmentIcon from "./AttachmentIcon"; -import {Trans, useTranslation} from "react-i18next"; -import {useOutletContext} from "react-router-dom"; -import {useAutoSubscribe} from "./hooks"; - -export const AllSubscriptions = () => { - const { subscriptions } = useOutletContext(); - if (!subscriptions) { - return ; - } - return ; -}; - -export const SingleSubscription = () => { - const { subscriptions, selected } = useOutletContext(); - useAutoSubscribe(subscriptions, selected); - if (!selected) { - return ; - } - return ; -}; - -const AllSubscriptionsList = (props) => { - const subscriptions = props.subscriptions; - const notifications = useLiveQuery(() => subscriptionManager.getAllNotifications(), []); - if (notifications === null || notifications === undefined) { - return ; - } else if (subscriptions.length === 0) { - return ; - } else if (notifications.length === 0) { - return ; - } - return ; -} - -const SingleSubscriptionList = (props) => { - const subscription = props.subscription; - const notifications = useLiveQuery(() => subscriptionManager.getNotifications(subscription.id), [subscription]); - if (notifications === null || notifications === undefined) { - return ; - } else if (notifications.length === 0) { - return ; - } - return ; -} - -const NotificationList = (props) => { - const { t } = useTranslation(); - const pageSize = 20; - const notifications = props.notifications; - const [snackOpen, setSnackOpen] = useState(false); - const [maxCount, setMaxCount] = useState(pageSize); - const count = Math.min(notifications.length, maxCount); - - useEffect(() => { - return () => { - setMaxCount(pageSize); - const main = document.getElementById("main"); - if (main) { - main.scrollTo(0, 0); - } - } - }, [props.id]); - - return ( - setMaxCount(prev => prev + pageSize)} - hasMore={count < notifications.length} - loader={<>Loading ...} - scrollThreshold={0.7} - scrollableTarget="main" - > - - - {notifications.slice(0, count).map(notification => - setSnackOpen(true)} - />)} - setSnackOpen(false)} - message={t("notifications_copied_to_clipboard")} - /> - - - - ); -} - -const NotificationItem = (props) => { - const { t } = useTranslation(); - const notification = props.notification; - const attachment = notification.attachment; - const date = formatShortDateTime(notification.time); - const otherTags = unmatchedTags(notification.tags); - const tags = (otherTags.length > 0) ? otherTags.join(', ') : null; - const handleDelete = async () => { - console.log(`[Notifications] Deleting notification ${notification.id}`); - await subscriptionManager.deleteNotification(notification.id) - } - const handleMarkRead = async () => { - console.log(`[Notifications] Marking notification ${notification.id} as read`); - await subscriptionManager.markNotificationRead(notification.id) - } - const handleCopy = (s) => { - navigator.clipboard.writeText(s); - props.onShowSnack(); - }; - const expired = attachment && attachment.expires && attachment.expires < Date.now()/1000; - const hasAttachmentActions = attachment && !expired; - const hasClickAction = notification.click; - const hasUserActions = notification.actions && notification.actions.length > 0; - const showActions = hasAttachmentActions || hasClickAction || hasUserActions; - return ( - - - - - - - - {notification.new === 1 && - - - - - } - - {date} - {[1,2,4,5].includes(notification.priority) && - {t("notifications_priority_x",} - {notification.new === 1 && - - - } - - {notification.title && {formatTitle(notification)}} - - {autolink(maybeAppendActionErrors(formatMessage(notification), notification))} - - {attachment && } - {tags && {t("notifications_tags")}: {tags}} - - {showActions && - - {hasAttachmentActions && <> - - - - - - - } - {hasClickAction && <> - - - - - - - } - {hasUserActions && } - } - - ); -} - -/** - * Replace links with components; this is a combination of the genius function - * in [1] and the regex in [2]. - * - * [1] https://github.com/facebook/react/issues/3386#issuecomment-78605760 - * [2] https://github.com/bryanwoods/autolink-js/blob/master/autolink.js#L9 - */ -const autolink = (s) => { - const parts = s.split(/(\bhttps?:\/\/[\-A-Z0-9+\u0026\u2019@#\/%?=()~_|!:,.;]*[\-A-Z0-9+\u0026@#\/%=~()_|]\b)/gi); - for (let i = 1; i < parts.length; i += 2) { - parts[i] = {shortUrl(parts[i])}; - } - return <>{parts}; -}; - -const priorityFiles = { - 1: priority1, - 2: priority2, - 4: priority4, - 5: priority5 -}; - -const Attachment = (props) => { - const { t } = useTranslation(); - const attachment = props.attachment; - const expired = attachment.expires && attachment.expires < Date.now()/1000; - const expires = attachment.expires && attachment.expires > Date.now()/1000; - const displayableImage = !expired && attachment.type && attachment.type.startsWith("image/"); - - // Unexpired image - if (displayableImage) { - return ; - } - - // Anything else: Show box - const infos = []; - if (attachment.size) { - infos.push(formatBytes(attachment.size)); - } - if (expires) { - infos.push(t("notifications_attachment_link_expires", { date: formatShortDateTime(attachment.expires) })); - } - if (expired) { - infos.push(t("notifications_attachment_link_expired")); - } - const maybeInfoText = (infos.length > 0) ? <>
{infos.join(", ")} : null; - - // If expired, just show infos without click target - if (expired) { - return ( - - - - {attachment.name} - {maybeInfoText} - - - ); - } - - // Not expired - return ( - - - - - {attachment.name} - {maybeInfoText} - - - - ); -}; - -const Image = (props) => { - const { t } = useTranslation(); - const [open, setOpen] = useState(false); - return ( - <> - setOpen(true)} - sx={{ - marginTop: 2, - borderRadius: '4px', - boxShadow: 2, - width: 1, - maxHeight: '400px', - objectFit: 'cover', - cursor: 'pointer' - }} - /> - setOpen(false)} - BackdropComponent={LightboxBackdrop} - > - - - - - - ); -} - -const UserActions = (props) => { - return ( - <>{props.notification.actions.map(action => - )} - ); -}; - -const UserAction = (props) => { - const { t } = useTranslation(); - const notification = props.notification; - const action = props.action; - if (action.action === "broadcast") { - return ( - - - - ); - } else if (action.action === "view") { - return ( - - - - ); - } else if (action.action === "http") { - const method = action.method ?? "POST"; - const label = action.label + (ACTION_LABEL_SUFFIX[action.progress ?? 0] ?? ""); - return ( - - - - ); - } - return null; // Others -}; - -const performHttpAction = async (notification, action) => { - console.log(`[Notifications] Performing HTTP user action`, action); - try { - updateActionStatus(notification, action, ACTION_PROGRESS_ONGOING, null); - const response = await fetch(action.url, { - method: action.method ?? "POST", - headers: action.headers ?? {}, - // This must not null-coalesce to a non nullish value. Otherwise, the fetch API - // will reject it for "having a body" - body: action.body - }); - console.log(`[Notifications] HTTP user action response`, response); - const success = response.status >= 200 && response.status <= 299; - if (success) { - updateActionStatus(notification, action, ACTION_PROGRESS_SUCCESS, null); - } else { - updateActionStatus(notification, action, ACTION_PROGRESS_FAILED, `${action.label}: Unexpected response HTTP ${response.status}`); - } - } catch (e) { - console.log(`[Notifications] HTTP action failed`, e); - updateActionStatus(notification, action, ACTION_PROGRESS_FAILED, `${action.label}: ${e} Check developer console for details.`); - } -}; - -const updateActionStatus = (notification, action, progress, error) => { - notification.actions = notification.actions.map(a => { - if (a.id !== action.id) { - return a; - } - return { ...a, progress: progress, error: error }; - }); - subscriptionManager.updateNotification(notification); -} - -const ACTION_PROGRESS_ONGOING = 1; -const ACTION_PROGRESS_SUCCESS = 2; -const ACTION_PROGRESS_FAILED = 3; - -const ACTION_LABEL_SUFFIX = { - [ACTION_PROGRESS_ONGOING]: " …", - [ACTION_PROGRESS_SUCCESS]: " ✔", - [ACTION_PROGRESS_FAILED]: " ❌" -}; - -const NoNotifications = (props) => { - const { t } = useTranslation(); - const shortUrl = topicShortUrl(props.subscription.baseUrl, props.subscription.topic); - return ( - - - {t("action_bar_logo_alt")}/
- {t("notifications_none_for_topic_title")} -
- - {t("notifications_none_for_topic_description")} - - - {t("notifications_example")}:
- - $ curl -d "Hi" {shortUrl} - -
- - - -
- ); -}; - -const NoNotificationsWithoutSubscription = (props) => { - const { t } = useTranslation(); - const subscription = props.subscriptions[0]; - const shortUrl = topicShortUrl(subscription.baseUrl, subscription.topic); - return ( - - - {t("action_bar_logo_alt")}/
- {t("notifications_none_for_any_title")} -
- - {t("notifications_none_for_any_description")} - - - {t("notifications_example")}:
- - $ curl -d "Hi" {shortUrl} - -
- - - -
- ); -}; - -const NoSubscriptions = () => { - const { t } = useTranslation(); - return ( - - - {t("action_bar_logo_alt")}/
- {t("notifications_no_subscriptions_title")} -
- - {t("notifications_no_subscriptions_description", { - linktext: t("nav_button_subscribe") - })} - - - - -
- ); -}; - -const ForMoreDetails = () => { - return ( - , - docsLink: - }} - /> - ); -}; - -const Loading = () => { - const { t } = useTranslation(); - return ( - - -
- {t("notifications_loading")} -
-
- ); -}; diff --git a/web/src/components/Notifications.jsx b/web/src/components/Notifications.jsx new file mode 100644 index 00000000..0b8b2e7d --- /dev/null +++ b/web/src/components/Notifications.jsx @@ -0,0 +1,672 @@ +import { + Container, + ButtonBase, + CardActions, + CardContent, + CircularProgress, + Fade, + Link, + Modal, + Snackbar, + Stack, + Tooltip, + Card, + Typography, + IconButton, + Box, + Button, +} from "@mui/material"; +import * as React from "react"; +import { useEffect, useState } from "react"; +import CheckIcon from "@mui/icons-material/Check"; +import CloseIcon from "@mui/icons-material/Close"; +import { useLiveQuery } from "dexie-react-hooks"; +import InfiniteScroll from "react-infinite-scroll-component"; +import { Trans, useTranslation } from "react-i18next"; +import { useOutletContext } from "react-router-dom"; +import { useRemark } from "react-remark"; +import styled from "@emotion/styled"; +import { formatBytes, formatShortDateTime, maybeActionErrors, openUrl, shortUrl, topicShortUrl, unmatchedTags } from "../app/utils"; +import { formatMessage, formatTitle, isImage } from "../app/notificationUtils"; +import { LightboxBackdrop, Paragraph, VerticallyCenteredContainer } from "./styles"; +import subscriptionManager from "../app/SubscriptionManager"; +import priority1 from "../img/priority-1.svg"; +import priority2 from "../img/priority-2.svg"; +import priority4 from "../img/priority-4.svg"; +import priority5 from "../img/priority-5.svg"; +import logoOutline from "../img/ntfy-outline.svg"; +import AttachmentIcon from "./AttachmentIcon"; +import { useAutoSubscribe } from "./hooks"; + +const priorityFiles = { + 1: priority1, + 2: priority2, + 4: priority4, + 5: priority5, +}; + +export const AllSubscriptions = () => { + const { subscriptions } = useOutletContext(); + if (!subscriptions) { + return ; + } + return ; +}; + +export const SingleSubscription = () => { + const { subscriptions, selected } = useOutletContext(); + useAutoSubscribe(subscriptions, selected); + if (!selected) { + return ; + } + return ; +}; + +const AllSubscriptionsList = (props) => { + const { subscriptions } = props; + const notifications = useLiveQuery(() => subscriptionManager.getAllNotifications(), []); + if (notifications === null || notifications === undefined) { + return ; + } + if (subscriptions.length === 0) { + return ; + } + if (notifications.length === 0) { + return ; + } + return ; +}; + +const SingleSubscriptionList = (props) => { + const { subscription } = props; + const notifications = useLiveQuery(() => subscriptionManager.getNotifications(subscription.id), [subscription]); + if (notifications === null || notifications === undefined) { + return ; + } + if (notifications.length === 0) { + return ; + } + return ; +}; + +const NotificationList = (props) => { + const { t } = useTranslation(); + const pageSize = 20; + const { notifications } = props; + const [snackOpen, setSnackOpen] = useState(false); + const [maxCount, setMaxCount] = useState(pageSize); + const count = Math.min(notifications.length, maxCount); + + useEffect( + () => () => { + setMaxCount(pageSize); + const main = document.getElementById("main"); + if (main) { + main.scrollTo(0, 0); + } + }, + [props.id] + ); + + return ( + setMaxCount((prev) => prev + pageSize)} + hasMore={count < notifications.length} + loader={<>Loading ...} + scrollThreshold={0.7} + scrollableTarget="main" + > + + + {notifications.slice(0, count).map((notification) => ( + setSnackOpen(true)} /> + ))} + setSnackOpen(false)} + message={t("notifications_copied_to_clipboard")} + /> + + + + ); +}; + +/** + * Replace links with components; this is a combination of the genius function + * in [1] and the regex in [2]. + * + * [1] https://github.com/facebook/react/issues/3386#issuecomment-78605760 + * [2] https://github.com/bryanwoods/autolink-js/blob/master/autolink.js#L9 + */ +const autolink = (s) => { + const parts = s.split(/(\bhttps?:\/\/[-A-Z0-9+\u0026\u2019@#/%?=()~_|!:,.;]*[-A-Z0-9+\u0026@#/%=~()_|]\b)/gi); + for (let i = 1; i < parts.length; i += 2) { + parts[i] = ( + + {shortUrl(parts[i])} + + ); + } + return <>{parts}; +}; + +const MarkdownContainer = styled("div")` + line-height: 1; + + h1, + h2, + h3, + h4, + h5, + h6, + p, + pre, + ul, + ol, + blockquote { + margin: 0; + } + + p { + line-height: 1.2; + } + + blockquote, + pre { + border-radius: 3px; + background: ${(props) => (props.theme.palette.mode === "light" ? "#f5f5f5" : "#333")}; + } + + pre { + padding: 0.9rem; + } + + ul, + ol, + blockquote { + padding-inline: 1rem; + } + + img { + max-width: 100%; + } +`; + +const MarkdownContent = ({ content }) => { + const [reactContent, setMarkdownSource] = useRemark(); + + useEffect(() => { + setMarkdownSource(content); + }, [content]); + + return {reactContent}; +}; + +const NotificationBody = ({ notification }) => { + const displayAsMarkdown = notification.content_type === "text/markdown"; + const formatted = formatMessage(notification); + if (displayAsMarkdown) { + return ; + } + return autolink(formatted); +}; + +const NotificationItem = (props) => { + const { t, i18n } = useTranslation(); + const { notification } = props; + const { attachment } = notification; + const date = formatShortDateTime(notification.time, i18n.language); + const otherTags = unmatchedTags(notification.tags); + const tags = otherTags.length > 0 ? otherTags.join(", ") : null; + const handleDelete = async () => { + console.log(`[Notifications] Deleting notification ${notification.id}`); + await subscriptionManager.deleteNotification(notification.id); + }; + const handleMarkRead = async () => { + console.log(`[Notifications] Marking notification ${notification.id} as read`); + await subscriptionManager.markNotificationRead(notification.id); + }; + const handleCopy = (s) => { + navigator.clipboard.writeText(s); + props.onShowSnack(); + }; + const expired = attachment && attachment.expires && attachment.expires < Date.now() / 1000; + const hasAttachmentActions = attachment && !expired; + const hasClickAction = notification.click; + const hasUserActions = notification.actions && notification.actions.length > 0; + const showActions = hasAttachmentActions || hasClickAction || hasUserActions; + + return ( + + + + + + + + {notification.new === 1 && ( + + + + + + )} + + {date} + {[1, 2, 4, 5].includes(notification.priority) && ( + {t("notifications_priority_x", + )} + {notification.new === 1 && ( + + + + )} + + {notification.title && ( + + {formatTitle(notification)} + + )} + + + {maybeActionErrors(notification)} + + {attachment && } + {tags && ( + + {t("notifications_tags")}: {tags} + + )} + + {showActions && ( + + {hasAttachmentActions && ( + <> + + + + + + + + )} + {hasClickAction && ( + <> + + + + + + + + )} + {hasUserActions && } + + )} + + ); +}; + +const Attachment = (props) => { + const { t, i18n } = useTranslation(); + const { attachment } = props; + const expired = attachment.expires && attachment.expires < Date.now() / 1000; + const expires = attachment.expires && attachment.expires > Date.now() / 1000; + const displayableImage = !expired && isImage(attachment); + + // Unexpired image + if (displayableImage) { + return ; + } + + // Anything else: Show box + const infos = []; + if (attachment.size) { + infos.push(formatBytes(attachment.size)); + } + if (expires) { + infos.push( + t("notifications_attachment_link_expires", { + date: formatShortDateTime(attachment.expires, i18n.language), + }) + ); + } + if (expired) { + infos.push(t("notifications_attachment_link_expired")); + } + const maybeInfoText = + infos.length > 0 ? ( + <> +
+ {infos.join(", ")} + + ) : null; + + // If expired, just show infos without click target + if (expired) { + return ( + + + + {attachment.name} + {maybeInfoText} + + + ); + } + + // Not expired + return ( + + + + + {attachment.name} + {maybeInfoText} + + + + ); +}; + +const Image = (props) => { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + return ( + <> + setOpen(true)} + sx={{ + marginTop: 2, + borderRadius: "4px", + boxShadow: 2, + width: 1, + maxHeight: "400px", + objectFit: "cover", + cursor: "pointer", + }} + /> + setOpen(false)} BackdropComponent={LightboxBackdrop}> + + + + + + ); +}; + +const UserActions = (props) => ( + <> + {props.notification.actions.map((action) => ( + + ))} + +); + +const ACTION_PROGRESS_ONGOING = 1; +const ACTION_PROGRESS_SUCCESS = 2; +const ACTION_PROGRESS_FAILED = 3; + +const ACTION_LABEL_SUFFIX = { + [ACTION_PROGRESS_ONGOING]: " …", + [ACTION_PROGRESS_SUCCESS]: " ✔", + [ACTION_PROGRESS_FAILED]: " ❌", +}; + +const updateActionStatus = (notification, action, progress, error) => { + subscriptionManager.updateNotification({ + ...notification, + actions: notification.actions.map((a) => (a.id === action.id ? { ...a, progress, error } : a)), + }); +}; + +const performHttpAction = async (notification, action) => { + console.log(`[Notifications] Performing HTTP user action`, action); + try { + updateActionStatus(notification, action, ACTION_PROGRESS_ONGOING, null); + const response = await fetch(action.url, { + method: action.method ?? "POST", + headers: action.headers ?? {}, + // This must not null-coalesce to a non nullish value. Otherwise, the fetch API + // will reject it for "having a body" + body: action.body, + }); + console.log(`[Notifications] HTTP user action response`, response); + const success = response.status >= 200 && response.status <= 299; + if (success) { + updateActionStatus(notification, action, ACTION_PROGRESS_SUCCESS, null); + } else { + updateActionStatus(notification, action, ACTION_PROGRESS_FAILED, `${action.label}: Unexpected response HTTP ${response.status}`); + } + } catch (e) { + console.log(`[Notifications] HTTP action failed`, e); + updateActionStatus(notification, action, ACTION_PROGRESS_FAILED, `${action.label}: ${e} Check developer console for details.`); + } +}; + +const UserAction = (props) => { + const { t } = useTranslation(); + const { notification } = props; + const { action } = props; + if (action.action === "broadcast") { + return ( + + + + + + ); + } + if (action.action === "view") { + return ( + + + + ); + } + if (action.action === "http") { + const method = action.method ?? "POST"; + const label = action.label + (ACTION_LABEL_SUFFIX[action.progress ?? 0] ?? ""); + return ( + + + + ); + } + return null; // Others +}; + +const NoNotifications = (props) => { + const { t } = useTranslation(); + const topicShortUrlResolved = topicShortUrl(props.subscription.baseUrl, props.subscription.topic); + return ( + + + {t("action_bar_logo_alt")} +
+ {t("notifications_none_for_topic_title")} +
+ {t("notifications_none_for_topic_description")} + + {t("notifications_example")}:
+ + {'$ curl -d "Hi" '} + {topicShortUrlResolved} + +
+ + + +
+ ); +}; + +const NoNotificationsWithoutSubscription = (props) => { + const { t } = useTranslation(); + const subscription = props.subscriptions[0]; + const topicShortUrlResolved = topicShortUrl(subscription.baseUrl, subscription.topic); + return ( + + + {t("action_bar_logo_alt")} +
+ {t("notifications_none_for_any_title")} +
+ {t("notifications_none_for_any_description")} + + {t("notifications_example")}:
+ + {'$ curl -d "Hi" '} + {topicShortUrlResolved} + +
+ + + +
+ ); +}; + +const NoSubscriptions = () => { + const { t } = useTranslation(); + return ( + + + {t("action_bar_logo_alt")} +
+ {t("notifications_no_subscriptions_title")} +
+ + {t("notifications_no_subscriptions_description", { + linktext: t("nav_button_subscribe"), + })} + + + + +
+ ); +}; + +const ForMoreDetails = () => ( + , + docsLink: , + }} + /> +); + +const Loading = () => { + const { t } = useTranslation(); + return ( + + + +
+ {t("notifications_loading")} +
+
+ ); +}; diff --git a/web/src/components/PopupMenu.js b/web/src/components/PopupMenu.js deleted file mode 100644 index 4d22398b..00000000 --- a/web/src/components/PopupMenu.js +++ /dev/null @@ -1,48 +0,0 @@ -import {Fade, Menu} from "@mui/material"; -import * as React from "react"; - -const PopupMenu = (props) => { - const horizontal = props.horizontal ?? "left"; - const arrow = (horizontal === "right") ? { right: 19 } : { left: 19 }; - return ( - - {props.children} - - ); -}; - -export default PopupMenu; diff --git a/web/src/components/PopupMenu.jsx b/web/src/components/PopupMenu.jsx new file mode 100644 index 00000000..89b20119 --- /dev/null +++ b/web/src/components/PopupMenu.jsx @@ -0,0 +1,48 @@ +import { Fade, Menu } from "@mui/material"; +import * as React from "react"; + +const PopupMenu = (props) => { + const horizontal = props.horizontal ?? "left"; + const arrow = horizontal === "right" ? { right: 19 } : { left: 19 }; + return ( + + {props.children} + + ); +}; + +export default PopupMenu; diff --git a/web/src/components/Pref.js b/web/src/components/Pref.js deleted file mode 100644 index 622d9bbf..00000000 --- a/web/src/components/Pref.js +++ /dev/null @@ -1,51 +0,0 @@ -import * as React from "react"; - -export const PrefGroup = (props) => { - return ( -
- {props.children} -
- ) -}; - -export const Pref = (props) => { - const justifyContent = (props.alignTop) ? "normal" : "center"; - return ( -
-
-
{props.title}{props.subtitle && ({props.subtitle})}
- {props.description &&
{props.description}
} -
-
- {props.children} -
-
- ); -}; diff --git a/web/src/components/Pref.jsx b/web/src/components/Pref.jsx new file mode 100644 index 00000000..4da17381 --- /dev/null +++ b/web/src/components/Pref.jsx @@ -0,0 +1,60 @@ +import { styled } from "@mui/material"; +import * as React from "react"; + +export const PrefGroup = styled("div", { attrs: { role: "table" } })` + display: flex; + flex-direction: column; + gap: 20px; +`; + +const PrefRow = styled("div")` + display: flex; + flex-direction: row; + + > div:first-of-type { + flex: 1 0 40%; + display: flex; + flex-direction: column; + justify-content: ${(props) => (props.alignTop ? "normal" : "center")}; + } + + > div:last-of-type { + flex: 1 0 calc(60% - 50px); + display: flex; + flex-direction: column; + justify-content: ${(props) => (props.alignTop ? "normal" : "center")}; + } + + @media (max-width: 1000px) { + flex-direction: column; + gap: 10px; + + > :div:first-of-type, + > :div:last-of-type { + flex: unset; + } + + > div:last-of-type { + .MuiFormControl-root { + margin: 0; + } + } + } +`; + +export const Pref = (props) => ( + +
+
+ {props.title} + {props.subtitle && ({props.subtitle})} +
+ {props.description && ( +
+ {props.description} +
+ )} +
+
{props.children}
+
+); diff --git a/web/src/components/Preferences.js b/web/src/components/Preferences.js deleted file mode 100644 index 3f6c1b39..00000000 --- a/web/src/components/Preferences.js +++ /dev/null @@ -1,646 +0,0 @@ -import * as React from 'react'; -import {useContext, useEffect, useState} from 'react'; -import { - Alert, - CardActions, - CardContent, - Chip, - FormControl, - Select, - Stack, - Table, - TableBody, - TableCell, - TableHead, - TableRow, - Tooltip, - useMediaQuery -} from "@mui/material"; -import Typography from "@mui/material/Typography"; -import prefs from "../app/Prefs"; -import {Paragraph} from "./styles"; -import EditIcon from '@mui/icons-material/Edit'; -import CloseIcon from "@mui/icons-material/Close"; -import IconButton from "@mui/material/IconButton"; -import PlayArrowIcon from '@mui/icons-material/PlayArrow'; -import Container from "@mui/material/Container"; -import TextField from "@mui/material/TextField"; -import MenuItem from "@mui/material/MenuItem"; -import Card from "@mui/material/Card"; -import Button from "@mui/material/Button"; -import {useLiveQuery} from "dexie-react-hooks"; -import theme from "./theme"; -import Dialog from "@mui/material/Dialog"; -import DialogTitle from "@mui/material/DialogTitle"; -import DialogContent from "@mui/material/DialogContent"; -import DialogActions from "@mui/material/DialogActions"; -import userManager from "../app/UserManager"; -import {playSound, shuffle, sounds, validUrl} from "../app/utils"; -import {useTranslation} from "react-i18next"; -import session from "../app/Session"; -import routes from "./routes"; -import accountApi, {Permission, Role} from "../app/AccountApi"; -import {Pref, PrefGroup} from "./Pref"; -import {Info} from "@mui/icons-material"; -import {AccountContext} from "./App"; -import {useOutletContext} from "react-router-dom"; -import {PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite} from "./ReserveIcons"; -import {ReserveAddDialog, ReserveDeleteDialog, ReserveEditDialog} from "./ReserveDialogs"; -import {UnauthorizedError} from "../app/errors"; -import subscriptionManager from "../app/SubscriptionManager"; -import {subscribeTopic} from "./SubscribeDialog"; - -const Preferences = () => { - return ( - - - - - - - - - ); -}; - -const Notifications = () => { - const { t } = useTranslation(); - return ( - - - {t("prefs_notifications_title")} - - - - - - - - ); -}; - -const Sound = () => { - const { t } = useTranslation(); - const labelId = "prefSound"; - const sound = useLiveQuery(async () => prefs.sound()); - const handleChange = async (ev) => { - await prefs.setSound(ev.target.value); - await maybeUpdateAccountSettings({ - notification: { - sound: ev.target.value - } - }); - } - if (!sound) { - return null; // While loading - } - let description; - if (sound === "none") { - description = t("prefs_notifications_sound_description_none"); - } else { - description = t("prefs_notifications_sound_description_some", { sound: sounds[sound].label }); - } - return ( - -
- - - - playSound(sound)} disabled={sound === "none"} aria-label={t("prefs_notifications_sound_play")}> - - -
-
- ) -}; - -const MinPriority = () => { - const { t } = useTranslation(); - const labelId = "prefMinPriority"; - const minPriority = useLiveQuery(async () => prefs.minPriority()); - const handleChange = async (ev) => { - await prefs.setMinPriority(ev.target.value); - await maybeUpdateAccountSettings({ - notification: { - min_priority: ev.target.value - } - }); - } - if (!minPriority) { - return null; // While loading - } - const priorities = { - 1: t("priority_min"), - 2: t("priority_low"), - 3: t("priority_default"), - 4: t("priority_high"), - 5: t("priority_max") - } - let description; - if (minPriority === 1) { - description = t("prefs_notifications_min_priority_description_any"); - } else if (minPriority === 5) { - description = t("prefs_notifications_min_priority_description_max"); - } else { - description = t("prefs_notifications_min_priority_description_x_or_higher", { - number: minPriority, - name: priorities[minPriority] - }); - } - return ( - - - - - - ) -}; - -const DeleteAfter = () => { - const { t } = useTranslation(); - const labelId = "prefDeleteAfter"; - const deleteAfter = useLiveQuery(async () => prefs.deleteAfter()); - const handleChange = async (ev) => { - await prefs.setDeleteAfter(ev.target.value); - await maybeUpdateAccountSettings({ - notification: { - delete_after: ev.target.value - } - }); - } - if (deleteAfter === null || deleteAfter === undefined) { // !deleteAfter will not work with "0" - return null; // While loading - } - const description = (() => { - switch (deleteAfter) { - case 0: return t("prefs_notifications_delete_after_never_description"); - case 10800: return t("prefs_notifications_delete_after_three_hours_description"); - case 86400: return t("prefs_notifications_delete_after_one_day_description"); - case 604800: return t("prefs_notifications_delete_after_one_week_description"); - case 2592000: return t("prefs_notifications_delete_after_one_month_description"); - } - })(); - return ( - - - - - - ) -}; - -const Users = () => { - const { t } = useTranslation(); - const [dialogKey, setDialogKey] = useState(0); - const [dialogOpen, setDialogOpen] = useState(false); - const users = useLiveQuery(() => userManager.all()); - const handleAddClick = () => { - setDialogKey(prev => prev+1); - setDialogOpen(true); - }; - const handleDialogCancel = () => { - setDialogOpen(false); - }; - const handleDialogSubmit = async (user) => { - setDialogOpen(false); - try { - await userManager.save(user); - console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} added`); - } catch (e) { - console.log(`[Preferences] Error adding user.`, e); - } - }; - return ( - - - - {t("prefs_users_title")} - - - {t("prefs_users_description")} - {session.exists() && <>{" " + t("prefs_users_description_no_sync")}} - - {users?.length > 0 && } - - - - - - - ); -}; - -const UserTable = (props) => { - const { t } = useTranslation(); - const [dialogKey, setDialogKey] = useState(0); - const [dialogOpen, setDialogOpen] = useState(false); - const [dialogUser, setDialogUser] = useState(null); - - const handleEditClick = (user) => { - setDialogKey(prev => prev+1); - setDialogUser(user); - setDialogOpen(true); - }; - - const handleDialogCancel = () => { - setDialogOpen(false); - }; - - const handleDialogSubmit = async (user) => { - setDialogOpen(false); - try { - await userManager.save(user); - console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} updated`); - } catch (e) { - console.log(`[Preferences] Error updating user.`, e); - } - }; - - const handleDeleteClick = async (user) => { - try { - await userManager.delete(user.baseUrl); - console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} deleted`); - } catch (e) { - console.error(`[Preferences] Error deleting user for ${user.baseUrl}`, e); - } - }; - - return ( - - - - {t("prefs_users_table_user_header")} - {t("prefs_users_table_base_url_header")} - - - - - {props.users?.map(user => ( - - {user.username} - {user.baseUrl} - - {(!session.exists() || user.baseUrl !== config.base_url) && - <> - handleEditClick(user)} aria-label={t("prefs_users_edit_button")}> - - - handleDeleteClick(user)} aria-label={t("prefs_users_delete_button")}> - - - - } - {session.exists() && user.baseUrl === config.base_url && - - - - - - - } - - - ))} - - -
- ); -}; - -const UserDialog = (props) => { - const { t } = useTranslation(); - const [baseUrl, setBaseUrl] = useState(""); - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); - const editMode = props.user !== null; - const addButtonEnabled = (() => { - if (editMode) { - return username.length > 0 && password.length > 0; - } - const baseUrlValid = validUrl(baseUrl); - const baseUrlExists = props.users?.map(user => user.baseUrl).includes(baseUrl); - return baseUrlValid - && !baseUrlExists - && username.length > 0 - && password.length > 0; - })(); - const handleSubmit = async () => { - props.onSubmit({ - baseUrl: baseUrl, - username: username, - password: password - }) - }; - useEffect(() => { - if (editMode) { - setBaseUrl(props.user.baseUrl); - setUsername(props.user.username); - setPassword(props.user.password); - } - }, [editMode, props.user]); - return ( - - {editMode ? t("prefs_users_dialog_title_edit") : t("prefs_users_dialog_title_add")} - - {!editMode && setBaseUrl(ev.target.value)} - type="url" - fullWidth - variant="standard" - />} - setUsername(ev.target.value)} - type="text" - fullWidth - variant="standard" - /> - setPassword(ev.target.value)} - fullWidth - variant="standard" - /> - - - - - - - ); -}; - -const Appearance = () => { - const { t } = useTranslation(); - return ( - - - {t("prefs_appearance_title")} - - - - - - ); -}; - -const Language = () => { - const { t, i18n } = useTranslation(); - const labelId = "prefLanguage"; - const randomFlags = shuffle(["🇬🇧", "🇺🇸", "🇪🇸", "🇫🇷", "🇧🇬", "🇨🇿", "🇩🇪", "🇵🇱", "🇺🇦", "🇨🇳", "🇮🇹", "🇭🇺", "🇧🇷", "🇳🇱", "🇮🇩", "🇯🇵", "🇷🇺", "🇹🇷"]).slice(0, 3); - const title = t("prefs_appearance_language_title") + " " + randomFlags.join(" "); - const lang = i18n.language ?? "en"; - - const handleChange = async (ev) => { - await i18n.changeLanguage(ev.target.value); - await maybeUpdateAccountSettings({ - language: ev.target.value - }); - }; - - // Remember: Flags are not languages. Don't put flags next to the language in the list. - // Languages names from: https://www.omniglot.com/language/names.htm - // Better: Sidebar in Wikipedia: https://en.wikipedia.org/wiki/Bokm%C3%A5l - - return ( - - - - - - ) -}; - -const Reservations = () => { - const { t } = useTranslation(); - const { account } = useContext(AccountContext); - const [dialogKey, setDialogKey] = useState(0); - const [dialogOpen, setDialogOpen] = useState(false); - - if (!config.enable_reservations || !session.exists() || !account) { - return <>; - } - const reservations = account.reservations || []; - const limitReached = account.role === Role.USER && account.stats.reservations_remaining === 0; - - const handleAddClick = () => { - setDialogKey(prev => prev+1); - setDialogOpen(true); - }; - - return ( - - - - {t("prefs_reservations_title")} - - - {t("prefs_reservations_description")} - - {reservations.length > 0 && } - {limitReached && {t("prefs_reservations_limit_reached")}} - - - - setDialogOpen(false)} - /> - - - ); -}; - -const ReservationsTable = (props) => { - const { t } = useTranslation(); - const [dialogKey, setDialogKey] = useState(0); - const [dialogReservation, setDialogReservation] = useState(null); - const [editDialogOpen, setEditDialogOpen] = useState(false); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const { subscriptions } = useOutletContext(); - const localSubscriptions = (subscriptions?.length > 0) - ? Object.assign(...subscriptions.filter(s => s.baseUrl === config.base_url).map(s => ({[s.topic]: s}))) - : []; - - const handleEditClick = (reservation) => { - setDialogKey(prev => prev+1); - setDialogReservation(reservation); - setEditDialogOpen(true); - }; - - const handleDeleteClick = async (reservation) => { - setDialogKey(prev => prev+1); - setDialogReservation(reservation); - setDeleteDialogOpen(true); - }; - - const handleSubscribeClick = async (reservation) => { - await subscribeTopic(config.base_url, reservation.topic); - }; - - return ( - - - - {t("prefs_reservations_table_topic_header")} - {t("prefs_reservations_table_access_header")} - - - - - {props.reservations.map(reservation => ( - - - {reservation.topic} - - - {reservation.everyone === Permission.READ_WRITE && - <> - - {t("prefs_reservations_table_everyone_read_write")} - - } - {reservation.everyone === Permission.READ_ONLY && - <> - - {t("prefs_reservations_table_everyone_read_only")} - - } - {reservation.everyone === Permission.WRITE_ONLY && - <> - - {t("prefs_reservations_table_everyone_write_only")} - - } - {reservation.everyone === Permission.DENY_ALL && - <> - - {t("prefs_reservations_table_everyone_deny_all")} - - } - - - {!localSubscriptions[reservation.topic] && - - } onClick={() => handleSubscribeClick(reservation)} label={t("prefs_reservations_table_not_subscribed")} color="primary" variant="outlined"/> - - } - handleEditClick(reservation)} aria-label={t("prefs_reservations_edit_button")}> - - - handleDeleteClick(reservation)} aria-label={t("prefs_reservations_delete_button")}> - - - - - ))} - - setEditDialogOpen(false)} - /> - setDeleteDialogOpen(false)} - /> -
- ); -}; - -const maybeUpdateAccountSettings = async (payload) => { - if (!session.exists()) { - return; - } - try { - await accountApi.updateSettings(payload); - } catch (e) { - console.log(`[Preferences] Error updating account settings`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } - } -}; - -export default Preferences; diff --git a/web/src/components/Preferences.jsx b/web/src/components/Preferences.jsx new file mode 100644 index 00000000..6770f282 --- /dev/null +++ b/web/src/components/Preferences.jsx @@ -0,0 +1,751 @@ +import * as React from "react"; +import { useContext, useEffect, useState } from "react"; +import { + Alert, + CardActions, + CardContent, + Chip, + FormControl, + Select, + Stack, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Tooltip, + useMediaQuery, + Typography, + IconButton, + Container, + TextField, + MenuItem, + Card, + Button, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + useTheme, +} from "@mui/material"; +import EditIcon from "@mui/icons-material/Edit"; +import CloseIcon from "@mui/icons-material/Close"; +import PlayArrowIcon from "@mui/icons-material/PlayArrow"; +import { useLiveQuery } from "dexie-react-hooks"; +import { useTranslation } from "react-i18next"; +import { Info } from "@mui/icons-material"; +import { useOutletContext } from "react-router-dom"; +import userManager from "../app/UserManager"; +import { playSound, shortUrl, shuffle, sounds, validUrl } from "../app/utils"; +import session from "../app/Session"; +import routes from "./routes"; +import accountApi, { Permission, Role } from "../app/AccountApi"; +import { Pref, PrefGroup } from "./Pref"; +import { AccountContext } from "./App"; +import { Paragraph } from "./styles"; +import prefs, { THEME } from "../app/Prefs"; +import { PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite } from "./ReserveIcons"; +import { ReserveAddDialog, ReserveDeleteDialog, ReserveEditDialog } from "./ReserveDialogs"; +import { UnauthorizedError } from "../app/errors"; +import { subscribeTopic } from "./SubscribeDialog"; +import notifier from "../app/Notifier"; +import { useIsLaunchedPWA, useNotificationPermissionListener } from "./hooks"; + +const maybeUpdateAccountSettings = async (payload) => { + if (!session.exists()) { + return; + } + try { + await accountApi.updateSettings(payload); + } catch (e) { + console.log(`[Preferences] Error updating account settings`, e); + if (e instanceof UnauthorizedError) { + await session.resetAndRedirect(routes.login); + } + } +}; + +const Preferences = () => ( + + + + + + + + +); + +const Notifications = () => { + const { t } = useTranslation(); + const isLaunchedPWA = useIsLaunchedPWA(); + const pushPossible = useNotificationPermissionListener(() => notifier.pushPossible()); + + return ( + + + {t("prefs_notifications_title")} + + + + + + {!isLaunchedPWA && pushPossible && } + + + ); +}; + +const Sound = () => { + const { t } = useTranslation(); + const labelId = "prefSound"; + const sound = useLiveQuery(async () => prefs.sound()); + const handleChange = async (ev) => { + await prefs.setSound(ev.target.value); + await maybeUpdateAccountSettings({ + notification: { + sound: ev.target.value, + }, + }); + }; + if (!sound) { + return null; // While loading + } + let description; + if (sound === "none") { + description = t("prefs_notifications_sound_description_none"); + } else { + description = t("prefs_notifications_sound_description_some", { + sound: sounds[sound].label, + }); + } + return ( + +
+ + + + playSound(sound)} disabled={sound === "none"} aria-label={t("prefs_notifications_sound_play")}> + + +
+
+ ); +}; + +const MinPriority = () => { + const { t } = useTranslation(); + const labelId = "prefMinPriority"; + const minPriority = useLiveQuery(async () => prefs.minPriority()); + const handleChange = async (ev) => { + await prefs.setMinPriority(ev.target.value); + await maybeUpdateAccountSettings({ + notification: { + min_priority: ev.target.value, + }, + }); + }; + if (!minPriority) { + return null; // While loading + } + const priorities = { + 1: t("priority_min"), + 2: t("priority_low"), + 3: t("priority_default"), + 4: t("priority_high"), + 5: t("priority_max"), + }; + let description; + if (minPriority === 1) { + description = t("prefs_notifications_min_priority_description_any"); + } else if (minPriority === 5) { + description = t("prefs_notifications_min_priority_description_max"); + } else { + description = t("prefs_notifications_min_priority_description_x_or_higher", { + number: minPriority, + name: priorities[minPriority], + }); + } + return ( + + + + + + ); +}; + +const DeleteAfter = () => { + const { t } = useTranslation(); + const labelId = "prefDeleteAfter"; + const deleteAfter = useLiveQuery(async () => prefs.deleteAfter()); + const handleChange = async (ev) => { + await prefs.setDeleteAfter(ev.target.value); + await maybeUpdateAccountSettings({ + notification: { + delete_after: ev.target.value, + }, + }); + }; + + if (deleteAfter === null || deleteAfter === undefined) { + // !deleteAfter will not work with "0" + return null; // While loading + } + + const description = (() => { + switch (deleteAfter) { + case 0: + return t("prefs_notifications_delete_after_never_description"); + case 10800: + return t("prefs_notifications_delete_after_three_hours_description"); + case 86400: + return t("prefs_notifications_delete_after_one_day_description"); + case 604800: + return t("prefs_notifications_delete_after_one_week_description"); + case 2592000: + return t("prefs_notifications_delete_after_one_month_description"); + default: + return ""; + } + })(); + + return ( + + + + + + ); +}; + +const Theme = () => { + const { t } = useTranslation(); + const labelId = "prefTheme"; + const theme = useLiveQuery(async () => prefs.theme()); + const handleChange = async (ev) => { + await prefs.setTheme(ev.target.value); + }; + + return ( + + + + + + ); +}; + +const WebPushEnabled = () => { + const { t } = useTranslation(); + const labelId = "prefWebPushEnabled"; + const enabled = useLiveQuery(async () => prefs.webPushEnabled()); + const handleChange = async (ev) => { + await prefs.setWebPushEnabled(ev.target.value); + }; + + return ( + + + + + + ); +}; + +const Users = () => { + const { t } = useTranslation(); + const [dialogKey, setDialogKey] = useState(0); + const [dialogOpen, setDialogOpen] = useState(false); + const users = useLiveQuery(() => userManager.all()); + const handleAddClick = () => { + setDialogKey((prev) => prev + 1); + setDialogOpen(true); + }; + const handleDialogCancel = () => { + setDialogOpen(false); + }; + const handleDialogSubmit = async (user) => { + setDialogOpen(false); + try { + await userManager.save(user); + console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} added`); + } catch (e) { + console.log(`[Preferences] Error adding user.`, e); + } + }; + return ( + + + + {t("prefs_users_title")} + + + {t("prefs_users_description")} + {session.exists() && <>{` ${t("prefs_users_description_no_sync")}`}} + + {users?.length > 0 && } + + + + + + + ); +}; + +const UserTable = (props) => { + const { t } = useTranslation(); + const [dialogKey, setDialogKey] = useState(0); + const [dialogOpen, setDialogOpen] = useState(false); + const [dialogUser, setDialogUser] = useState(null); + + const handleEditClick = (user) => { + setDialogKey((prev) => prev + 1); + setDialogUser(user); + setDialogOpen(true); + }; + + const handleDialogCancel = () => { + setDialogOpen(false); + }; + + const handleDialogSubmit = async (user) => { + setDialogOpen(false); + try { + await userManager.save(user); + console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} updated`); + } catch (e) { + console.log(`[Preferences] Error updating user.`, e); + } + }; + + const handleDeleteClick = async (user) => { + try { + await userManager.delete(user.baseUrl); + console.debug(`[Preferences] User ${user.username} for ${user.baseUrl} deleted`); + } catch (e) { + console.error(`[Preferences] Error deleting user for ${user.baseUrl}`, e); + } + }; + + return ( + + + + {t("prefs_users_table_user_header")} + {t("prefs_users_table_base_url_header")} + + + + + {props.users?.map((user) => ( + + + {user.username} + + {user.baseUrl} + + {(!session.exists() || user.baseUrl !== config.base_url) && ( + <> + handleEditClick(user)} aria-label={t("prefs_users_edit_button")}> + + + handleDeleteClick(user)} aria-label={t("prefs_users_delete_button")}> + + + + )} + {session.exists() && user.baseUrl === config.base_url && ( + + + + + + + + + + + )} + + + ))} + + +
+ ); +}; + +const UserDialog = (props) => { + const theme = useTheme(); + const { t } = useTranslation(); + const [baseUrl, setBaseUrl] = useState(""); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); + const editMode = props.user !== null; + const addButtonEnabled = (() => { + if (editMode) { + return username.length > 0 && password.length > 0; + } + const baseUrlValid = validUrl(baseUrl); + const baseUrlExists = props.users?.map((user) => user.baseUrl).includes(baseUrl); + return baseUrlValid && !baseUrlExists && username.length > 0 && password.length > 0; + })(); + const handleSubmit = async () => { + props.onSubmit({ + baseUrl, + username, + password, + }); + }; + useEffect(() => { + if (editMode) { + setBaseUrl(props.user.baseUrl); + setUsername(props.user.username); + setPassword(props.user.password); + } + }, [editMode, props.user]); + return ( + + {editMode ? t("prefs_users_dialog_title_edit") : t("prefs_users_dialog_title_add")} + + {!editMode && ( + setBaseUrl(ev.target.value)} + type="url" + fullWidth + variant="standard" + /> + )} + setUsername(ev.target.value)} + type="text" + fullWidth + variant="standard" + /> + setPassword(ev.target.value)} + fullWidth + variant="standard" + /> + + + + + + + ); +}; + +const Appearance = () => { + const { t } = useTranslation(); + return ( + + + {t("prefs_appearance_title")} + + + + + + + ); +}; + +const Language = () => { + const { t, i18n } = useTranslation(); + const labelId = "prefLanguage"; + const lang = i18n.resolvedLanguage ?? "en"; + + // Country flags are displayed using emoji. Emoji rendering is handled by platform fonts. + // Windows in particular does not yet play nicely with flag emoji so for now, hide flags on Windows. + const randomFlags = shuffle([ + "🇬🇧", + "🇺🇸", + "🇪🇸", + "🇫🇷", + "🇧🇬", + "🇨🇿", + "🇩🇪", + "🇵🇱", + "🇺🇦", + "🇨🇳", + "🇮🇹", + "🇭🇺", + "🇧🇷", + "🇳🇱", + "🇮🇩", + "🇯🇵", + "🇷🇺", + "🇹🇷", + "🇫🇮", + ]).slice(0, 3); + const showFlags = !navigator.userAgent.includes("Windows"); + let title = t("prefs_appearance_language_title"); + if (showFlags) { + title += ` ${randomFlags.join(" ")}`; + } + + const handleChange = async (ev) => { + await i18n.changeLanguage(ev.target.value); + await maybeUpdateAccountSettings({ + language: ev.target.value, + }); + }; + + // Remember: Flags are not languages. Don't put flags next to the language in the list. + // Languages names from: https://www.omniglot.com/language/names.htm + // Better: Sidebar in Wikipedia: https://en.wikipedia.org/wiki/Bokm%C3%A5l + + return ( + + + + + + ); +}; + +const Reservations = () => { + const { t } = useTranslation(); + const { account } = useContext(AccountContext); + const [dialogKey, setDialogKey] = useState(0); + const [dialogOpen, setDialogOpen] = useState(false); + + if (!config.enable_reservations || !session.exists() || !account) { + return <>; + } + const reservations = account.reservations || []; + const limitReached = account.role === Role.USER && account.stats.reservations_remaining === 0; + + const handleAddClick = () => { + setDialogKey((prev) => prev + 1); + setDialogOpen(true); + }; + + return ( + + + + {t("prefs_reservations_title")} + + {t("prefs_reservations_description")} + {reservations.length > 0 && } + {limitReached && {t("prefs_reservations_limit_reached")}} + + + + setDialogOpen(false)} + /> + + + ); +}; + +const ReservationsTable = (props) => { + const { t } = useTranslation(); + const [dialogKey, setDialogKey] = useState(0); + const [dialogReservation, setDialogReservation] = useState(null); + const [editDialogOpen, setEditDialogOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const { subscriptions } = useOutletContext(); + const localSubscriptions = + subscriptions?.length > 0 + ? Object.assign({}, ...subscriptions.filter((s) => s.baseUrl === config.base_url).map((s) => ({ [s.topic]: s }))) + : {}; + + const handleEditClick = (reservation) => { + setDialogKey((prev) => prev + 1); + setDialogReservation(reservation); + setEditDialogOpen(true); + }; + + const handleDeleteClick = async (reservation) => { + setDialogKey((prev) => prev + 1); + setDialogReservation(reservation); + setDeleteDialogOpen(true); + }; + + const handleSubscribeClick = async (reservation) => { + await subscribeTopic(config.base_url, reservation.topic, {}); + }; + + return ( + + + + {t("prefs_reservations_table_topic_header")} + {t("prefs_reservations_table_access_header")} + + + + + {props.reservations.map((reservation) => ( + + + {reservation.topic} + + + {reservation.everyone === Permission.READ_WRITE && ( + <> + + {t("prefs_reservations_table_everyone_read_write")} + + )} + {reservation.everyone === Permission.READ_ONLY && ( + <> + + {t("prefs_reservations_table_everyone_read_only")} + + )} + {reservation.everyone === Permission.WRITE_ONLY && ( + <> + + {t("prefs_reservations_table_everyone_write_only")} + + )} + {reservation.everyone === Permission.DENY_ALL && ( + <> + + {t("prefs_reservations_table_everyone_deny_all")} + + )} + + + {!localSubscriptions[reservation.topic] && ( + + } + onClick={() => handleSubscribeClick(reservation)} + label={t("prefs_reservations_table_not_subscribed")} + color="primary" + variant="outlined" + /> + + )} + handleEditClick(reservation)} aria-label={t("prefs_reservations_edit_button")}> + + + handleDeleteClick(reservation)} aria-label={t("prefs_reservations_delete_button")}> + + + + + ))} + + setEditDialogOpen(false)} + /> + setDeleteDialogOpen(false)} + /> +
+ ); +}; + +export default Preferences; diff --git a/web/src/components/PublishDialog.js b/web/src/components/PublishDialog.js deleted file mode 100644 index bdf6fb62..00000000 --- a/web/src/components/PublishDialog.js +++ /dev/null @@ -1,740 +0,0 @@ -import * as React from 'react'; -import {useEffect, useRef, useState} from 'react'; -import theme from "./theme"; -import {Checkbox, Chip, FormControl, FormControlLabel, InputLabel, Link, Select, useMediaQuery} from "@mui/material"; -import TextField from "@mui/material/TextField"; -import priority1 from "../img/priority-1.svg"; -import priority2 from "../img/priority-2.svg"; -import priority3 from "../img/priority-3.svg"; -import priority4 from "../img/priority-4.svg"; -import priority5 from "../img/priority-5.svg"; -import Dialog from "@mui/material/Dialog"; -import DialogTitle from "@mui/material/DialogTitle"; -import DialogContent from "@mui/material/DialogContent"; -import Button from "@mui/material/Button"; -import Typography from "@mui/material/Typography"; -import IconButton from "@mui/material/IconButton"; -import InsertEmoticonIcon from '@mui/icons-material/InsertEmoticon'; -import {Close} from "@mui/icons-material"; -import MenuItem from "@mui/material/MenuItem"; -import {formatBytes, maybeWithAuth, topicShortUrl, topicUrl, validTopic, validUrl} from "../app/utils"; -import Box from "@mui/material/Box"; -import AttachmentIcon from "./AttachmentIcon"; -import DialogFooter from "./DialogFooter"; -import api from "../app/Api"; -import userManager from "../app/UserManager"; -import EmojiPicker from "./EmojiPicker"; -import {Trans, useTranslation} from "react-i18next"; -import session from "../app/Session"; -import routes from "./routes"; -import accountApi from "../app/AccountApi"; -import {UnauthorizedError} from "../app/errors"; - -const PublishDialog = (props) => { - const { t } = useTranslation(); - const [baseUrl, setBaseUrl] = useState(""); - const [topic, setTopic] = useState(""); - const [message, setMessage] = useState(""); - const [messageFocused, setMessageFocused] = useState(true); - const [title, setTitle] = useState(""); - const [tags, setTags] = useState(""); - const [priority, setPriority] = useState(3); - const [clickUrl, setClickUrl] = useState(""); - const [attachUrl, setAttachUrl] = useState(""); - const [attachFile, setAttachFile] = useState(null); - const [filename, setFilename] = useState(""); - const [filenameEdited, setFilenameEdited] = useState(false); - const [email, setEmail] = useState(""); - const [delay, setDelay] = useState(""); - const [publishAnother, setPublishAnother] = useState(false); - - const [showTopicUrl, setShowTopicUrl] = useState(""); - const [showClickUrl, setShowClickUrl] = useState(false); - const [showAttachUrl, setShowAttachUrl] = useState(false); - const [showEmail, setShowEmail] = useState(false); - const [showDelay, setShowDelay] = useState(false); - - const showAttachFile = !!attachFile && !showAttachUrl; - const attachFileInput = useRef(); - const [attachFileError, setAttachFileError] = useState(""); - - const [activeRequest, setActiveRequest] = useState(null); - const [status, setStatus] = useState(""); - const disabled = !!activeRequest; - - const [emojiPickerAnchorEl, setEmojiPickerAnchorEl] = useState(null); - - const [dropZone, setDropZone] = useState(false); - const [sendButtonEnabled, setSendButtonEnabled] = useState(true); - - const open = !!props.openMode; - const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); - - useEffect(() => { - window.addEventListener('dragenter', () => { - props.onDragEnter(); - setDropZone(true); - }); - }, []); - - useEffect(() => { - setBaseUrl(props.baseUrl); - setTopic(props.topic); - setShowTopicUrl(!props.baseUrl || !props.topic); - setMessageFocused(!!props.topic); // Focus message only if topic is set - }, [props.baseUrl, props.topic]); - - useEffect(() => { - const valid = validUrl(baseUrl) && validTopic(topic) && !attachFileError; - setSendButtonEnabled(valid); - }, [baseUrl, topic, attachFileError]); - - useEffect(() => { - setMessage(props.message); - }, [props.message]); - - const updateBaseUrl = (newVal) => { - if (validUrl(newVal)) { - setBaseUrl(newVal.replace(/\/$/, '')); // strip traililng slash after https?:// - } else { - setBaseUrl(newVal); - } - }; - - const handleSubmit = async () => { - const url = new URL(topicUrl(baseUrl, topic)); - if (title.trim()) { - url.searchParams.append("title", title.trim()); - } - if (tags.trim()) { - url.searchParams.append("tags", tags.trim()); - } - if (priority && priority !== 3) { - url.searchParams.append("priority", priority.toString()); - } - if (clickUrl.trim()) { - url.searchParams.append("click", clickUrl.trim()); - } - if (attachUrl.trim()) { - url.searchParams.append("attach", attachUrl.trim()); - } - if (filename.trim()) { - url.searchParams.append("filename", filename.trim()); - } - if (email.trim()) { - url.searchParams.append("email", email.trim()); - } - if (delay.trim()) { - url.searchParams.append("delay", delay.trim()); - } - if (attachFile && message.trim()) { - url.searchParams.append("message", message.replaceAll("\n", "\\n").trim()); - } - const body = (attachFile) ? attachFile : message; - try { - const user = await userManager.get(baseUrl); - const headers = maybeWithAuth({}, user); - const progressFn = (ev) => { - if (ev.loaded > 0 && ev.total > 0) { - setStatus(t("publish_dialog_progress_uploading_detail", { - loaded: formatBytes(ev.loaded), - total: formatBytes(ev.total), - percent: Math.round(ev.loaded * 100.0 / ev.total) - })); - } else { - setStatus(t("publish_dialog_progress_uploading")); - } - }; - const request = api.publishXHR(url, body, headers, progressFn); - setActiveRequest(request); - await request; - if (!publishAnother) { - props.onClose(); - } else { - setStatus(t("publish_dialog_message_published")); - setActiveRequest(null); - } - } catch (e) { - setStatus({e}); - setActiveRequest(null); - } - }; - - const checkAttachmentLimits = async (file) => { - try { - const account = await accountApi.get(); - const fileSizeLimit = account.limits.attachment_file_size ?? 0; - const remainingBytes = account.stats.attachment_total_size_remaining; - const fileSizeLimitReached = fileSizeLimit > 0 && file.size > fileSizeLimit; - const quotaReached = remainingBytes > 0 && file.size > remainingBytes; - if (fileSizeLimitReached && quotaReached) { - return setAttachFileError(t("publish_dialog_attachment_limits_file_and_quota_reached", { - fileSizeLimit: formatBytes(fileSizeLimit), - remainingBytes: formatBytes(remainingBytes) - })); - } else if (fileSizeLimitReached) { - return setAttachFileError(t("publish_dialog_attachment_limits_file_reached", { fileSizeLimit: formatBytes(fileSizeLimit) })); - } else if (quotaReached) { - return setAttachFileError(t("publish_dialog_attachment_limits_quota_reached", { remainingBytes: formatBytes(remainingBytes) })); - } - setAttachFileError(""); - } catch (e) { - console.log(`[PublishDialog] Retrieving attachment limits failed`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } else { - setAttachFileError(""); // Reset error (rely on server-side checking) - } - } - }; - - const handleAttachFileClick = () => { - attachFileInput.current.click(); - }; - - const handleAttachFileChanged = async (ev) => { - await updateAttachFile(ev.target.files[0]); - }; - - const handleAttachFileDrop = async (ev) => { - ev.preventDefault(); - setDropZone(false); - await updateAttachFile(ev.dataTransfer.files[0]); - }; - - const updateAttachFile = async (file) => { - setAttachFile(file); - setFilename(file.name); - props.onResetOpenMode(); - await checkAttachmentLimits(file); - }; - - const handleAttachFileDragLeave = () => { - setDropZone(false); - if (props.openMode === PublishDialog.OPEN_MODE_DRAG) { - props.onClose(); // Only close dialog if it was not open before dragging file in - } - }; - - const handleEmojiClick = (ev) => { - setEmojiPickerAnchorEl(ev.currentTarget); - }; - - const handleEmojiPick = (emoji) => { - setTags(tags => (tags.trim()) ? `${tags.trim()}, ${emoji}` : emoji); - }; - - const handleEmojiClose = () => { - setEmojiPickerAnchorEl(null); - }; - - const priorities = { - 1: { label: t("publish_dialog_priority_min"), file: priority1 }, - 2: { label: t("publish_dialog_priority_low"), file: priority2 }, - 3: { label: t("publish_dialog_priority_default"), file: priority3 }, - 4: { label: t("publish_dialog_priority_high"), file: priority4 }, - 5: { label: t("publish_dialog_priority_max"), file: priority5 } - }; - - return ( - <> - {dropZone && - } - - {(baseUrl && topic) ? t("publish_dialog_title_topic", { topic: topicShortUrl(baseUrl, topic) }) : t("publish_dialog_title_no_topic")} - - {dropZone && } - {showTopicUrl && - { - setBaseUrl(props.baseUrl); - setTopic(props.topic); - setShowTopicUrl(false); - }}> - updateBaseUrl(ev.target.value)} - disabled={disabled} - type="url" - variant="standard" - sx={{flexGrow: 1, marginRight: 1}} - inputProps={{ - "aria-label": t("publish_dialog_base_url_label") - }} - /> - setTopic(ev.target.value)} - disabled={disabled} - type="text" - variant="standard" - autoFocus={!messageFocused} - sx={{flexGrow: 1}} - inputProps={{ - "aria-label": t("publish_dialog_topic_label") - }} - /> - - } - setTitle(ev.target.value)} - disabled={disabled} - type="text" - fullWidth - variant="standard" - inputProps={{ - "aria-label": t("publish_dialog_title_label") - }} - /> - setMessage(ev.target.value)} - disabled={disabled} - type="text" - variant="standard" - rows={5} - autoFocus={messageFocused} - fullWidth - multiline - inputProps={{ - "aria-label": t("publish_dialog_message_label") - }} - /> -
- - - - - setTags(ev.target.value)} - disabled={disabled} - type="text" - variant="standard" - sx={{flexGrow: 1, marginRight: 1}} - inputProps={{ - "aria-label": t("publish_dialog_tags_label") - }} - /> - - - - -
- {showClickUrl && - { - setClickUrl(""); - setShowClickUrl(false); - }}> - setClickUrl(ev.target.value)} - disabled={disabled} - type="url" - fullWidth - variant="standard" - inputProps={{ - "aria-label": t("publish_dialog_click_label") - }} - /> - - } - {showEmail && - { - setEmail(""); - setShowEmail(false); - }}> - setEmail(ev.target.value)} - disabled={disabled} - type="email" - variant="standard" - fullWidth - inputProps={{ - "aria-label": t("publish_dialog_email_label") - }} - /> - - } - {showAttachUrl && - { - setAttachUrl(""); - setFilename(""); - setFilenameEdited(false); - setShowAttachUrl(false); - }}> - { - const url = ev.target.value; - setAttachUrl(url); - if (!filenameEdited) { - try { - const u = new URL(url); - const parts = u.pathname.split("/"); - if (parts.length > 0) { - setFilename(parts[parts.length-1]); - } - } catch (e) { - // Do nothing - } - } - }} - disabled={disabled} - type="url" - variant="standard" - sx={{flexGrow: 5, marginRight: 1}} - inputProps={{ - "aria-label": t("publish_dialog_attach_label") - }} - /> - { - setFilename(ev.target.value); - setFilenameEdited(true); - }} - disabled={disabled} - type="text" - variant="standard" - sx={{flexGrow: 1}} - inputProps={{ - "aria-label": t("publish_dialog_filename_label") - }} - /> - - } - - {showAttachFile && setFilename(f)} - onClose={() => { - setAttachFile(null); - setAttachFileError(""); - setFilename(""); - }} - />} - {showDelay && - { - setDelay(""); - setShowDelay(false); - }}> - setDelay(ev.target.value)} - disabled={disabled} - type="text" - variant="standard" - fullWidth - inputProps={{ - "aria-label": t("publish_dialog_delay_label") - }} - /> - - } - - {t("publish_dialog_other_features")} - -
- {!showClickUrl && setShowClickUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>} - {!showEmail && setShowEmail(true)} sx={{marginRight: 1, marginBottom: 1}}/>} - {!showAttachUrl && !showAttachFile && setShowAttachUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>} - {!showAttachFile && !showAttachUrl && handleAttachFileClick()} sx={{marginRight: 1, marginBottom: 1}}/>} - {!showDelay && setShowDelay(true)} sx={{marginRight: 1, marginBottom: 1}}/>} - {!showTopicUrl && setShowTopicUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>} -
- - - }} - /> - -
- - {activeRequest && } - {!activeRequest && - <> - setPublishAnother(ev.target.checked)} - inputProps={{ - "aria-label": t("publish_dialog_checkbox_publish_another") - }} /> - } /> - - - - } - -
- - ); -}; - -const Row = (props) => { - return ( -
- {props.children} -
- ); -}; - -const ClosableRow = (props) => { - const closable = (props.hasOwnProperty("closable")) ? props.closable : true; - return ( - - {props.children} - {closable && - - - - } - - ); -}; - -const DialogIconButton = (props) => { - const sx = props.sx || {}; - return ( - - {props.children} - - ); -}; - -const AttachmentBox = (props) => { - const { t } = useTranslation(); - const file = props.file; - return ( - <> - - {t("publish_dialog_attached_file_title")} - - - - - props.onChangeFilename(ev.target.value)} - disabled={props.disabled} - /> -
- - {formatBytes(file.size)} - {props.error && - - {" "}({props.error}) - - } - -
- - - -
- - ); -}; - -const ExpandingTextField = (props) => { - const invisibleFieldRef = useRef(); - const [textWidth, setTextWidth] = useState(props.minWidth); - const determineTextWidth = () => { - const boundingRect = invisibleFieldRef?.current?.getBoundingClientRect(); - if (!boundingRect) { - return props.minWidth; - } - return (boundingRect.width >= props.minWidth) ? Math.round(boundingRect.width) : props.minWidth; - }; - useEffect(() => { - setTextWidth(determineTextWidth() + 5); - }, [props.value]); - return ( - <> - - {props.value} - - - - ) -}; - -const DropArea = (props) => { - const allowDrag = (ev) => { - // This is where we could disallow certain files to be dragged in. - // For now we allow all files. - - ev.dataTransfer.dropEffect = 'copy'; - ev.preventDefault(); - }; - - return ( - - ); -}; - -const DropBox = () => { - const { t } = useTranslation(); - return ( - - - {t("publish_dialog_drop_file_here")} - - - ); -} - -PublishDialog.OPEN_MODE_DEFAULT = "default"; -PublishDialog.OPEN_MODE_DRAG = "drag"; - -export default PublishDialog; diff --git a/web/src/components/PublishDialog.jsx b/web/src/components/PublishDialog.jsx new file mode 100644 index 00000000..f18eec8d --- /dev/null +++ b/web/src/components/PublishDialog.jsx @@ -0,0 +1,934 @@ +import * as React from "react"; +import { useContext, useEffect, useRef, useState } from "react"; +import { + Checkbox, + Chip, + FormControl, + FormControlLabel, + InputLabel, + Link, + Select, + Tooltip, + useMediaQuery, + TextField, + Dialog, + DialogTitle, + DialogContent, + Button, + Typography, + IconButton, + MenuItem, + Box, + useTheme, +} from "@mui/material"; +import InsertEmoticonIcon from "@mui/icons-material/InsertEmoticon"; +import { Close } from "@mui/icons-material"; +import { Trans, useTranslation } from "react-i18next"; +import priority1 from "../img/priority-1.svg"; +import priority2 from "../img/priority-2.svg"; +import priority3 from "../img/priority-3.svg"; +import priority4 from "../img/priority-4.svg"; +import priority5 from "../img/priority-5.svg"; +import { formatBytes, maybeWithAuth, topicShortUrl, topicUrl, validTopic, validUrl } from "../app/utils"; +import AttachmentIcon from "./AttachmentIcon"; +import DialogFooter from "./DialogFooter"; +import api from "../app/Api"; +import userManager from "../app/UserManager"; +import EmojiPicker from "./EmojiPicker"; +import session from "../app/Session"; +import routes from "./routes"; +import accountApi from "../app/AccountApi"; +import { UnauthorizedError } from "../app/errors"; +import { AccountContext } from "./App"; + +const PublishDialog = (props) => { + const theme = useTheme(); + const { t } = useTranslation(); + const { account } = useContext(AccountContext); + const [baseUrl, setBaseUrl] = useState(""); + const [topic, setTopic] = useState(""); + const [message, setMessage] = useState(""); + const [messageFocused, setMessageFocused] = useState(true); + const [title, setTitle] = useState(""); + const [tags, setTags] = useState(""); + const [priority, setPriority] = useState(3); + const [clickUrl, setClickUrl] = useState(""); + const [attachUrl, setAttachUrl] = useState(""); + const [attachFile, setAttachFile] = useState(null); + const [filename, setFilename] = useState(""); + const [filenameEdited, setFilenameEdited] = useState(false); + const [email, setEmail] = useState(""); + const [call, setCall] = useState(""); + const [delay, setDelay] = useState(""); + const [publishAnother, setPublishAnother] = useState(false); + const [markdownEnabled, setMarkdownEnabled] = useState(false); + + const [showTopicUrl, setShowTopicUrl] = useState(""); + const [showClickUrl, setShowClickUrl] = useState(false); + const [showAttachUrl, setShowAttachUrl] = useState(false); + const [showEmail, setShowEmail] = useState(false); + const [showCall, setShowCall] = useState(false); + const [showDelay, setShowDelay] = useState(false); + + const showAttachFile = !!attachFile && !showAttachUrl; + const attachFileInput = useRef(); + const [attachFileError, setAttachFileError] = useState(""); + + const [activeRequest, setActiveRequest] = useState(null); + const [status, setStatus] = useState(""); + const disabled = !!activeRequest; + + const [emojiPickerAnchorEl, setEmojiPickerAnchorEl] = useState(null); + + const [dropZone, setDropZone] = useState(false); + const [sendButtonEnabled, setSendButtonEnabled] = useState(true); + + const open = !!props.openMode; + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); + + useEffect(() => { + window.addEventListener("dragenter", () => { + props.onDragEnter(); + setDropZone(true); + }); + }, []); + + useEffect(() => { + setBaseUrl(props.baseUrl); + setTopic(props.topic); + setShowTopicUrl(!props.baseUrl || !props.topic); + setMessageFocused(!!props.topic); // Focus message only if topic is set + }, [props.baseUrl, props.topic]); + + useEffect(() => { + const valid = validUrl(baseUrl) && validTopic(topic) && !attachFileError; + setSendButtonEnabled(valid); + }, [baseUrl, topic, attachFileError]); + + useEffect(() => { + setMessage(props.message); + }, [props.message]); + + const updateBaseUrl = (newVal) => { + if (validUrl(newVal)) { + setBaseUrl(newVal.replace(/\/$/, "")); // strip traililng slash after https?:// + } else { + setBaseUrl(newVal); + } + }; + + const handleSubmit = async () => { + const url = new URL(topicUrl(baseUrl, topic)); + if (title.trim()) { + url.searchParams.append("title", title.trim()); + } + if (tags.trim()) { + url.searchParams.append("tags", tags.trim()); + } + if (priority && priority !== 3) { + url.searchParams.append("priority", priority.toString()); + } + if (clickUrl.trim()) { + url.searchParams.append("click", clickUrl.trim()); + } + if (attachUrl.trim()) { + url.searchParams.append("attach", attachUrl.trim()); + } + if (filename.trim()) { + url.searchParams.append("filename", filename.trim()); + } + if (email.trim()) { + url.searchParams.append("email", email.trim()); + } + if (call.trim()) { + url.searchParams.append("call", call.trim()); + } + if (delay.trim()) { + url.searchParams.append("delay", delay.trim()); + } + if (attachFile && message.trim()) { + url.searchParams.append("message", message.replaceAll("\n", "\\n").trim()); + } + if (markdownEnabled) { + url.searchParams.append("markdown", "true"); + } + + const body = attachFile || message; + try { + const user = await userManager.get(baseUrl); + const headers = maybeWithAuth({}, user); + const progressFn = (ev) => { + if (ev.loaded > 0 && ev.total > 0) { + setStatus( + t("publish_dialog_progress_uploading_detail", { + loaded: formatBytes(ev.loaded), + total: formatBytes(ev.total), + percent: Math.round((ev.loaded * 100.0) / ev.total), + }) + ); + } else { + setStatus(t("publish_dialog_progress_uploading")); + } + }; + const request = api.publishXHR(url, body, headers, progressFn); + setActiveRequest(request); + await request; + if (!publishAnother) { + props.onClose(); + } else { + setStatus(t("publish_dialog_message_published")); + setActiveRequest(null); + } + } catch (e) { + setStatus({e}); + setActiveRequest(null); + } + }; + + const checkAttachmentLimits = async (file) => { + try { + const apiAccount = await accountApi.get(); + const fileSizeLimit = apiAccount.limits.attachment_file_size ?? 0; + const remainingBytes = apiAccount.stats.attachment_total_size_remaining; + const fileSizeLimitReached = fileSizeLimit > 0 && file.size > fileSizeLimit; + const quotaReached = remainingBytes > 0 && file.size > remainingBytes; + if (fileSizeLimitReached && quotaReached) { + setAttachFileError( + t("publish_dialog_attachment_limits_file_and_quota_reached", { + fileSizeLimit: formatBytes(fileSizeLimit), + remainingBytes: formatBytes(remainingBytes), + }) + ); + } else if (fileSizeLimitReached) { + setAttachFileError( + t("publish_dialog_attachment_limits_file_reached", { + fileSizeLimit: formatBytes(fileSizeLimit), + }) + ); + } else if (quotaReached) { + setAttachFileError( + t("publish_dialog_attachment_limits_quota_reached", { + remainingBytes: formatBytes(remainingBytes), + }) + ); + } else { + setAttachFileError(""); + } + } catch (e) { + console.log(`[PublishDialog] Retrieving attachment limits failed`, e); + if (e instanceof UnauthorizedError) { + await session.resetAndRedirect(routes.login); + } else { + setAttachFileError(""); // Reset error (rely on server-side checking) + } + } + }; + + const handleAttachFileClick = () => { + attachFileInput.current.click(); + }; + + const updateAttachFile = async (file) => { + setAttachFile(file); + setFilename(file.name); + props.onResetOpenMode(); + await checkAttachmentLimits(file); + }; + + const handleAttachFileChanged = async (ev) => { + await updateAttachFile(ev.target.files[0]); + }; + + const handleAttachFileDrop = async (ev) => { + ev.preventDefault(); + setDropZone(false); + await updateAttachFile(ev.dataTransfer.files[0]); + }; + + const handleAttachFileDragLeave = () => { + setDropZone(false); + if (props.openMode === PublishDialog.OPEN_MODE_DRAG) { + props.onClose(); // Only close dialog if it was not open before dragging file in + } + }; + + const handleEmojiClick = (ev) => { + setEmojiPickerAnchorEl(ev.currentTarget); + }; + + const handleEmojiPick = (emoji) => { + setTags((prevTags) => (prevTags.trim() ? `${prevTags.trim()}, ${emoji}` : emoji)); + }; + + const handleEmojiClose = () => { + setEmojiPickerAnchorEl(null); + }; + + const priorities = { + 1: { label: t("publish_dialog_priority_min"), file: priority1 }, + 2: { label: t("publish_dialog_priority_low"), file: priority2 }, + 3: { label: t("publish_dialog_priority_default"), file: priority3 }, + 4: { label: t("publish_dialog_priority_high"), file: priority4 }, + 5: { label: t("publish_dialog_priority_max"), file: priority5 }, + }; + + return ( + <> + {dropZone && } + + + {baseUrl && topic + ? t("publish_dialog_title_topic", { + topic: topicShortUrl(baseUrl, topic), + }) + : t("publish_dialog_title_no_topic")} + + + {dropZone && } + {showTopicUrl && ( + { + setBaseUrl(props.baseUrl); + setTopic(props.topic); + setShowTopicUrl(false); + }} + > + updateBaseUrl(ev.target.value)} + disabled={disabled} + type="url" + variant="standard" + sx={{ flexGrow: 1, marginRight: 1 }} + inputProps={{ + "aria-label": t("publish_dialog_base_url_label"), + }} + /> + setTopic(ev.target.value)} + disabled={disabled} + type="text" + variant="standard" + autoFocus={!messageFocused} + sx={{ flexGrow: 1 }} + inputProps={{ + "aria-label": t("publish_dialog_topic_label"), + }} + /> + + )} + setTitle(ev.target.value)} + disabled={disabled} + type="text" + fullWidth + variant="standard" + inputProps={{ + "aria-label": t("publish_dialog_title_label"), + }} + /> + setMessage(ev.target.value)} + disabled={disabled} + type="text" + variant="standard" + rows={5} + autoFocus={messageFocused} + fullWidth + multiline + inputProps={{ + "aria-label": t("publish_dialog_message_label"), + }} + /> + setMarkdownEnabled(ev.target.checked)} + inputProps={{ + "aria-label": t("publish_dialog_checkbox_markdown"), + }} + /> + } + /> +
+ + + + + setTags(ev.target.value)} + disabled={disabled} + type="text" + variant="standard" + sx={{ flexGrow: 1, marginRight: 1 }} + inputProps={{ + "aria-label": t("publish_dialog_tags_label"), + }} + /> + + + + +
+ {showClickUrl && ( + { + setClickUrl(""); + setShowClickUrl(false); + }} + > + setClickUrl(ev.target.value)} + disabled={disabled} + type="url" + fullWidth + variant="standard" + inputProps={{ + "aria-label": t("publish_dialog_click_label"), + }} + /> + + )} + {showEmail && ( + { + setEmail(""); + setShowEmail(false); + }} + > + setEmail(ev.target.value)} + disabled={disabled} + type="email" + variant="standard" + fullWidth + inputProps={{ + "aria-label": t("publish_dialog_email_label"), + }} + /> + + )} + {showCall && ( + { + setCall(""); + setShowCall(false); + }} + > + + + + + + )} + {showAttachUrl && ( + { + setAttachUrl(""); + setFilename(""); + setFilenameEdited(false); + setShowAttachUrl(false); + }} + > + { + const url = ev.target.value; + setAttachUrl(url); + if (!filenameEdited) { + try { + const u = new URL(url); + const parts = u.pathname.split("/"); + if (parts.length > 0) { + setFilename(parts[parts.length - 1]); + } + } catch (e) { + // Do nothing + } + } + }} + disabled={disabled} + type="url" + variant="standard" + sx={{ flexGrow: 5, marginRight: 1 }} + inputProps={{ + "aria-label": t("publish_dialog_attach_label"), + }} + /> + { + setFilename(ev.target.value); + setFilenameEdited(true); + }} + disabled={disabled} + type="text" + variant="standard" + sx={{ flexGrow: 1 }} + inputProps={{ + "aria-label": t("publish_dialog_filename_label"), + }} + /> + + )} + + {showAttachFile && ( + setFilename(f)} + onClose={() => { + setAttachFile(null); + setAttachFileError(""); + setFilename(""); + }} + /> + )} + {showDelay && ( + { + setDelay(""); + setShowDelay(false); + }} + > + setDelay(ev.target.value)} + disabled={disabled} + type="text" + variant="standard" + fullWidth + inputProps={{ + "aria-label": t("publish_dialog_delay_label"), + }} + /> + + )} + + {t("publish_dialog_other_features")} + +
+ {!showClickUrl && ( + setShowClickUrl(true)} + sx={{ marginRight: 1, marginBottom: 1 }} + /> + )} + {!showEmail && ( + setShowEmail(true)} + sx={{ marginRight: 1, marginBottom: 1 }} + /> + )} + {account?.phone_numbers?.length > 0 && !showCall && ( + { + setShowCall(true); + setCall(account.phone_numbers[0]); + }} + sx={{ marginRight: 1, marginBottom: 1 }} + /> + )} + {!showAttachUrl && !showAttachFile && ( + setShowAttachUrl(true)} + sx={{ marginRight: 1, marginBottom: 1 }} + /> + )} + {!showAttachFile && !showAttachUrl && ( + handleAttachFileClick()} + sx={{ marginRight: 1, marginBottom: 1 }} + /> + )} + {!showDelay && ( + setShowDelay(true)} + sx={{ marginRight: 1, marginBottom: 1 }} + /> + )} + {!showTopicUrl && ( + setShowTopicUrl(true)} + sx={{ marginRight: 1, marginBottom: 1 }} + /> + )} + {account && !account?.phone_numbers && ( + + + + + + )} +
+ + , + }} + /> + +
+ + {activeRequest && } + {!activeRequest && ( + <> + setPublishAnother(ev.target.checked)} + inputProps={{ + "aria-label": t("publish_dialog_checkbox_publish_another"), + }} + /> + } + /> + + + + )} + +
+ + ); +}; + +const Row = (props) => ( +
+ {props.children} +
+); + +const ClosableRow = (props) => { + const closable = props.closable !== undefined ? props.closable : true; + return ( + + {props.children} + {closable && ( + + + + )} + + ); +}; + +const DialogIconButton = (props) => { + const sx = props.sx || {}; + return ( + + {props.children} + + ); +}; + +const AttachmentBox = (props) => { + const { t } = useTranslation(); + const { file } = props; + return ( + <> + + {t("publish_dialog_attached_file_title")} + + + + + props.onChangeFilename(ev.target.value)} + disabled={props.disabled} + /> +
+ + {formatBytes(file.size)} + {props.error && ( + + {" "} + ({props.error}) + + )} + +
+ + + +
+ + ); +}; + +const ExpandingTextField = (props) => { + const theme = useTheme(); + const invisibleFieldRef = useRef(); + const [textWidth, setTextWidth] = useState(props.minWidth); + const determineTextWidth = () => { + const boundingRect = invisibleFieldRef?.current?.getBoundingClientRect(); + if (!boundingRect) { + return props.minWidth; + } + return boundingRect.width >= props.minWidth ? Math.round(boundingRect.width) : props.minWidth; + }; + useEffect(() => { + setTextWidth(determineTextWidth() + 5); + }, [props.value]); + return ( + <> + + {props.value} + + + + ); +}; + +const DropArea = (props) => { + const allowDrag = (ev) => { + // This is where we could disallow certain files to be dragged in. + // For now we allow all files. + + // eslint-disable-next-line no-param-reassign + ev.dataTransfer.dropEffect = "copy"; + ev.preventDefault(); + }; + + return ( + + ); +}; + +const DropBox = () => { + const { t } = useTranslation(); + return ( + + + {t("publish_dialog_drop_file_here")} + + + ); +}; + +PublishDialog.OPEN_MODE_DEFAULT = "default"; +PublishDialog.OPEN_MODE_DRAG = "drag"; + +export default PublishDialog; diff --git a/web/src/components/RTLCacheProvider.jsx b/web/src/components/RTLCacheProvider.jsx new file mode 100644 index 00000000..a85fced6 --- /dev/null +++ b/web/src/components/RTLCacheProvider.jsx @@ -0,0 +1,22 @@ +import React from "react"; + +import rtlPlugin from "stylis-plugin-rtl"; +import { CacheProvider } from "@emotion/react"; +import createCache from "@emotion/cache"; +import { prefixer } from "stylis"; +import { useTranslation } from "react-i18next"; + +// https://mui.com/material-ui/guides/right-to-left + +const cacheRtl = createCache({ + key: "muirtl", + stylisPlugins: [prefixer, rtlPlugin], +}); + +const RTLCacheProvider = ({ children }) => { + const { i18n } = useTranslation(); + + return i18n.dir() === "rtl" ? {children} : children; +}; + +export default RTLCacheProvider; diff --git a/web/src/components/ReserveDialogs.js b/web/src/components/ReserveDialogs.js deleted file mode 100644 index 7a6a044f..00000000 --- a/web/src/components/ReserveDialogs.js +++ /dev/null @@ -1,199 +0,0 @@ -import * as React from 'react'; -import {useState} from 'react'; -import Button from '@mui/material/Button'; -import TextField from '@mui/material/TextField'; -import Dialog from '@mui/material/Dialog'; -import DialogContent from '@mui/material/DialogContent'; -import DialogContentText from '@mui/material/DialogContentText'; -import DialogTitle from '@mui/material/DialogTitle'; -import {Alert, FormControl, Select, useMediaQuery} from "@mui/material"; -import theme from "./theme"; -import {validTopic} from "../app/utils"; -import DialogFooter from "./DialogFooter"; -import {useTranslation} from "react-i18next"; -import session from "../app/Session"; -import routes from "./routes"; -import accountApi, {Permission} from "../app/AccountApi"; -import ReserveTopicSelect from "./ReserveTopicSelect"; -import MenuItem from "@mui/material/MenuItem"; -import ListItemIcon from "@mui/material/ListItemIcon"; -import ListItemText from "@mui/material/ListItemText"; -import {Check, DeleteForever} from "@mui/icons-material"; -import {TopicReservedError, UnauthorizedError} from "../app/errors"; - -export const ReserveAddDialog = (props) => { - const { t } = useTranslation(); - const [error, setError] = useState(""); - const [topic, setTopic] = useState(props.topic || ""); - const [everyone, setEveryone] = useState(Permission.DENY_ALL); - const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); - const allowTopicEdit = !props.topic; - const alreadyReserved = props.reservations.filter(r => r.topic === topic).length > 0; - const submitButtonEnabled = validTopic(topic) && !alreadyReserved; - - const handleSubmit = async () => { - try { - await accountApi.upsertReservation(topic, everyone); - console.debug(`[ReserveAddDialog] Added reservation for topic ${t}: ${everyone}`); - } catch (e) { - console.log(`[ReserveAddDialog] Error adding topic reservation.`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } else if (e instanceof TopicReservedError) { - setError(t("subscribe_dialog_error_topic_already_reserved")); - return; - } else { - setError(e.message); - return; - } - } - props.onClose(); - }; - - return ( - - {t("prefs_reservations_dialog_title_add")} - - - {t("prefs_reservations_dialog_description")} - - {allowTopicEdit && setTopic(ev.target.value)} - type="url" - fullWidth - variant="standard" - />} - - - - - - - - ); -}; - -export const ReserveEditDialog = (props) => { - const { t } = useTranslation(); - const [error, setError] = useState(""); - const [everyone, setEveryone] = useState(props.reservation?.everyone || Permission.DENY_ALL); - const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); - - const handleSubmit = async () => { - try { - await accountApi.upsertReservation(props.reservation.topic, everyone); - console.debug(`[ReserveEditDialog] Updated reservation for topic ${t}: ${everyone}`); - } catch (e) { - console.log(`[ReserveEditDialog] Error updating topic reservation.`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } else { - setError(e.message); - return; - } - } - props.onClose(); - }; - - return ( - - {t("prefs_reservations_dialog_title_edit")} - - - {t("prefs_reservations_dialog_description")} - - - - - - - - - ); -}; - -export const ReserveDeleteDialog = (props) => { - const { t } = useTranslation(); - const [error, setError] = useState(""); - const [deleteMessages, setDeleteMessages] = useState(false); - const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); - - const handleSubmit = async () => { - try { - await accountApi.deleteReservation(props.topic, deleteMessages); - console.debug(`[ReserveDeleteDialog] Deleted reservation for topic ${props.topic}`); - } catch (e) { - console.log(`[ReserveDeleteDialog] Error deleting topic reservation.`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } else { - setError(e.message); - return; - } - } - props.onClose(); - }; - - return ( - - {t("prefs_reservations_dialog_title_delete")} - - - {t("reservation_delete_dialog_description")} - - - - - {!deleteMessages && - - {t("reservation_delete_dialog_action_keep_description")} - - } - {deleteMessages && - - {t("reservation_delete_dialog_action_delete_description")} - - } - - - - - - - ); -}; - diff --git a/web/src/components/ReserveDialogs.jsx b/web/src/components/ReserveDialogs.jsx new file mode 100644 index 00000000..7eb893cd --- /dev/null +++ b/web/src/components/ReserveDialogs.jsx @@ -0,0 +1,202 @@ +import * as React from "react"; +import { useState } from "react"; +import { + Button, + TextField, + Dialog, + DialogContent, + DialogContentText, + DialogTitle, + Alert, + FormControl, + Select, + useMediaQuery, + MenuItem, + ListItemIcon, + ListItemText, + useTheme, +} from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { Check, DeleteForever } from "@mui/icons-material"; +import { validTopic } from "../app/utils"; +import DialogFooter from "./DialogFooter"; +import session from "../app/Session"; +import routes from "./routes"; +import accountApi, { Permission } from "../app/AccountApi"; +import ReserveTopicSelect from "./ReserveTopicSelect"; +import { TopicReservedError, UnauthorizedError } from "../app/errors"; + +export const ReserveAddDialog = (props) => { + const theme = useTheme(); + const { t } = useTranslation(); + const [error, setError] = useState(""); + const [topic, setTopic] = useState(props.topic || ""); + const [everyone, setEveryone] = useState(Permission.DENY_ALL); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); + const allowTopicEdit = !props.topic; + const alreadyReserved = props.reservations.filter((r) => r.topic === topic).length > 0; + const submitButtonEnabled = validTopic(topic) && !alreadyReserved; + + const handleSubmit = async () => { + try { + await accountApi.upsertReservation(topic, everyone); + console.debug(`[ReserveAddDialog] Added reservation for topic ${topic}: ${everyone}`); + } catch (e) { + console.log(`[ReserveAddDialog] Error adding topic reservation.`, e); + if (e instanceof UnauthorizedError) { + await session.resetAndRedirect(routes.login); + } else if (e instanceof TopicReservedError) { + setError(t("subscribe_dialog_error_topic_already_reserved")); + return; + } else { + setError(e.message); + return; + } + } + props.onClose(); + }; + + return ( + + {t("prefs_reservations_dialog_title_add")} + + {t("prefs_reservations_dialog_description")} + {allowTopicEdit && ( + setTopic(ev.target.value)} + type="url" + fullWidth + variant="standard" + /> + )} + + + + + + + + ); +}; + +export const ReserveEditDialog = (props) => { + const theme = useTheme(); + const { t } = useTranslation(); + const [error, setError] = useState(""); + const [everyone, setEveryone] = useState(props.reservation?.everyone || Permission.DENY_ALL); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); + + const handleSubmit = async () => { + try { + await accountApi.upsertReservation(props.reservation.topic, everyone); + console.debug(`[ReserveEditDialog] Updated reservation for topic ${t}: ${everyone}`); + } catch (e) { + console.log(`[ReserveEditDialog] Error updating topic reservation.`, e); + if (e instanceof UnauthorizedError) { + await session.resetAndRedirect(routes.login); + } else { + setError(e.message); + return; + } + } + props.onClose(); + }; + + return ( + + {t("prefs_reservations_dialog_title_edit")} + + {t("prefs_reservations_dialog_description")} + + + + + + + + ); +}; + +export const ReserveDeleteDialog = (props) => { + const theme = useTheme(); + const { t } = useTranslation(); + const [error, setError] = useState(""); + const [deleteMessages, setDeleteMessages] = useState(false); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); + + const handleSubmit = async () => { + try { + await accountApi.deleteReservation(props.topic, deleteMessages); + console.debug(`[ReserveDeleteDialog] Deleted reservation for topic ${props.topic}`); + } catch (e) { + console.log(`[ReserveDeleteDialog] Error deleting topic reservation.`, e); + if (e instanceof UnauthorizedError) { + await session.resetAndRedirect(routes.login); + } else { + setError(e.message); + return; + } + } + props.onClose(); + }; + + return ( + + {t("prefs_reservations_dialog_title_delete")} + + {t("reservation_delete_dialog_description")} + + + + {!deleteMessages && ( + + {t("reservation_delete_dialog_action_keep_description")} + + )} + {deleteMessages && ( + + {t("reservation_delete_dialog_action_delete_description")} + + )} + + + + + + + ); +}; diff --git a/web/src/components/ReserveIcons.js b/web/src/components/ReserveIcons.js deleted file mode 100644 index 0d7b05bd..00000000 --- a/web/src/components/ReserveIcons.js +++ /dev/null @@ -1,46 +0,0 @@ -import * as React from 'react'; -import {Lock, Public} from "@mui/icons-material"; -import Box from "@mui/material/Box"; - -export const PermissionReadWrite = React.forwardRef((props, ref) => { - return ; -}); - -export const PermissionDenyAll = React.forwardRef((props, ref) => { - return ; -}); - -export const PermissionRead = React.forwardRef((props, ref) => { - return ; -}); - -export const PermissionWrite = React.forwardRef((props, ref) => { - return ; -}); - -const PermissionInternal = React.forwardRef((props, ref) => { - const size = props.size ?? "medium"; - const Icon = props.icon; - return ( - - - {props.text && - - {props.text} - - } - - ); -}); diff --git a/web/src/components/ReserveIcons.jsx b/web/src/components/ReserveIcons.jsx new file mode 100644 index 00000000..95f6f47c --- /dev/null +++ b/web/src/components/ReserveIcons.jsx @@ -0,0 +1,47 @@ +import * as React from "react"; +import { Lock, Public } from "@mui/icons-material"; +import { Box } from "@mui/material"; + +export const PermissionReadWrite = React.forwardRef((props, ref) => ); + +export const PermissionDenyAll = React.forwardRef((props, ref) => ); + +export const PermissionRead = React.forwardRef((props, ref) => ); + +export const PermissionWrite = React.forwardRef((props, ref) => ); + +const PermissionInternal = React.forwardRef((props, ref) => { + const size = props.size ?? "medium"; + const Icon = props.icon; + return ( + + + {props.text && ( + + {props.text} + + )} + + ); +}); diff --git a/web/src/components/ReserveTopicSelect.js b/web/src/components/ReserveTopicSelect.js deleted file mode 100644 index e5daf695..00000000 --- a/web/src/components/ReserveTopicSelect.js +++ /dev/null @@ -1,49 +0,0 @@ -import * as React from 'react'; -import {FormControl, Select} from "@mui/material"; -import {useTranslation} from "react-i18next"; -import MenuItem from "@mui/material/MenuItem"; -import ListItemIcon from "@mui/material/ListItemIcon"; -import ListItemText from "@mui/material/ListItemText"; -import {PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite} from "./ReserveIcons"; -import {Permission} from "../app/AccountApi"; - -const ReserveTopicSelect = (props) => { - const { t } = useTranslation(); - const sx = props.sx || {}; - return ( - - - - ); -}; - -export default ReserveTopicSelect; diff --git a/web/src/components/ReserveTopicSelect.jsx b/web/src/components/ReserveTopicSelect.jsx new file mode 100644 index 00000000..39ae5df2 --- /dev/null +++ b/web/src/components/ReserveTopicSelect.jsx @@ -0,0 +1,54 @@ +import * as React from "react"; +import { FormControl, Select, MenuItem, ListItemIcon, ListItemText } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite } from "./ReserveIcons"; +import { Permission } from "../app/AccountApi"; + +const ReserveTopicSelect = (props) => { + const { t } = useTranslation(); + const sx = props.sx || {}; + return ( + + + + ); +}; + +export default ReserveTopicSelect; diff --git a/web/src/components/Signup.js b/web/src/components/Signup.js deleted file mode 100644 index 856ce8f1..00000000 --- a/web/src/components/Signup.js +++ /dev/null @@ -1,158 +0,0 @@ -import * as React from 'react'; -import {useState} from 'react'; -import TextField from "@mui/material/TextField"; -import Button from "@mui/material/Button"; -import Box from "@mui/material/Box"; -import routes from "./routes"; -import session from "../app/Session"; -import Typography from "@mui/material/Typography"; -import {NavLink} from "react-router-dom"; -import AvatarBox from "./AvatarBox"; -import {useTranslation} from "react-i18next"; -import WarningAmberIcon from "@mui/icons-material/WarningAmber"; -import accountApi from "../app/AccountApi"; -import {InputAdornment} from "@mui/material"; -import IconButton from "@mui/material/IconButton"; -import {Visibility, VisibilityOff} from "@mui/icons-material"; -import {AccountCreateLimitReachedError, UserExistsError} from "../app/errors"; - -const Signup = () => { - const { t } = useTranslation(); - const [error, setError] = useState(""); - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - const [confirm, setConfirm] = useState(""); - const [showPassword, setShowPassword] = useState(false); - const [showConfirm, setShowConfirm] = useState(false); - - const handleSubmit = async (event) => { - event.preventDefault(); - const user = { username, password }; - try { - await accountApi.create(user.username, user.password); - const token = await accountApi.login(user); - console.log(`[Signup] User signup for user ${user.username} successful, token is ${token}`); - session.store(user.username, token); - window.location.href = routes.app; - } catch (e) { - console.log(`[Signup] Signup for user ${user.username} failed`, e); - if (e instanceof UserExistsError) { - setError(t("signup_error_username_taken", { username: e.username })); - } else if ((e instanceof AccountCreateLimitReachedError)) { - setError(t("signup_error_creation_limit_reached")); - } else { - setError(e.message); - } - } - }; - - if (!config.enable_signup) { - return ( - - {t("signup_disabled")} - - ); - } - - return ( - - - {t("signup_title")} - - - setUsername(ev.target.value.trim())} - autoFocus - /> - setPassword(ev.target.value.trim())} - InputProps={{ - endAdornment: ( - - setShowPassword(!showPassword)} - onMouseDown={(ev) => ev.preventDefault()} - edge="end" - > - {showPassword ? : } - - - ) - }} - /> - setConfirm(ev.target.value.trim())} - InputProps={{ - endAdornment: ( - - setShowConfirm(!showConfirm)} - onMouseDown={(ev) => ev.preventDefault()} - edge="end" - > - {showConfirm ? : } - - - ) - }} - /> - - {error && - - - {error} - - } - - {config.enable_login && - - - {t("signup_already_have_account")} - - - } - - ); -} - -export default Signup; diff --git a/web/src/components/Signup.jsx b/web/src/components/Signup.jsx new file mode 100644 index 00000000..7da54c49 --- /dev/null +++ b/web/src/components/Signup.jsx @@ -0,0 +1,153 @@ +import * as React from "react"; +import { useState } from "react"; +import { TextField, Button, Box, Typography, InputAdornment, IconButton } from "@mui/material"; +import { NavLink } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import WarningAmberIcon from "@mui/icons-material/WarningAmber"; +import { Visibility, VisibilityOff } from "@mui/icons-material"; +import accountApi from "../app/AccountApi"; +import AvatarBox from "./AvatarBox"; +import session from "../app/Session"; +import routes from "./routes"; +import { AccountCreateLimitReachedError, UserExistsError } from "../app/errors"; + +const Signup = () => { + const { t } = useTranslation(); + const [error, setError] = useState(""); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [confirm, setConfirm] = useState(""); + const [showPassword, setShowPassword] = useState(false); + const [showConfirm, setShowConfirm] = useState(false); + + const handleSubmit = async (event) => { + event.preventDefault(); + const user = { username, password }; + try { + await accountApi.create(user.username, user.password); + const token = await accountApi.login(user); + console.log(`[Signup] User signup for user ${user.username} successful, token is ${token}`); + await session.store(user.username, token); + window.location.href = routes.app; + } catch (e) { + console.log(`[Signup] Signup for user ${user.username} failed`, e); + if (e instanceof UserExistsError) { + setError(t("signup_error_username_taken", { username: e.username })); + } else if (e instanceof AccountCreateLimitReachedError) { + setError(t("signup_error_creation_limit_reached")); + } else { + setError(e.message); + } + } + }; + + if (!config.enable_signup) { + return ( + + {t("signup_disabled")} + + ); + } + + return ( + + {t("signup_title")} + + setUsername(ev.target.value.trim())} + autoFocus + /> + setPassword(ev.target.value.trim())} + InputProps={{ + endAdornment: ( + + setShowPassword(!showPassword)} + onMouseDown={(ev) => ev.preventDefault()} + edge="end" + > + {showPassword ? : } + + + ), + }} + /> + setConfirm(ev.target.value.trim())} + InputProps={{ + endAdornment: ( + + setShowConfirm(!showConfirm)} + onMouseDown={(ev) => ev.preventDefault()} + edge="end" + > + {showConfirm ? : } + + + ), + }} + /> + + {error && ( + + + {error} + + )} + + {config.enable_login && ( + + + {t("signup_already_have_account")} + + + )} + + ); +}; + +export default Signup; diff --git a/web/src/components/SubscribeDialog.js b/web/src/components/SubscribeDialog.js deleted file mode 100644 index 4fd4f8c4..00000000 --- a/web/src/components/SubscribeDialog.js +++ /dev/null @@ -1,313 +0,0 @@ -import * as React from 'react'; -import {useContext, useState} from 'react'; -import Button from '@mui/material/Button'; -import TextField from '@mui/material/TextField'; -import Dialog from '@mui/material/Dialog'; -import DialogContent from '@mui/material/DialogContent'; -import DialogContentText from '@mui/material/DialogContentText'; -import DialogTitle from '@mui/material/DialogTitle'; -import {Autocomplete, Checkbox, FormControlLabel, FormGroup, useMediaQuery} from "@mui/material"; -import theme from "./theme"; -import api from "../app/Api"; -import {randomAlphanumericString, topicUrl, validTopic, validUrl} from "../app/utils"; -import userManager from "../app/UserManager"; -import subscriptionManager from "../app/SubscriptionManager"; -import poller from "../app/Poller"; -import DialogFooter from "./DialogFooter"; -import {useTranslation} from "react-i18next"; -import session from "../app/Session"; -import routes from "./routes"; -import accountApi, {Permission, Role} from "../app/AccountApi"; -import ReserveTopicSelect from "./ReserveTopicSelect"; -import {AccountContext} from "./App"; -import {TopicReservedError, UnauthorizedError} from "../app/errors"; -import {ReserveLimitChip} from "./SubscriptionPopup"; - -const publicBaseUrl = "https://ntfy.sh"; - -const SubscribeDialog = (props) => { - const [baseUrl, setBaseUrl] = useState(""); - const [topic, setTopic] = useState(""); - const [showLoginPage, setShowLoginPage] = useState(false); - const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); - - const handleSuccess = async () => { - console.log(`[SubscribeDialog] Subscribing to topic ${topic}`); - const actualBaseUrl = (baseUrl) ? baseUrl : config.base_url; - const subscription = await subscribeTopic(actualBaseUrl, topic); - poller.pollInBackground(subscription); // Dangle! - props.onSuccess(subscription); - } - - return ( - - {!showLoginPage && setShowLoginPage(true)} - onSuccess={handleSuccess} - />} - {showLoginPage && setShowLoginPage(false)} - onSuccess={handleSuccess} - />} - - ); -}; - -const SubscribePage = (props) => { - const { t } = useTranslation(); - const { account } = useContext(AccountContext); - const [error, setError] = useState(""); - const [reserveTopicVisible, setReserveTopicVisible] = useState(false); - const [anotherServerVisible, setAnotherServerVisible] = useState(false); - const [everyone, setEveryone] = useState(Permission.DENY_ALL); - const baseUrl = (anotherServerVisible) ? props.baseUrl : config.base_url; - const topic = props.topic; - const existingTopicUrls = props.subscriptions.map(s => topicUrl(s.baseUrl, s.topic)); - const existingBaseUrls = Array - .from(new Set([publicBaseUrl, ...props.subscriptions.map(s => s.baseUrl)])) - .filter(s => s !== config.base_url); - const showReserveTopicCheckbox = config.enable_reservations && !anotherServerVisible && (config.enable_payments || account); - const reserveTopicEnabled = session.exists() && (account?.role === Role.ADMIN || (account?.role === Role.USER && (account?.stats.reservations_remaining || 0) > 0)); - - const handleSubscribe = async () => { - const user = await userManager.get(baseUrl); // May be undefined - const username = (user) ? user.username : t("subscribe_dialog_error_user_anonymous"); - - // Check read access to topic - const success = await api.topicAuth(baseUrl, topic, user); - if (!success) { - console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`); - if (user) { - setError(t("subscribe_dialog_error_user_not_authorized", { username: username })); - return; - } else { - props.onNeedsLogin(); - return; - } - } - - // Reserve topic (if requested) - if (session.exists() && baseUrl === config.base_url && reserveTopicVisible) { - console.log(`[SubscribeDialog] Reserving topic ${topic} with everyone access ${everyone}`); - try { - await accountApi.upsertReservation(topic, everyone); - } catch (e) { - console.log(`[SubscribeDialog] Error reserving topic`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } else if (e instanceof TopicReservedError) { - setError(t("subscribe_dialog_error_topic_already_reserved")); - return; - } - } - } - - console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`); - props.onSuccess(); - }; - - const handleUseAnotherChanged = (e) => { - props.setBaseUrl(""); - setAnotherServerVisible(e.target.checked); - }; - - const subscribeButtonEnabled = (() => { - if (anotherServerVisible) { - const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(baseUrl, topic)); - return validTopic(topic) && validUrl(baseUrl) && !isExistingTopicUrl; - } else { - const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(config.base_url, topic)); - return validTopic(topic) && !isExistingTopicUrl; - } - })(); - - const updateBaseUrl = (ev, newVal) => { - if (validUrl(newVal)) { - props.setBaseUrl(newVal.replace(/\/$/, '')); // strip trailing slash after https?:// - } else { - props.setBaseUrl(newVal); - } - }; - - return ( - <> - {t("subscribe_dialog_subscribe_title")} - - - {t("subscribe_dialog_subscribe_description")} - -
- props.setTopic(ev.target.value)} - type="text" - fullWidth - variant="standard" - inputProps={{ - maxLength: 64, - "aria-label": t("subscribe_dialog_subscribe_topic_placeholder") - }} - /> - -
- {showReserveTopicCheckbox && - - setReserveTopicVisible(ev.target.checked)} - inputProps={{ - "aria-label": t("reserve_dialog_checkbox_label") - }} - /> - } - label={ - <> - {t("reserve_dialog_checkbox_label")} - - - } - /> - {reserveTopicVisible && - - } - - } - {!reserveTopicVisible && - - - } - label={t("subscribe_dialog_subscribe_use_another_label")}/> - {anotherServerVisible && - - } - />} - - } -
- - - - - - ); -}; - -const LoginPage = (props) => { - const { t } = useTranslation(); - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - const [error, setError] = useState(""); - const baseUrl = (props.baseUrl) ? props.baseUrl : config.base_url; - const topic = props.topic; - - const handleLogin = async () => { - const user = {baseUrl, username, password}; - const success = await api.topicAuth(baseUrl, topic, user); - if (!success) { - console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`); - setError(t("subscribe_dialog_error_user_not_authorized", { username: username })); - return; - } - console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`); - await userManager.save(user); - props.onSuccess(); - }; - - return ( - <> - {t("subscribe_dialog_login_title")} - - - {t("subscribe_dialog_login_description")} - - setUsername(ev.target.value)} - type="text" - fullWidth - variant="standard" - inputProps={{ - "aria-label": t("subscribe_dialog_login_username_label") - }} - /> - setPassword(ev.target.value)} - fullWidth - variant="standard" - inputProps={{ - "aria-label": t("subscribe_dialog_login_password_label") - }} - /> - - - - - - - ); -}; - -export const subscribeTopic = async (baseUrl, topic) => { - const subscription = await subscriptionManager.add(baseUrl, topic); - if (session.exists()) { - try { - await accountApi.addSubscription(baseUrl, topic); - } catch (e) { - console.log(`[SubscribeDialog] Subscribing to topic ${topic} failed`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } - } - } - return subscription; -}; - -export default SubscribeDialog; diff --git a/web/src/components/SubscribeDialog.jsx b/web/src/components/SubscribeDialog.jsx new file mode 100644 index 00000000..f7a24f5e --- /dev/null +++ b/web/src/components/SubscribeDialog.jsx @@ -0,0 +1,332 @@ +import * as React from "react"; +import { useContext, useState } from "react"; +import { + Button, + TextField, + Dialog, + DialogContent, + DialogContentText, + DialogTitle, + Autocomplete, + FormControlLabel, + FormGroup, + useMediaQuery, + Switch, + useTheme, +} from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useLiveQuery } from "dexie-react-hooks"; +import api from "../app/Api"; +import { randomAlphanumericString, topicUrl, validTopic, validUrl } from "../app/utils"; +import userManager from "../app/UserManager"; +import subscriptionManager from "../app/SubscriptionManager"; +import poller from "../app/Poller"; +import DialogFooter from "./DialogFooter"; +import session from "../app/Session"; +import routes from "./routes"; +import accountApi, { Permission, Role } from "../app/AccountApi"; +import ReserveTopicSelect from "./ReserveTopicSelect"; +import { AccountContext } from "./App"; +import { TopicReservedError, UnauthorizedError } from "../app/errors"; +import { ReserveLimitChip } from "./SubscriptionPopup"; +import prefs from "../app/Prefs"; + +const publicBaseUrl = "https://ntfy.sh"; + +export const subscribeTopic = async (baseUrl, topic, opts) => { + const subscription = await subscriptionManager.add(baseUrl, topic, opts); + if (session.exists()) { + try { + await accountApi.addSubscription(baseUrl, topic); + } catch (e) { + console.log(`[SubscribeDialog] Subscribing to topic ${topic} failed`, e); + if (e instanceof UnauthorizedError) { + await session.resetAndRedirect(routes.login); + } + } + } + return subscription; +}; + +const SubscribeDialog = (props) => { + const theme = useTheme(); + const [baseUrl, setBaseUrl] = useState(""); + const [topic, setTopic] = useState(""); + const [showLoginPage, setShowLoginPage] = useState(false); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); + + const handleSuccess = async () => { + console.log(`[SubscribeDialog] Subscribing to topic ${topic}`); + const actualBaseUrl = baseUrl || config.base_url; + const subscription = await subscribeTopic(actualBaseUrl, topic, {}); + poller.pollInBackground(subscription); // Dangle! + props.onSuccess(subscription); + }; + + return ( + + {!showLoginPage && ( + setShowLoginPage(true)} + onSuccess={handleSuccess} + /> + )} + {showLoginPage && setShowLoginPage(false)} onSuccess={handleSuccess} />} + + ); +}; + +const SubscribePage = (props) => { + const { t } = useTranslation(); + const { account } = useContext(AccountContext); + const [error, setError] = useState(""); + const [reserveTopicVisible, setReserveTopicVisible] = useState(false); + const [anotherServerVisible, setAnotherServerVisible] = useState(false); + const [everyone, setEveryone] = useState(Permission.DENY_ALL); + const baseUrl = anotherServerVisible ? props.baseUrl : config.base_url; + const { topic } = props; + const existingTopicUrls = props.subscriptions.map((s) => topicUrl(s.baseUrl, s.topic)); + const existingBaseUrls = Array.from(new Set([publicBaseUrl, ...props.subscriptions.map((s) => s.baseUrl)])).filter( + (s) => s !== config.base_url + ); + const showReserveTopicCheckbox = config.enable_reservations && !anotherServerVisible && (config.enable_payments || account); + const reserveTopicEnabled = + session.exists() && (account?.role === Role.ADMIN || (account?.role === Role.USER && (account?.stats.reservations_remaining || 0) > 0)); + + const webPushEnabled = useLiveQuery(() => prefs.webPushEnabled()); + + const handleSubscribe = async () => { + const user = await userManager.get(baseUrl); // May be undefined + const username = user ? user.username : t("subscribe_dialog_error_user_anonymous"); + + // Check read access to topic + const success = await api.topicAuth(baseUrl, topic, user); + if (!success) { + console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`); + if (user) { + setError( + t("subscribe_dialog_error_user_not_authorized", { + username, + }) + ); + return; + } + props.onNeedsLogin(); + return; + } + + // Reserve topic (if requested) + if (session.exists() && baseUrl === config.base_url && reserveTopicVisible) { + console.log(`[SubscribeDialog] Reserving topic ${topic} with everyone access ${everyone}`); + try { + await accountApi.upsertReservation(topic, everyone); + } catch (e) { + console.log(`[SubscribeDialog] Error reserving topic`, e); + if (e instanceof UnauthorizedError) { + await session.resetAndRedirect(routes.login); + } else if (e instanceof TopicReservedError) { + setError(t("subscribe_dialog_error_topic_already_reserved")); + return; + } + } + } + + console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`); + props.onSuccess(); + }; + + const handleUseAnotherChanged = (e) => { + props.setBaseUrl(""); + setAnotherServerVisible(e.target.checked); + }; + + const subscribeButtonEnabled = (() => { + if (anotherServerVisible) { + const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(baseUrl, topic)); + return validTopic(topic) && validUrl(baseUrl) && !isExistingTopicUrl; + } + const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(config.base_url, topic)); + return validTopic(topic) && !isExistingTopicUrl; + })(); + + const updateBaseUrl = (ev, newVal) => { + if (validUrl(newVal)) { + props.setBaseUrl(newVal.replace(/\/$/, "")); // strip trailing slash after https?:// + } else { + props.setBaseUrl(newVal); + } + }; + + return ( + <> + {t("subscribe_dialog_subscribe_title")} + + {t("subscribe_dialog_subscribe_description")} +
+ props.setTopic(ev.target.value)} + type="text" + fullWidth + variant="standard" + inputProps={{ + maxLength: 64, + "aria-label": t("subscribe_dialog_subscribe_topic_placeholder"), + }} + /> + +
+ {showReserveTopicCheckbox && ( + + setReserveTopicVisible(ev.target.checked)} + inputProps={{ + "aria-label": t("reserve_dialog_checkbox_label"), + }} + /> + } + label={ + <> + {t("reserve_dialog_checkbox_label")} + + + } + /> + {reserveTopicVisible && } + + )} + {!reserveTopicVisible && ( + + + } + label={t("subscribe_dialog_subscribe_use_another_label")} + /> + {anotherServerVisible && ( + ( + <> + + {webPushEnabled && ( +
+ {t("subscribe_dialog_subscribe_use_another_background_info")} +
+ )} + + )} + /> + )} +
+ )} +
+ + + + + + ); +}; + +const LoginPage = (props) => { + const { t } = useTranslation(); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const baseUrl = props.baseUrl ? props.baseUrl : config.base_url; + const { topic } = props; + + const handleLogin = async () => { + const user = { baseUrl, username, password }; + const success = await api.topicAuth(baseUrl, topic, user); + if (!success) { + console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`); + setError(t("subscribe_dialog_error_user_not_authorized", { username })); + return; + } + console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`); + await userManager.save(user); + props.onSuccess(); + }; + + return ( + <> + {t("subscribe_dialog_login_title")} + + {t("subscribe_dialog_login_description")} + setUsername(ev.target.value)} + type="text" + fullWidth + variant="standard" + inputProps={{ + "aria-label": t("subscribe_dialog_login_username_label"), + }} + /> + setPassword(ev.target.value)} + fullWidth + variant="standard" + inputProps={{ + "aria-label": t("subscribe_dialog_login_password_label"), + }} + /> + + + + + + + ); +}; + +export default SubscribeDialog; diff --git a/web/src/components/SubscriptionPopup.js b/web/src/components/SubscriptionPopup.js deleted file mode 100644 index 7655605d..00000000 --- a/web/src/components/SubscriptionPopup.js +++ /dev/null @@ -1,292 +0,0 @@ -import * as React from 'react'; -import {useContext, useState} from 'react'; -import Button from '@mui/material/Button'; -import TextField from '@mui/material/TextField'; -import Dialog from '@mui/material/Dialog'; -import DialogContent from '@mui/material/DialogContent'; -import DialogContentText from '@mui/material/DialogContentText'; -import DialogTitle from '@mui/material/DialogTitle'; -import {Chip, InputAdornment, Portal, Snackbar, useMediaQuery} from "@mui/material"; -import theme from "./theme"; -import subscriptionManager from "../app/SubscriptionManager"; -import DialogFooter from "./DialogFooter"; -import {useTranslation} from "react-i18next"; -import accountApi, {Role} from "../app/AccountApi"; -import session from "../app/Session"; -import routes from "./routes"; -import MenuItem from "@mui/material/MenuItem"; -import PopupMenu from "./PopupMenu"; -import {formatShortDateTime, shuffle} from "../app/utils"; -import api from "../app/Api"; -import {useNavigate} from "react-router-dom"; -import IconButton from "@mui/material/IconButton"; -import {Clear} from "@mui/icons-material"; -import {AccountContext} from "./App"; -import {ReserveAddDialog, ReserveDeleteDialog, ReserveEditDialog} from "./ReserveDialogs"; -import {UnauthorizedError} from "../app/errors"; - -export const SubscriptionPopup = (props) => { - const { t } = useTranslation(); - const { account } = useContext(AccountContext); - const navigate = useNavigate(); - const [displayNameDialogOpen, setDisplayNameDialogOpen] = useState(false); - const [reserveAddDialogOpen, setReserveAddDialogOpen] = useState(false); - const [reserveEditDialogOpen, setReserveEditDialogOpen] = useState(false); - const [reserveDeleteDialogOpen, setReserveDeleteDialogOpen] = useState(false); - const [showPublishError, setShowPublishError] = useState(false); - const subscription = props.subscription; - const placement = props.placement ?? "left"; - const reservations = account?.reservations || []; - - const showReservationAdd = config.enable_reservations && !subscription?.reservation && account?.stats.reservations_remaining > 0; - const showReservationAddDisabled = !showReservationAdd && config.enable_reservations && !subscription?.reservation && (config.enable_payments || account?.stats.reservations_remaining === 0); - const showReservationEdit = config.enable_reservations && !!subscription?.reservation; - const showReservationDelete = config.enable_reservations && !!subscription?.reservation; - - const handleChangeDisplayName = async () => { - setDisplayNameDialogOpen(true); - } - - const handleReserveAdd = async () => { - setReserveAddDialogOpen(true); - } - - const handleReserveEdit = async () => { - setReserveEditDialogOpen(true); - } - - const handleReserveDelete = async () => { - setReserveDeleteDialogOpen(true); - } - - const handleSendTestMessage = async () => { - const baseUrl = props.subscription.baseUrl; - const topic = props.subscription.topic; - const tags = shuffle([ - "grinning", "octopus", "upside_down_face", "palm_tree", "maple_leaf", "apple", "skull", "warning", "jack_o_lantern", - "de-server-1", "backups", "cron-script", "script-error", "phils-automation", "mouse", "go-rocks", "hi-ben"]) - .slice(0, Math.round(Math.random() * 4)); - const priority = shuffle([1, 2, 3, 4, 5])[0]; - const title = shuffle([ - "", - "", - "", // Higher chance of no title - "Oh my, another test message?", - "Titles are optional, did you know that?", - "ntfy is open source, and will always be free. Cool, right?", - "I don't really like apples", - "My favorite TV show is The Wire. You should watch it!", - "You can attach files and URLs to messages too", - "You can delay messages up to 3 days" - ])[0]; - const nowSeconds = Math.round(Date.now()/1000); - const message = shuffle([ - `Hello friend, this is a test notification from ntfy web. It's ${formatShortDateTime(nowSeconds)} right now. Is that early or late?`, - `So I heard you like ntfy? If that's true, go to GitHub and star it, or to the Play store and rate it. Thanks! Oh yeah, this is a test notification.`, - `It's almost like you want to hear what I have to say. I'm not even a machine. I'm just a sentence that Phil typed on a random Thursday.`, - `Alright then, it's ${formatShortDateTime(nowSeconds)} already. Boy oh boy, where did the time go? I hope you're alright, friend.`, - `There are nine million bicycles in Beijing That's a fact; It's a thing we can't deny. I wonder if that's true ...`, - `I'm really excited that you're trying out ntfy. Did you know that there are a few public topics, such as ntfy.sh/stats and ntfy.sh/announcements.`, - `It's interesting to hear what people use ntfy for. I've heard people talk about using it for so many cool things. What do you use it for?` - ])[0]; - try { - await api.publish(baseUrl, topic, message, { - title: title, - priority: priority, - tags: tags - }); - } catch (e) { - console.log(`[SubscriptionPopup] Error publishing message`, e); - setShowPublishError(true); - } - } - - const handleClearAll = async () => { - console.log(`[SubscriptionPopup] Deleting all notifications from ${props.subscription.id}`); - await subscriptionManager.deleteNotifications(props.subscription.id); - }; - - const handleUnsubscribe = async () => { - console.log(`[SubscriptionPopup] Unsubscribing from ${props.subscription.id}`, props.subscription); - await subscriptionManager.remove(props.subscription.id); - if (session.exists() && !subscription.internal) { - try { - await accountApi.deleteSubscription(props.subscription.baseUrl, props.subscription.topic); - } catch (e) { - console.log(`[SubscriptionPopup] Error unsubscribing`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } - } - } - const newSelected = await subscriptionManager.first(); // May be undefined - if (newSelected && !newSelected.internal) { - navigate(routes.forSubscription(newSelected)); - } else { - navigate(routes.app); - } - }; - - return ( - <> - - {t("action_bar_change_display_name")} - {showReservationAdd && {t("action_bar_reservation_add")}} - {showReservationAddDisabled && - - {t("action_bar_reservation_add")} - - - } - {showReservationEdit && {t("action_bar_reservation_edit")}} - {showReservationDelete && {t("action_bar_reservation_delete")}} - {t("action_bar_send_test_notification")} - {t("action_bar_clear_notifications")} - {t("action_bar_unsubscribe")} - - - setShowPublishError(false)} - message={t("message_bar_error_publishing")} - /> - setDisplayNameDialogOpen(false)} - /> - {showReservationAdd && - setReserveAddDialogOpen(false)} - /> - } - {showReservationEdit && - setReserveEditDialogOpen(false)} - /> - } - {showReservationDelete && - setReserveDeleteDialogOpen(false)} - /> - } - - - ); -}; - -const DisplayNameDialog = (props) => { - const { t } = useTranslation(); - const subscription = props.subscription; - const [error, setError] = useState(""); - const [displayName, setDisplayName] = useState(subscription.displayName ?? ""); - const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); - - const handleSave = async () => { - await subscriptionManager.setDisplayName(subscription.id, displayName); - if (session.exists() && !subscription.internal) { - try { - console.log(`[SubscriptionSettingsDialog] Updating subscription display name to ${displayName}`); - await accountApi.updateSubscription(subscription.baseUrl, subscription.topic, { display_name: displayName }); - } catch (e) { - console.log(`[SubscriptionSettingsDialog] Error updating subscription`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } else { - setError(e.message); - return; - } - } - } - props.onClose(); - } - - return ( - - {t("display_name_dialog_title")} - - - {t("display_name_dialog_description")} - - setDisplayName(ev.target.value)} - type="text" - fullWidth - variant="standard" - inputProps={{ - maxLength: 64, - "aria-label": t("display_name_dialog_placeholder") - }} - InputProps={{ - endAdornment: ( - - setDisplayName("")} edge="end"> - - - - ) - }} - /> - - - - - - - ); -}; - -export const ReserveLimitChip = () => { - const { account } = useContext(AccountContext); - if (account?.role === Role.ADMIN || account?.stats.reservations_remaining > 0) { - return <>; - } else if (config.enable_payments) { - return (account?.limits.reservations > 0) ? : ; - } else if (account) { - return ; - } - return <>; -}; - -const LimitReachedChip = () => { - const { t } = useTranslation(); - return ( - - ); -}; - -const ProChip = () => { - const { t } = useTranslation(); - return ( - - ); -}; - - diff --git a/web/src/components/SubscriptionPopup.jsx b/web/src/components/SubscriptionPopup.jsx new file mode 100644 index 00000000..1a6a689c --- /dev/null +++ b/web/src/components/SubscriptionPopup.jsx @@ -0,0 +1,396 @@ +import * as React from "react"; +import { useContext, useState } from "react"; +import { + Button, + TextField, + Dialog, + DialogContent, + DialogContentText, + DialogTitle, + Chip, + InputAdornment, + Portal, + Snackbar, + useMediaQuery, + MenuItem, + IconButton, + ListItemIcon, + useTheme, +} from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { + Clear, + ClearAll, + Edit, + EnhancedEncryption, + Lock, + LockOpen, + Notifications, + NotificationsOff, + RemoveCircle, + Send, +} from "@mui/icons-material"; +import subscriptionManager from "../app/SubscriptionManager"; +import DialogFooter from "./DialogFooter"; +import accountApi, { Role } from "../app/AccountApi"; +import session from "../app/Session"; +import routes from "./routes"; +import PopupMenu from "./PopupMenu"; +import { formatShortDateTime, shuffle } from "../app/utils"; +import api from "../app/Api"; +import { AccountContext } from "./App"; +import { ReserveAddDialog, ReserveDeleteDialog, ReserveEditDialog } from "./ReserveDialogs"; +import { UnauthorizedError } from "../app/errors"; + +export const SubscriptionPopup = (props) => { + const { t } = useTranslation(); + const { account } = useContext(AccountContext); + const navigate = useNavigate(); + const [displayNameDialogOpen, setDisplayNameDialogOpen] = useState(false); + const [reserveAddDialogOpen, setReserveAddDialogOpen] = useState(false); + const [reserveEditDialogOpen, setReserveEditDialogOpen] = useState(false); + const [reserveDeleteDialogOpen, setReserveDeleteDialogOpen] = useState(false); + const [showPublishError, setShowPublishError] = useState(false); + const { subscription } = props; + const placement = props.placement ?? "left"; + const reservations = account?.reservations || []; + + const showReservationAdd = config.enable_reservations && !subscription?.reservation && account?.stats.reservations_remaining > 0; + const showReservationAddDisabled = + !showReservationAdd && + config.enable_reservations && + !subscription?.reservation && + (config.enable_payments || account?.stats.reservations_remaining === 0); + const showReservationEdit = config.enable_reservations && !!subscription?.reservation; + const showReservationDelete = config.enable_reservations && !!subscription?.reservation; + + const handleChangeDisplayName = async () => { + setDisplayNameDialogOpen(true); + }; + + const handleReserveAdd = async () => { + setReserveAddDialogOpen(true); + }; + + const handleReserveEdit = async () => { + setReserveEditDialogOpen(true); + }; + + const handleReserveDelete = async () => { + setReserveDeleteDialogOpen(true); + }; + + const handleSendTestMessage = async () => { + const { baseUrl, topic } = props.subscription; + const tags = shuffle([ + "grinning", + "octopus", + "upside_down_face", + "palm_tree", + "maple_leaf", + "apple", + "skull", + "warning", + "jack_o_lantern", + "de-server-1", + "backups", + "cron-script", + "script-error", + "phils-automation", + "mouse", + "go-rocks", + "hi-ben", + ]).slice(0, Math.round(Math.random() * 4)); + const priority = shuffle([1, 2, 3, 4, 5])[0]; + const title = shuffle([ + "", + "", + "", // Higher chance of no title + "Oh my, another test message?", + "Titles are optional, did you know that?", + "ntfy is open source, and will always be free. Cool, right?", + "I don't really like apples", + "My favorite TV show is The Wire. You should watch it!", + "You can attach files and URLs to messages too", + "You can delay messages up to 3 days", + ])[0]; + const nowSeconds = Math.round(Date.now() / 1000); + const message = shuffle([ + `Hello friend, this is a test notification from ntfy web. It's ${formatShortDateTime( + nowSeconds, + "en-US" + )} right now. Is that early or late?`, + `So I heard you like ntfy? If that's true, go to GitHub and star it, or to the Play store and rate it. Thanks! Oh yeah, this is a test notification.`, + `It's almost like you want to hear what I have to say. I'm not even a machine. I'm just a sentence that Phil typed on a random Thursday.`, + `Alright then, it's ${formatShortDateTime( + nowSeconds, + "en-US" + )} already. Boy oh boy, where did the time go? I hope you're alright, friend.`, + `There are nine million bicycles in Beijing That's a fact; It's a thing we can't deny. I wonder if that's true ...`, + `I'm really excited that you're trying out ntfy. Did you know that there are a few public topics, such as ntfy.sh/stats and ntfy.sh/announcements.`, + `It's interesting to hear what people use ntfy for. I've heard people talk about using it for so many cool things. What do you use it for?`, + ])[0]; + try { + await api.publish(baseUrl, topic, message, { + title, + priority, + tags, + }); + } catch (e) { + console.log(`[SubscriptionPopup] Error publishing message`, e); + setShowPublishError(true); + } + }; + + const handleClearAll = async () => { + console.log(`[SubscriptionPopup] Deleting all notifications from ${props.subscription.id}`); + await subscriptionManager.deleteNotifications(props.subscription.id); + }; + + const handleSetMutedUntil = async (mutedUntil) => { + await subscriptionManager.setMutedUntil(subscription.id, mutedUntil); + }; + + const handleUnsubscribe = async () => { + console.log(`[SubscriptionPopup] Unsubscribing from ${props.subscription.id}`, props.subscription); + await subscriptionManager.remove(props.subscription); + if (session.exists() && !subscription.internal) { + try { + await accountApi.deleteSubscription(props.subscription.baseUrl, props.subscription.topic); + } catch (e) { + console.log(`[SubscriptionPopup] Error unsubscribing`, e); + if (e instanceof UnauthorizedError) { + await session.resetAndRedirect(routes.login); + } + } + } + const newSelected = await subscriptionManager.first(); // May be undefined + if (newSelected && !newSelected.internal) { + navigate(routes.forSubscription(newSelected)); + } else { + navigate(routes.app); + } + }; + + return ( + <> + + + + + + {t("action_bar_change_display_name")} + + {showReservationAdd && ( + + + + + {t("action_bar_reservation_add")} + + )} + {showReservationAddDisabled && ( + + + + + {t("action_bar_reservation_add")} + + + )} + {showReservationEdit && ( + + + + + {t("action_bar_reservation_edit")} + + )} + {showReservationDelete && ( + + + + + {t("action_bar_reservation_delete")} + + )} + + + + + {t("action_bar_send_test_notification")} + + + + + + {t("action_bar_clear_notifications")} + + {!!subscription.mutedUntil && ( + handleSetMutedUntil(0)}> + + + + {t("action_bar_unmute_notifications")} + + )} + {!subscription.mutedUntil && ( + handleSetMutedUntil(1)}> + + + + {t("action_bar_mute_notifications")} + + )} + + + + + {t("action_bar_unsubscribe")} + + + + setShowPublishError(false)} + message={t("message_bar_error_publishing")} + /> + setDisplayNameDialogOpen(false)} /> + {showReservationAdd && ( + setReserveAddDialogOpen(false)} + /> + )} + {showReservationEdit && ( + setReserveEditDialogOpen(false)} + /> + )} + {showReservationDelete && ( + setReserveDeleteDialogOpen(false)} + /> + )} + + + ); +}; + +const DisplayNameDialog = (props) => { + const theme = useTheme(); + const { t } = useTranslation(); + const { subscription } = props; + const [error, setError] = useState(""); + const [displayName, setDisplayName] = useState(subscription.displayName ?? ""); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); + + const handleSave = async () => { + await subscriptionManager.setDisplayName(subscription.id, displayName); + if (session.exists() && !subscription.internal) { + try { + console.log(`[SubscriptionSettingsDialog] Updating subscription display name to ${displayName}`); + await accountApi.updateSubscription(subscription.baseUrl, subscription.topic, { display_name: displayName }); + } catch (e) { + console.log(`[SubscriptionSettingsDialog] Error updating subscription`, e); + if (e instanceof UnauthorizedError) { + await session.resetAndRedirect(routes.login); + } else { + setError(e.message); + return; + } + } + } + props.onClose(); + }; + + return ( + + {t("display_name_dialog_title")} + + {t("display_name_dialog_description")} + setDisplayName(ev.target.value)} + type="text" + fullWidth + variant="standard" + inputProps={{ + maxLength: 64, + "aria-label": t("display_name_dialog_placeholder"), + }} + InputProps={{ + endAdornment: ( + + setDisplayName("")} edge="end"> + + + + ), + }} + /> + + + + + + + ); +}; + +export const ReserveLimitChip = () => { + const { account } = useContext(AccountContext); + if (account?.role === Role.ADMIN || account?.stats.reservations_remaining > 0) { + return <>; + } + if (config.enable_payments) { + return account?.limits.reservations > 0 ? : ; + } + if (account) { + return ; + } + return <>; +}; + +const LimitReachedChip = () => { + const { t } = useTranslation(); + return ( + + ); +}; + +export const ProChip = () => ( + +); diff --git a/web/src/components/UpgradeDialog.js b/web/src/components/UpgradeDialog.js deleted file mode 100644 index c62560a3..00000000 --- a/web/src/components/UpgradeDialog.js +++ /dev/null @@ -1,366 +0,0 @@ -import * as React from 'react'; -import {useContext, useEffect, useState} from 'react'; -import Dialog from '@mui/material/Dialog'; -import DialogContent from '@mui/material/DialogContent'; -import DialogTitle from '@mui/material/DialogTitle'; -import {Alert, CardActionArea, CardContent, Chip, Link, ListItem, Switch, useMediaQuery} from "@mui/material"; -import theme from "./theme"; -import Button from "@mui/material/Button"; -import accountApi, {SubscriptionInterval} from "../app/AccountApi"; -import session from "../app/Session"; -import routes from "./routes"; -import Card from "@mui/material/Card"; -import Typography from "@mui/material/Typography"; -import {AccountContext} from "./App"; -import {formatBytes, formatNumber, formatPrice, formatShortDate} from "../app/utils"; -import {Trans, useTranslation} from "react-i18next"; -import List from "@mui/material/List"; -import {Check, Close} from "@mui/icons-material"; -import ListItemIcon from "@mui/material/ListItemIcon"; -import ListItemText from "@mui/material/ListItemText"; -import Box from "@mui/material/Box"; -import {NavLink} from "react-router-dom"; -import {UnauthorizedError} from "../app/errors"; -import DialogContentText from "@mui/material/DialogContentText"; -import DialogActions from "@mui/material/DialogActions"; - -const UpgradeDialog = (props) => { - const { t } = useTranslation(); - const { account } = useContext(AccountContext); // May be undefined! - const [error, setError] = useState(""); - const [tiers, setTiers] = useState(null); - const [interval, setInterval] = useState(account?.billing?.interval || SubscriptionInterval.YEAR); - const [newTierCode, setNewTierCode] = useState(account?.tier?.code); // May be undefined - const [loading, setLoading] = useState(false); - const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); - - useEffect(() => { - const fetchTiers = async () => { - setTiers(await accountApi.billingTiers()); - } - fetchTiers(); // Dangle - }, []); - - if (!tiers) { - return <>; - } - - const tiersMap = Object.assign(...tiers.map(tier => ({[tier.code]: tier}))); - const newTier = tiersMap[newTierCode]; // May be undefined - const currentTier = account?.tier; // May be undefined - const currentInterval = account?.billing?.interval; // May be undefined - const currentTierCode = currentTier?.code; // May be undefined - - // Figure out buttons, labels and the submit action - let submitAction, submitButtonLabel, banner; - if (!account) { - submitButtonLabel = t("account_upgrade_dialog_button_redirect_signup"); - submitAction = Action.REDIRECT_SIGNUP; - banner = null; - } else if (currentTierCode === newTierCode && (currentInterval === undefined || currentInterval === interval)) { - submitButtonLabel = t("account_upgrade_dialog_button_update_subscription"); - submitAction = null; - banner = (currentTierCode) ? Banner.PRORATION_INFO : null; - } else if (!currentTierCode) { - submitButtonLabel = t("account_upgrade_dialog_button_pay_now"); - submitAction = Action.CREATE_SUBSCRIPTION; - banner = null; - } else if (!newTierCode) { - submitButtonLabel = t("account_upgrade_dialog_button_cancel_subscription"); - submitAction = Action.CANCEL_SUBSCRIPTION; - banner = Banner.CANCEL_WARNING; - } else { - submitButtonLabel = t("account_upgrade_dialog_button_update_subscription"); - submitAction = Action.UPDATE_SUBSCRIPTION; - banner = Banner.PRORATION_INFO; - } - - // Exceptional conditions - if (loading) { - submitAction = null; - } else if (newTier?.code && account?.reservations?.length > newTier?.limits?.reservations) { - submitAction = null; - banner = Banner.RESERVATIONS_WARNING; - } - - const handleSubmit = async () => { - if (submitAction === Action.REDIRECT_SIGNUP) { - window.location.href = routes.signup; - return; - } - try { - setLoading(true); - if (submitAction === Action.CREATE_SUBSCRIPTION) { - const response = await accountApi.createBillingSubscription(newTierCode, interval); - window.location.href = response.redirect_url; - } else if (submitAction === Action.UPDATE_SUBSCRIPTION) { - await accountApi.updateBillingSubscription(newTierCode, interval); - } else if (submitAction === Action.CANCEL_SUBSCRIPTION) { - await accountApi.deleteBillingSubscription(); - } - props.onCancel(); - } catch (e) { - console.log(`[UpgradeDialog] Error changing billing subscription`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } else { - setError(e.message); - } - } finally { - setLoading(false); - } - } - - // Figure out discount - let discount = 0, upto = false; - if (newTier?.prices) { - discount = Math.round(((newTier.prices.month*12/newTier.prices.year)-1)*100); - } else { - let n = 0; - for (const t of tiers) { - if (t.prices) { - const tierDiscount = Math.round(((t.prices.month*12/t.prices.year)-1)*100); - if (tierDiscount > discount) { - discount = tierDiscount; - n++; - } - } - } - upto = n > 1; - } - - return ( - - -
-
{t("account_upgrade_dialog_title")}
-
- {t("account_upgrade_dialog_interval_monthly")} - setInterval(ev.target.checked ? SubscriptionInterval.YEAR : SubscriptionInterval.MONTH)} - /> - {t("account_upgrade_dialog_interval_yearly")} - {discount > 0 && - - } -
-
-
- -
- {tiers.map(tier => - setNewTierCode(tier.code)} // tier.code may be undefined! - /> - )} -
- {banner === Banner.CANCEL_WARNING && - - - - } - {banner === Banner.PRORATION_INFO && - - - - } - {banner === Banner.RESERVATIONS_WARNING && - - , - }} - /> - - } -
- - - {config.billing_contact.indexOf('@') !== -1 && - <> }}/>{" "} - } - {config.billing_contact.match(`^http?s://`) && - <> }}/>{" "} - } - {error} - - - - - - -
- ); -}; - -const TierCard = (props) => { - const { t } = useTranslation(); - const tier = props.tier; - - let cardStyle, labelStyle, labelText; - if (props.selected) { - cardStyle = { background: "#eee", border: "3px solid #338574" }; - labelStyle = { background: "#338574", color: "white" }; - labelText = t("account_upgrade_dialog_tier_selected_label"); - } else if (props.current) { - cardStyle = { border: "3px solid #eee" }; - labelStyle = { background: "#eee", color: "black" }; - labelText = t("account_upgrade_dialog_tier_current_label"); - } else { - cardStyle = { border: "3px solid transparent" }; - } - - let monthlyPrice; - if (!tier.prices) { - monthlyPrice = 0; - } else if (props.interval === SubscriptionInterval.YEAR) { - monthlyPrice = tier.prices.year/12; - } else if (props.interval === SubscriptionInterval.MONTH) { - monthlyPrice = tier.prices.month; - } - - return ( - - - - - {labelStyle && -
{labelText}
- } - - {tier.name || t("account_basics_tier_free")} - -
- {formatPrice(monthlyPrice)} - {monthlyPrice > 0 && <>/ {t("account_upgrade_dialog_tier_price_per_month")}} -
- - {tier.limits.reservations > 0 && {t("account_upgrade_dialog_tier_features_reservations", { reservations: tier.limits.reservations, count: tier.limits.reservations })}} - {tier.limits.reservations === 0 && {t("account_upgrade_dialog_tier_features_no_reservations")}} - {t("account_upgrade_dialog_tier_features_messages", { messages: formatNumber(tier.limits.messages), count: tier.limits.messages })} - {t("account_upgrade_dialog_tier_features_emails", { emails: formatNumber(tier.limits.emails), count: tier.limits.emails })} - {t("account_upgrade_dialog_tier_features_attachment_file_size", { filesize: formatBytes(tier.limits.attachment_file_size, 0) })} - {t("account_upgrade_dialog_tier_features_attachment_total_size", { totalsize: formatBytes(tier.limits.attachment_total_size, 0) })} - - {tier.prices && props.interval === SubscriptionInterval.MONTH && - - {t("account_upgrade_dialog_tier_price_billed_monthly", { price: formatPrice(tier.prices.month*12) })} - - } - {tier.prices && props.interval === SubscriptionInterval.YEAR && - - {t("account_upgrade_dialog_tier_price_billed_yearly", { price: formatPrice(tier.prices.year), save: formatPrice(tier.prices.month*12-tier.prices.year) })} - - } -
-
-
-
- - ); -} - -const Feature = (props) => { - return {props.children}; -} - -const NoFeature = (props) => { - return {props.children}; -} - -const FeatureItem = (props) => { - return ( - - - {props.feature && } - {!props.feature && } - - - {props.children} - - } - /> - - - ); -}; - -const Action = { - REDIRECT_SIGNUP: 1, - CREATE_SUBSCRIPTION: 2, - UPDATE_SUBSCRIPTION: 3, - CANCEL_SUBSCRIPTION: 4 -}; - -const Banner = { - CANCEL_WARNING: 1, - PRORATION_INFO: 2, - RESERVATIONS_WARNING: 3 -}; - -export default UpgradeDialog; diff --git a/web/src/components/UpgradeDialog.jsx b/web/src/components/UpgradeDialog.jsx new file mode 100644 index 00000000..712c47ec --- /dev/null +++ b/web/src/components/UpgradeDialog.jsx @@ -0,0 +1,436 @@ +import * as React from "react"; +import { useContext, useEffect, useState } from "react"; +import { + Dialog, + DialogContent, + DialogTitle, + Alert, + CardActionArea, + CardContent, + Chip, + Link, + ListItem, + Switch, + useMediaQuery, + Button, + Card, + Typography, + List, + ListItemIcon, + ListItemText, + Box, + DialogContentText, + DialogActions, + useTheme, +} from "@mui/material"; +import { Trans, useTranslation } from "react-i18next"; +import { Check, Close } from "@mui/icons-material"; +import { NavLink } from "react-router-dom"; +import { UnauthorizedError } from "../app/errors"; +import { formatBytes, formatNumber, formatPrice, formatShortDate } from "../app/utils"; +import { AccountContext } from "./App"; +import routes from "./routes"; +import session from "../app/Session"; +import accountApi, { SubscriptionInterval } from "../app/AccountApi"; + +const Feature = (props) => {props.children}; + +const NoFeature = (props) => {props.children}; + +const FeatureItem = (props) => ( + + + {props.feature && } + {!props.feature && } + + {props.children}} /> + +); + +const Action = { + REDIRECT_SIGNUP: 1, + CREATE_SUBSCRIPTION: 2, + UPDATE_SUBSCRIPTION: 3, + CANCEL_SUBSCRIPTION: 4, +}; + +const Banner = { + CANCEL_WARNING: 1, + PRORATION_INFO: 2, + RESERVATIONS_WARNING: 3, +}; + +const UpgradeDialog = (props) => { + const theme = useTheme(); + const { t, i18n } = useTranslation(); + const { account } = useContext(AccountContext); // May be undefined! + const [error, setError] = useState(""); + const [tiers, setTiers] = useState(null); + const [interval, setInterval] = useState(account?.billing?.interval || SubscriptionInterval.YEAR); + const [newTierCode, setNewTierCode] = useState(account?.tier?.code); // May be undefined + const [loading, setLoading] = useState(false); + const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); + + useEffect(() => { + const fetchTiers = async () => { + setTiers(await accountApi.billingTiers()); + }; + fetchTiers(); // Dangle + }, []); + + if (!tiers) { + return <>; + } + + const tiersMap = Object.assign(...tiers.map((tier) => ({ [tier.code]: tier }))); + const newTier = tiersMap[newTierCode]; // May be undefined + const currentTier = account?.tier; // May be undefined + const currentInterval = account?.billing?.interval; // May be undefined + const currentTierCode = currentTier?.code; // May be undefined + + // Figure out buttons, labels and the submit action + let submitAction; + let submitButtonLabel; + let banner; + if (!account) { + submitButtonLabel = t("account_upgrade_dialog_button_redirect_signup"); + submitAction = Action.REDIRECT_SIGNUP; + banner = null; + } else if (currentTierCode === newTierCode && (currentInterval === undefined || currentInterval === interval)) { + submitButtonLabel = t("account_upgrade_dialog_button_update_subscription"); + submitAction = null; + banner = currentTierCode ? Banner.PRORATION_INFO : null; + } else if (!currentTierCode) { + submitButtonLabel = t("account_upgrade_dialog_button_pay_now"); + submitAction = Action.CREATE_SUBSCRIPTION; + banner = null; + } else if (!newTierCode) { + submitButtonLabel = t("account_upgrade_dialog_button_cancel_subscription"); + submitAction = Action.CANCEL_SUBSCRIPTION; + banner = Banner.CANCEL_WARNING; + } else { + submitButtonLabel = t("account_upgrade_dialog_button_update_subscription"); + submitAction = Action.UPDATE_SUBSCRIPTION; + banner = Banner.PRORATION_INFO; + } + + // Exceptional conditions + if (loading) { + submitAction = null; + } else if (newTier?.code && account?.reservations?.length > newTier?.limits?.reservations) { + submitAction = null; + banner = Banner.RESERVATIONS_WARNING; + } + + const handleSubmit = async () => { + if (submitAction === Action.REDIRECT_SIGNUP) { + window.location.href = routes.signup; + return; + } + try { + setLoading(true); + if (submitAction === Action.CREATE_SUBSCRIPTION) { + const response = await accountApi.createBillingSubscription(newTierCode, interval); + window.location.href = response.redirect_url; + } else if (submitAction === Action.UPDATE_SUBSCRIPTION) { + await accountApi.updateBillingSubscription(newTierCode, interval); + } else if (submitAction === Action.CANCEL_SUBSCRIPTION) { + await accountApi.deleteBillingSubscription(); + } + props.onCancel(); + } catch (e) { + console.log(`[UpgradeDialog] Error changing billing subscription`, e); + if (e instanceof UnauthorizedError) { + await session.resetAndRedirect(routes.login); + } else { + setError(e.message); + } + } finally { + setLoading(false); + } + }; + + // Figure out discount + let discount = 0; + let upto = false; + if (newTier?.prices) { + discount = Math.round(((newTier.prices.month * 12) / newTier.prices.year - 1) * 100); + } else { + let n = 0; + for (const tier of tiers) { + if (tier.prices) { + const tierDiscount = Math.round(((tier.prices.month * 12) / tier.prices.year - 1) * 100); + if (tierDiscount > discount) { + discount = tierDiscount; + n += 1; + } + } + } + upto = n > 1; + } + + return ( + + +
+
{t("account_upgrade_dialog_title")}
+
+ + {t("account_upgrade_dialog_interval_monthly")} + + setInterval(ev.target.checked ? SubscriptionInterval.YEAR : SubscriptionInterval.MONTH)} + /> + + {t("account_upgrade_dialog_interval_yearly")} + + {discount > 0 && ( + + )} +
+
+
+ +
+ {tiers.map((tier) => ( + setNewTierCode(tier.code)} // tier.code may be undefined! + /> + ))} +
+ {banner === Banner.CANCEL_WARNING && ( + + + + )} + {banner === Banner.PRORATION_INFO && ( + + + + )} + {banner === Banner.RESERVATIONS_WARNING && ( + + , + }} + /> + + )} +
+ + + {config.billing_contact.indexOf("@") !== -1 && ( + <> + , + }} + />{" "} + + )} + {config.billing_contact.match(`^http?s://`) && ( + <> + , + }} + />{" "} + + )} + {error} + + + + + + +
+ ); +}; + +const TierCard = (props) => { + const { t } = useTranslation(); + const { tier } = props; + + let cardStyle; + let labelStyle; + let labelText; + if (props.selected) { + cardStyle = { background: "#eee", border: "3px solid #338574" }; + labelStyle = { background: "#338574", color: "white" }; + labelText = t("account_upgrade_dialog_tier_selected_label"); + } else if (props.current) { + cardStyle = { border: "3px solid #eee" }; + labelStyle = { background: "#eee", color: "black" }; + labelText = t("account_upgrade_dialog_tier_current_label"); + } else { + cardStyle = { border: "3px solid transparent" }; + } + + let monthlyPrice; + if (!tier.prices) { + monthlyPrice = 0; + } else if (props.interval === SubscriptionInterval.YEAR) { + monthlyPrice = tier.prices.year / 12; + } else if (props.interval === SubscriptionInterval.MONTH) { + monthlyPrice = tier.prices.month; + } + + return ( + + + + + {labelStyle && ( +
+ {labelText} +
+ )} + + {tier.name || t("account_basics_tier_free")} + +
+ + {formatPrice(monthlyPrice)} + + {monthlyPrice > 0 && <>/ {t("account_upgrade_dialog_tier_price_per_month")}} +
+ + {tier.limits.reservations > 0 && ( + + {t("account_upgrade_dialog_tier_features_reservations", { + reservations: tier.limits.reservations, + count: tier.limits.reservations, + })} + + )} + + {t("account_upgrade_dialog_tier_features_messages", { + messages: formatNumber(tier.limits.messages), + count: tier.limits.messages, + })} + + + {t("account_upgrade_dialog_tier_features_emails", { + emails: formatNumber(tier.limits.emails), + count: tier.limits.emails, + })} + + {tier.limits.calls > 0 && ( + + {t("account_upgrade_dialog_tier_features_calls", { + calls: formatNumber(tier.limits.calls), + count: tier.limits.calls, + })} + + )} + + {t("account_upgrade_dialog_tier_features_attachment_file_size", { + filesize: formatBytes(tier.limits.attachment_file_size, 0), + })} + + {tier.limits.reservations === 0 && {t("account_upgrade_dialog_tier_features_no_reservations")}} + {tier.limits.calls === 0 && {t("account_upgrade_dialog_tier_features_no_calls")}} + + {tier.prices && props.interval === SubscriptionInterval.MONTH && ( + + {t("account_upgrade_dialog_tier_price_billed_monthly", { + price: formatPrice(tier.prices.month * 12), + })} + + )} + {tier.prices && props.interval === SubscriptionInterval.YEAR && ( + + {t("account_upgrade_dialog_tier_price_billed_yearly", { + price: formatPrice(tier.prices.year), + save: formatPrice(tier.prices.month * 12 - tier.prices.year), + })} + + )} +
+
+
+
+ ); +}; + +export default UpgradeDialog; diff --git a/web/src/components/hooks.js b/web/src/components/hooks.js index b1ce8ffb..519d4c6a 100644 --- a/web/src/components/hooks.js +++ b/web/src/components/hooks.js @@ -1,81 +1,102 @@ -import {useNavigate, useParams} from "react-router-dom"; -import {useEffect, useState} from "react"; +import { useParams } from "react-router-dom"; +import { useEffect, useMemo, useState } from "react"; +import { useLiveQuery } from "dexie-react-hooks"; import subscriptionManager from "../app/SubscriptionManager"; -import {disallowedTopic, expandSecureUrl, topicUrl} from "../app/utils"; -import notifier from "../app/Notifier"; +import { disallowedTopic, expandSecureUrl, topicUrl } from "../app/utils"; import routes from "./routes"; import connectionManager from "../app/ConnectionManager"; import poller from "../app/Poller"; import pruner from "../app/Pruner"; import session from "../app/Session"; import accountApi from "../app/AccountApi"; -import {UnauthorizedError} from "../app/errors"; +import { UnauthorizedError } from "../app/errors"; +import notifier from "../app/Notifier"; +import prefs from "../app/Prefs"; /** * Wire connectionManager and subscriptionManager so that subscriptions are updated when the connection * state changes. Conversely, when the subscription changes, the connection is refreshed (which may lead * to the connection being re-established). + * + * When Web Push is enabled, we do not need to connect to our home server via WebSocket, since notifications + * will be delivered via Web Push. However, we still need to connect to other servers via WebSocket, or for internal + * topics, such as sync topics (st_...). */ -export const useConnectionListeners = (account, subscriptions, users) => { - const navigate = useNavigate(); +export const useConnectionListeners = (account, subscriptions, users, webPushTopics) => { + const wsSubscriptions = useMemo( + () => (subscriptions && webPushTopics ? subscriptions.filter((s) => !webPushTopics.includes(s.topic)) : []), + // wsSubscriptions should stay stable unless the list of subscription IDs changes. Without the memo, the connection + // listener calls a refresh for no reason. This isn't a problem due to the makeConnectionId, but it triggers an + // unnecessary recomputation for every received message. + [JSON.stringify({ subscriptions: subscriptions?.map(({ id }) => id), webPushTopics })] + ); - // Register listeners for incoming messages, and connection state changes - useEffect(() => { - const handleMessage = async (subscriptionId, message) => { - const subscription = await subscriptionManager.get(subscriptionId); - if (subscription.internal) { - await handleInternalMessage(message); - } else { - await handleNotification(subscriptionId, message); - } - }; - - const handleInternalMessage = async (message) => { - console.log(`[ConnectionListener] Received message on sync topic`, message.message); - try { - const data = JSON.parse(message.message); - if (data.event === "sync") { - console.log(`[ConnectionListener] Triggering account sync`); - await accountApi.sync(); - } else { - console.log(`[ConnectionListener] Unknown message type. Doing nothing.`); - } - } catch (e) { - console.log(`[ConnectionListener] Error parsing sync topic message`, e); - } - }; - - const handleNotification = async (subscriptionId, notification) => { - const added = await subscriptionManager.addNotification(subscriptionId, notification); - if (added) { - const defaultClickAction = (subscription) => navigate(routes.forSubscription(subscription)); - await notifier.notify(subscriptionId, notification, defaultClickAction) - } - }; - connectionManager.registerStateListener(subscriptionManager.updateState); - connectionManager.registerMessageListener(handleMessage); - return () => { - connectionManager.resetStateListener(); - connectionManager.resetMessageListener(); - } - }, - // We have to disable dep checking for "navigate". This is fine, it never changes. - // eslint-disable-next-line - [] - ); - - // Sync topic listener: For accounts with sync_topic, subscribe to an internal topic - useEffect(() => { - if (!account || !account.sync_topic) { - return; + // Register listeners for incoming messages, and connection state changes + useEffect( + () => { + const handleInternalMessage = async (message) => { + console.log(`[ConnectionListener] Received message on sync topic`, message.message); + try { + const data = JSON.parse(message.message); + if (data.event === "sync") { + console.log(`[ConnectionListener] Triggering account sync`); + await accountApi.sync(); + } else { + console.log(`[ConnectionListener] Unknown message type. Doing nothing.`); + } + } catch (e) { + console.log(`[ConnectionListener] Error parsing sync topic message`, e); } - subscriptionManager.add(config.base_url, account.sync_topic, true); // Dangle! - }, [account]); + }; - // When subscriptions or users change, refresh the connections - useEffect(() => { - connectionManager.refresh(subscriptions, users); // Dangle - }, [subscriptions, users]); + const handleNotification = async (subscriptionId, notification) => { + const added = await subscriptionManager.addNotification(subscriptionId, notification); + if (added) { + await subscriptionManager.notify(subscriptionId, notification); + } + }; + + const handleMessage = async (subscriptionId, message) => { + const subscription = await subscriptionManager.get(subscriptionId); + + // Race condition: sometimes the subscription is already unsubscribed from account + // sync before the message is handled + if (!subscription) { + return; + } + + if (subscription.internal) { + await handleInternalMessage(message); + } else { + await handleNotification(subscriptionId, message); + } + }; + + connectionManager.registerStateListener((id, state) => subscriptionManager.updateState(id, state)); + connectionManager.registerMessageListener(handleMessage); + + return () => { + connectionManager.resetStateListener(); + connectionManager.resetMessageListener(); + }; + }, + // We have to disable dep checking for "navigate". This is fine, it never changes. + + [] + ); + + // Sync topic listener: For accounts with sync_topic, subscribe to an internal topic + useEffect(() => { + if (!account || !account.sync_topic) { + return; + } + subscriptionManager.add(config.base_url, account.sync_topic, { internal: true }); // Dangle! + }, [account]); + + // When subscriptions or users change, refresh the connections + useEffect(() => { + connectionManager.refresh(wsSubscriptions, users); // Dangle + }, [wsSubscriptions, users]); }; /** @@ -83,35 +104,160 @@ export const useConnectionListeners = (account, subscriptions, users) => { * This will only be run once after the initial page load. */ export const useAutoSubscribe = (subscriptions, selected) => { - const [hasRun, setHasRun] = useState(false); - const params = useParams(); + const [hasRun, setHasRun] = useState(false); + const params = useParams(); - useEffect(() => { - const loaded = subscriptions !== null && subscriptions !== undefined; - if (!loaded || hasRun) { - return; + useEffect(() => { + const loaded = subscriptions !== null && subscriptions !== undefined; + if (!loaded || hasRun) { + return; + } + setHasRun(true); + const eligible = params.topic && !selected && !disallowedTopic(params.topic); + if (eligible) { + const baseUrl = params.baseUrl ? expandSecureUrl(params.baseUrl) : config.base_url; + console.log(`[Hooks] Auto-subscribing to ${topicUrl(baseUrl, params.topic)}`); + (async () => { + const subscription = await subscriptionManager.add(baseUrl, params.topic); + if (session.exists()) { + try { + await accountApi.addSubscription(baseUrl, params.topic); + } catch (e) { + console.log(`[Hooks] Auto-subscribing failed`, e); + if (e instanceof UnauthorizedError) { + await session.resetAndRedirect(routes.login); + } + } } - setHasRun(true); - const eligible = params.topic && !selected && !disallowedTopic(params.topic); - if (eligible) { - const baseUrl = (params.baseUrl) ? expandSecureUrl(params.baseUrl) : config.base_url; - console.log(`[Hooks] Auto-subscribing to ${topicUrl(baseUrl, params.topic)}`); - (async () => { - const subscription = await subscriptionManager.add(baseUrl, params.topic); - if (session.exists()) { - try { - await accountApi.addSubscription(baseUrl, params.topic); - } catch (e) { - console.log(`[Hooks] Auto-subscribing failed`, e); - if (e instanceof UnauthorizedError) { - session.resetAndRedirect(routes.login); - } - } - } - poller.pollInBackground(subscription); // Dangle! - })(); - } - }, [params, subscriptions, selected, hasRun]); + poller.pollInBackground(subscription); // Dangle! + })(); + } + }, [params, subscriptions, selected, hasRun]); +}; + +const webPushBroadcastChannel = new BroadcastChannel("web-push-broadcast"); + +/** + * Hook to return a value that's refreshed when the notification permission changes + */ +export const useNotificationPermissionListener = (query) => { + const [result, setResult] = useState(query()); + + useEffect(() => { + const handler = () => { + setResult(query()); + }; + + if ("permissions" in navigator) { + navigator.permissions.query({ name: "notifications" }).then((permission) => { + permission.addEventListener("change", handler); + + return () => { + permission.removeEventListener("change", handler); + }; + }); + } + }, []); + + return result; +}; + +/** + * Updates the Web Push subscriptions when the list of topics changes, + * as well as plays a sound when a new broadcast message is received from + * the service worker, since the service worker cannot play sounds. + */ +const useWebPushListener = (topics) => { + const [prevUpdate, setPrevUpdate] = useState(); + const pushPossible = useNotificationPermissionListener(() => notifier.pushPossible()); + + useEffect(() => { + const nextUpdate = JSON.stringify({ topics, pushPossible }); + if (topics === undefined || nextUpdate === prevUpdate) { + return; + } + + (async () => { + try { + console.log("[useWebPushListener] Refreshing web push subscriptions", topics); + await subscriptionManager.updateWebPushSubscriptions(topics); + setPrevUpdate(nextUpdate); + } catch (e) { + console.error("[useWebPushListener] Error refreshing web push subscriptions", e); + } + })(); + }, [topics, pushPossible, prevUpdate]); + + useEffect(() => { + const onMessage = () => { + notifier.playSound(); // Service Worker cannot play sound, so we do it here! + }; + + webPushBroadcastChannel.addEventListener("message", onMessage); + + return () => { + webPushBroadcastChannel.removeEventListener("message", onMessage); + }; + }); +}; + +/** + * Hook to return a list of Web Push enabled topics using a live query. This hook will return an empty list if + * permissions are not granted, or if the browser does not support Web Push. Notification permissions are acted upon + * automatically. + */ +export const useWebPushTopics = () => { + const pushPossible = useNotificationPermissionListener(() => notifier.pushPossible()); + + const topics = useLiveQuery( + async () => subscriptionManager.webPushTopics(pushPossible), + // invalidate (reload) query when these values change + [pushPossible] + ); + + useWebPushListener(topics); + + return topics; +}; + +const matchMedia = window.matchMedia("(display-mode: standalone)"); +const isIOSStandalone = window.navigator.standalone === true; + +/* + * Watches the "display-mode" to detect if the app is running as a standalone app (PWA). + */ +export const useIsLaunchedPWA = () => { + const [isStandalone, setIsStandalone] = useState(matchMedia.matches); + + useEffect(() => { + if (isIOSStandalone) { + return () => {}; // No need to listen for events on iOS + } + const handler = (evt) => { + console.log(`[useIsLaunchedPWA] App is now running ${evt.matches ? "standalone" : "in the browser"}`); + setIsStandalone(evt.matches); + }; + matchMedia.addEventListener("change", handler); + return () => { + matchMedia.removeEventListener("change", handler); + }; + }, []); + + return isIOSStandalone || isStandalone; +}; + +/** + * Watches the result of `useIsLaunchedPWA` and enables "Web Push" if it is. + */ +export const useStandaloneWebPushAutoSubscribe = () => { + const isLaunchedPWA = useIsLaunchedPWA(); + + useEffect(() => { + if (isLaunchedPWA) { + console.log(`[useStandaloneWebPushAutoSubscribe] Turning on web push automatically`); + prefs.setWebPushEnabled(true); // Dangle! + } + }, [isLaunchedPWA]); }; /** @@ -119,20 +265,39 @@ export const useAutoSubscribe = (subscriptions, selected) => { * and Poller.js, because side effect imports are not a thing in JS, and "Optimize imports" cleans * up "unused" imports. See https://github.com/binwiederhier/ntfy/issues/186. */ + +const startWorkers = () => { + poller.startWorker(); + pruner.startWorker(); + accountApi.startWorker(); +}; + +const stopWorkers = () => { + poller.stopWorker(); + pruner.stopWorker(); + accountApi.stopWorker(); +}; + export const useBackgroundProcesses = () => { - useEffect(() => { - poller.startWorker(); - pruner.startWorker(); - accountApi.startWorker(); - }, []); -} + useStandaloneWebPushAutoSubscribe(); + + useEffect(() => { + console.log("[useBackgroundProcesses] mounting"); + startWorkers(); + + return () => { + console.log("[useBackgroundProcesses] unloading"); + stopWorkers(); + }; + }, []); +}; export const useAccountListener = (setAccount) => { - useEffect(() => { - accountApi.registerListener(setAccount); - accountApi.sync(); // Dangle - return () => { - accountApi.resetListener(); - } - }, []); -} + useEffect(() => { + accountApi.registerListener(setAccount); + accountApi.sync(); // Dangle + return () => { + accountApi.resetListener(); + }; + }, []); +}; diff --git a/web/src/components/routes.js b/web/src/components/routes.js index d1db160a..17e0eac6 100644 --- a/web/src/components/routes.js +++ b/web/src/components/routes.js @@ -1,20 +1,20 @@ import config from "../app/config"; -import {shortUrl} from "../app/utils"; +import { shortUrl } from "../app/utils"; const routes = { - login: "/login", - signup: "/signup", - app: config.app_root, - account: "/account", - settings: "/settings", - subscription: "/:topic", - subscriptionExternal: "/:baseUrl/:topic", - forSubscription: (subscription) => { - if (subscription.baseUrl !== config.base_url) { - return `/${shortUrl(subscription.baseUrl)}/${subscription.topic}`; - } - return `/${subscription.topic}`; + login: "/login", + signup: "/signup", + app: config.app_root, + account: "/account", + settings: "/settings", + subscription: "/:topic", + subscriptionExternal: "/:baseUrl/:topic", + forSubscription: (subscription) => { + if (subscription.baseUrl !== config.base_url) { + return `/${shortUrl(subscription.baseUrl)}/${subscription.topic}`; } + return `/${subscription.topic}`; + }, }; export default routes; diff --git a/web/src/components/styles.js b/web/src/components/styles.js index d6127941..db0690bc 100644 --- a/web/src/components/styles.js +++ b/web/src/components/styles.js @@ -1,22 +1,19 @@ -import Typography from "@mui/material/Typography"; -import theme from "./theme"; -import Container from "@mui/material/Container"; -import {Backdrop, styled} from "@mui/material"; +import { Typography, Container, Backdrop, styled } from "@mui/material"; export const Paragraph = styled(Typography)({ paddingTop: 8, paddingBottom: 8, }); -export const VerticallyCenteredContainer = styled(Container)({ - display: 'flex', +export const VerticallyCenteredContainer = styled(Container)(({ theme }) => ({ + display: "flex", flexGrow: 1, - flexDirection: 'column', - justifyContent: 'center', - alignContent: 'center', - color: theme.palette.text.primary -}); + flexDirection: "column", + justifyContent: "center", + alignContent: "center", + color: theme.palette.text.primary, +})); export const LightboxBackdrop = styled(Backdrop)({ - backgroundColor: 'rgba(0, 0, 0, 0.8)' // was: 0.5 + backgroundColor: "rgba(0, 0, 0, 0.8)", // was: 0.5 }); diff --git a/web/src/components/theme.js b/web/src/components/theme.js index 3fdafae8..64217eee 100644 --- a/web/src/components/theme.js +++ b/web/src/components/theme.js @@ -1,36 +1,74 @@ -import { red } from '@mui/material/colors'; -import { createTheme } from '@mui/material/styles'; - -const theme = createTheme({ - palette: { - primary: { - main: '#338574', - }, - secondary: { - main: '#6cead0', - }, - error: { - main: red.A400, - }, - }, +/** @type {import("@mui/material").ThemeOptions} */ +const baseThemeOptions = { components: { MuiListItemIcon: { styleOverrides: { root: { - minWidth: '36px', + minWidth: "36px", }, }, }, MuiCardContent: { styleOverrides: { root: { - ':last-child': { - paddingBottom: '16px' - } - } - } - } + ":last-child": { + paddingBottom: "16px", + }, + }, + }, + }, }, -}); +}; -export default theme; +// https://github.com/binwiederhier/ntfy-android/blob/main/app/src/main/res/values/colors.xml + +/** @type {import("@mui/material").ThemeOptions} */ +export const lightTheme = { + ...baseThemeOptions, + components: { + ...baseThemeOptions.components, + }, + palette: { + mode: "light", + primary: { + main: "#338574", + }, + secondary: { + main: "#6cead0", + }, + error: { + main: "#c30000", + }, + }, +}; + +/** @type {import("@mui/material").ThemeOptions} */ +export const darkTheme = { + ...baseThemeOptions, + components: { + ...baseThemeOptions.components, + MuiSnackbarContent: { + styleOverrides: { + root: { + color: "#000", + backgroundColor: "#aeaeae", + }, + }, + }, + }, + palette: { + mode: "dark", + background: { + paper: "#1b2124", + }, + primary: { + main: "#65b5a3", + }, + secondary: { + main: "#6cead0", + }, + error: { + main: "#fe4d2e", + }, + }, +}; diff --git a/web/src/index.js b/web/src/index.js deleted file mode 100644 index 659bcb8f..00000000 --- a/web/src/index.js +++ /dev/null @@ -1,6 +0,0 @@ -import * as React from 'react'; -import { createRoot } from 'react-dom/client'; -import App from './components/App'; - -const root = createRoot(document.querySelector('#root')); -root.render(); diff --git a/web/src/index.jsx b/web/src/index.jsx new file mode 100644 index 00000000..1a123a8a --- /dev/null +++ b/web/src/index.jsx @@ -0,0 +1,9 @@ +import * as React from "react"; +import { createRoot } from "react-dom/client"; +import App from "./components/App"; +import registerSW from "./registerSW"; + +registerSW(); + +const root = createRoot(document.querySelector("#root")); +root.render(); diff --git a/web/src/registerSW.js b/web/src/registerSW.js new file mode 100644 index 00000000..adef4746 --- /dev/null +++ b/web/src/registerSW.js @@ -0,0 +1,31 @@ +// eslint-disable-next-line import/no-unresolved +import { registerSW as viteRegisterSW } from "virtual:pwa-register"; + +// fetch new sw every hour, i.e. update app every hour while running +const intervalMS = 60 * 60 * 1000; + +// https://vite-pwa-org.netlify.app/guide/periodic-sw-updates.html +const registerSW = () => + viteRegisterSW({ + onRegisteredSW(swUrl, registration) { + if (!registration) { + return; + } + + setInterval(async () => { + if (registration.installing || navigator?.onLine === false) return; + + const resp = await fetch(swUrl, { + cache: "no-store", + headers: { + cache: "no-store", + "cache-control": "no-cache", + }, + }); + + if (resp?.status === 200) await registration.update(); + }, intervalMS); + }, + }); + +export default registerSW; diff --git a/web/vite.config.js b/web/vite.config.js new file mode 100644 index 00000000..a4fd5a31 --- /dev/null +++ b/web/vite.config.js @@ -0,0 +1,60 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import { VitePWA } from "vite-plugin-pwa"; + +export default defineConfig(({ mode }) => ({ + build: { + outDir: "build", + assetsDir: "static/media", + sourcemap: true, + }, + server: { + port: 3000, + }, + plugins: [ + react(), + VitePWA({ + registerType: "autoUpdate", + // see registerSW.js imported by index.jsx + injectRegister: null, + strategies: "injectManifest", + devOptions: { + enabled: true, + /* when using generateSW the PWA plugin will switch to classic */ + type: "module", + navigateFallback: "index.html", + }, + injectManifest: { + globPatterns: ["**/*.{js,css,html,ico,png,svg,json}"], + globIgnores: ["config.js"], + manifestTransforms: [ + (entries) => ({ + manifest: entries.map((entry) => + // this matches the build step in the Makefile. + // since ntfy needs the ability to serve another page on /index.html, + // it's renamed and served from server.go as app.html as well. + entry.url === "index.html" + ? { + ...entry, + url: "app.html", + } + : entry + ), + }), + ], + }, + // The actual prod manifest is served from the go server, see server.go handleWebManifest. + manifest: mode === "development" && { + theme_color: "#317f6f", + icons: [ + { + src: "/static/images/pwa-192x192.png", + sizes: "192x192", + type: "image/png", + }, + ], + }, + }), + ], +}));