commit 1af2e64aae6f8ade6137eb8ae1618bc55863109a Author: Your Name Date: Thu Nov 13 21:00:23 2025 -0600 Add Cloudron packaging for Maubot diff --git a/maubot-src/.dockerignore b/maubot-src/.dockerignore new file mode 100644 index 0000000..546a6c6 --- /dev/null +++ b/maubot-src/.dockerignore @@ -0,0 +1,5 @@ +.editorconfig +.codeclimate.yml +*.png +.venv +maubot/management/frontend/node_modules diff --git a/maubot-src/.editorconfig b/maubot-src/.editorconfig new file mode 100644 index 0000000..3d6370d --- /dev/null +++ b/maubot-src/.editorconfig @@ -0,0 +1,19 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +max_line_length = 99 + +[*.json] +indent_size = 2 + +[spec.yaml] +indent_size = 2 + +[CHANGELOG.md] +max_line_length = 80 diff --git a/maubot-src/.github/workflows/python-lint.yml b/maubot-src/.github/workflows/python-lint.yml new file mode 100644 index 0000000..28d6df2 --- /dev/null +++ b/maubot-src/.github/workflows/python-lint.yml @@ -0,0 +1,26 @@ +name: Python lint + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + - uses: isort/isort-action@master + with: + sortPaths: "./maubot" + - uses: psf/black@stable + with: + src: "./maubot" + version: "24.10.0" + - name: pre-commit + run: | + pip install pre-commit + pre-commit run -av trailing-whitespace + pre-commit run -av end-of-file-fixer + pre-commit run -av check-yaml + pre-commit run -av check-added-large-files diff --git a/maubot-src/.gitignore b/maubot-src/.gitignore new file mode 100644 index 0000000..9fd28ef --- /dev/null +++ b/maubot-src/.gitignore @@ -0,0 +1,19 @@ +build/ +dist/ +*.egg-info + +.venv +pip-selfcheck.json +*.pyc +__pycache__ + +*.db* +*.log +/*.yaml +!example-config.yaml +!.pre-commit-config.yaml + +/start +logs/ +plugins/ +trash/ diff --git a/maubot-src/.gitlab-ci-plugin.yml b/maubot-src/.gitlab-ci-plugin.yml new file mode 100644 index 0000000..45ef06b --- /dev/null +++ b/maubot-src/.gitlab-ci-plugin.yml @@ -0,0 +1,29 @@ +image: dock.mau.dev/maubot/maubot + +stages: +- build + +variables: + PYTHONPATH: /opt/maubot + +build: + stage: build + except: + - tags + script: + - python3 -m maubot.cli build -o xyz.maubot.$CI_PROJECT_NAME-$CI_COMMIT_REF_NAME-$CI_COMMIT_SHORT_SHA.mbp + artifacts: + paths: + - "*.mbp" + expire_in: 365 days + +build tags: + stage: build + only: + - tags + script: + - python3 -m maubot.cli build -o xyz.maubot.$CI_PROJECT_NAME-$CI_COMMIT_TAG.mbp + artifacts: + paths: + - "*.mbp" + expire_in: never diff --git a/maubot-src/.gitlab-ci.yml b/maubot-src/.gitlab-ci.yml new file mode 100644 index 0000000..0671528 --- /dev/null +++ b/maubot-src/.gitlab-ci.yml @@ -0,0 +1,86 @@ +image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/docker:latest + +stages: +- build frontend +- build +- manifest + +default: + before_script: + - docker login -u "$CI_DEPENDENCY_PROXY_USER" -p "$CI_DEPENDENCY_PROXY_PASSWORD" "$CI_DEPENDENCY_PROXY_SERVER" + - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY" + +build frontend: + image: node:24-alpine + stage: build frontend + before_script: [] + variables: + NODE_ENV: "production" + cache: + paths: + - maubot/management/frontend/node_modules + script: + - cd maubot/management/frontend + - yarn --prod + - yarn build + - mv build ../../../frontend + artifacts: + paths: + - frontend + expire_in: 1 hour + +build amd64: + stage: build + tags: + - amd64 + script: + - echo maubot/management/frontend >> .dockerignore + - docker pull $CI_REGISTRY_IMAGE:latest || true + - | + docker build \ + --pull --cache-from $CI_REGISTRY_IMAGE:latest \ + --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 \ + --build-arg DOCKER_HUB=$CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX \ + . -f Dockerfile.ci + - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 + - docker rmi $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 + +build arm64: + stage: build + tags: + - arm64 + script: + - echo maubot/management/frontend >> .dockerignore + - docker pull $CI_REGISTRY_IMAGE:latest || true + - | + docker build \ + --pull --cache-from $CI_REGISTRY_IMAGE:latest \ + --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64 \ + --build-arg DOCKER_HUB=$CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX \ + . -f Dockerfile.ci + - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64 + - docker rmi $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64 + +manifest: + stage: manifest + before_script: + - "mkdir -p $HOME/.docker && echo '{\"experimental\": \"enabled\"}' > $HOME/.docker/config.json" + - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY" + script: + - docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 + - docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64 + - if [ "$CI_COMMIT_BRANCH" = "master" ]; then docker manifest create $CI_REGISTRY_IMAGE:latest $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64 && docker manifest push $CI_REGISTRY_IMAGE:latest; fi + - if [ "$CI_COMMIT_BRANCH" != "master" ]; then docker manifest create $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64 && docker manifest push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME; fi + - docker rmi $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64 + + +build standalone amd64: + stage: build + tags: + - amd64 + script: + - docker pull $CI_REGISTRY_IMAGE:standalone || true + - docker build --pull --cache-from $CI_REGISTRY_IMAGE:standalone --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-standalone . -f maubot/standalone/Dockerfile + - if [ "$CI_COMMIT_BRANCH" = "master" ]; then docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-standalone $CI_REGISTRY_IMAGE:standalone && docker push $CI_REGISTRY_IMAGE:standalone; fi + - if [ "$CI_COMMIT_BRANCH" != "master" ]; then docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-standalone $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME-standalone && docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME-standalone; fi + - docker rmi $CI_REGISTRY_IMAGE:standalone $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME-standalone $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-standalone || true diff --git a/maubot-src/.pre-commit-config.yaml b/maubot-src/.pre-commit-config.yaml new file mode 100644 index 0000000..4a6328e --- /dev/null +++ b/maubot-src/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + exclude_types: [markdown] + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - repo: https://github.com/psf/black + rev: 24.10.0 + hooks: + - id: black + language_version: python3 + files: ^maubot/.*\.pyi?$ + - repo: https://github.com/PyCQA/isort + rev: 5.13.2 + hooks: + - id: isort + files: ^maubot/.*\.pyi?$ diff --git a/maubot-src/CHANGELOG.md b/maubot-src/CHANGELOG.md new file mode 100644 index 0000000..001df37 --- /dev/null +++ b/maubot-src/CHANGELOG.md @@ -0,0 +1,171 @@ +# v0.6.0 (unreleased) + +* Added support for room v12 creator power when checking power levels after + following a tombstone. +* Added support for verifying the maubot device using a recovery key. +* Improved cutting off long plaintext body when parsing markdown. + +# v0.5.2 (2025-05-05) + +* Improved tombstone handling to ensure that the tombstone sender has + permissions to invite users to the target room. +* Fixed autojoin and online flags not being applied if set during client + creation (thanks to [@bnsh] in [#258]). +* Fixed plugin web apps not being cleared properly when unloading plugins. + +[@bnsh]: https://github.com/bnsh +[#258]: https://github.com/maubot/maubot/pull/258 + +# v0.5.1 (2025-01-03) + +* Updated Docker image to Alpine 3.21. +* Updated media upload/download endpoints in management frontend + (thanks to [@domrim] in [#253]). +* Fixed plugin web app base path not including a trailing slash + (thanks to [@jkhsjdhjs] in [#240]). +* Changed markdown parsing to cut off plaintext body if necessary to allow + longer formatted messages. +* Updated dependencies to fix Python 3.13 compatibility. + +[@domrim]: https://github.com/domrim +[@jkhsjdhjs]: https://github.com/jkhsjdhjs +[#253]: https://github.com/maubot/maubot/pull/253 +[#240]: https://github.com/maubot/maubot/pull/240 + +# v0.5.0 (2024-08-24) + +* Dropped Python 3.9 support. +* Updated Docker image to Alpine 3.20. +* Updated mautrix-python to 0.20.6 to support authenticated media. +* Removed hard dependency on SQLAlchemy. +* Fixed `main_class` to default to being loaded from the last module instead of + the first if a module name is not explicitly specified. + * This was already the [documented behavior](https://docs.mau.fi/maubot/dev/reference/plugin-metadata.html), + and loading from the first module doesn't make sense due to import order. +* Added simple scheduler utility for running background tasks periodically or + after a certain delay. +* Added testing framework for plugins (thanks to [@abompard] in [#225]). +* Changed `mbc build` to ignore directories declared in `modules` that are + missing an `__init__.py` file. + * Importing the modules at runtime would fail and break the plugin. + To include non-code resources outside modules in the mbp archive, + use `extra_files` instead. + +[#225]: https://github.com/maubot/maubot/issues/225 +[@abompard]: https://github.com/abompard + +# v0.4.2 (2023-09-20) + +* Updated Pillow to 10.0.1. +* Updated Docker image to Alpine 3.18. +* Added logging for errors for /whoami errors when adding new bot accounts. +* Added support for using appservice tokens (including appservice encryption) + in standalone mode. + +# v0.4.1 (2023-03-15) + +* Added `in_thread` parameter to `evt.reply()` and `evt.respond()`. + * By default, responses will go to the thread if the command is in a thread. + * By setting the flag to `True` or `False`, the plugin can force the response + to either be or not be in a thread. +* Fixed static files like the frontend app manifest not being served correctly. +* Fixed `self.loader.meta` not being available to plugins in standalone mode. +* Updated to mautrix-python v0.19.6. + +# v0.4.0 (2023-01-29) + +* Dropped support for using a custom maubot API base path. + * The public URL can still have a path prefix, e.g. when using a reverse + proxy. Both the web interface and `mbc` CLI tool should work fine with + custom prefixes. +* Added `evt.redact()` as a shortcut for `self.client.redact(evt.room_id, evt.event_id)`. +* Fixed `mbc logs` command not working on Python 3.8+. +* Fixed saving plugin configs (broke in v0.3.0). +* Fixed SSO login using the wrong API path (probably broke in v0.3.0). +* Stopped using `cd` in the docker image's `mbc` wrapper to enable using + path-dependent commands like `mbc build` by mounting a directory. +* Updated Docker image to Alpine 3.17. + +# v0.3.1 (2022-03-29) + +* Added encryption dependencies to standalone dockerfile. +* Fixed running without encryption dependencies installed. +* Removed unnecessary imports that broke on SQLAlchemy 1.4+. +* Removed unused alembic dependency. + +# v0.3.0 (2022-03-28) + +* Dropped Python 3.7 support. +* Switched main maubot database to asyncpg/aiosqlite. + * Using the same SQLite database for crypto is now safe again. +* Added support for asyncpg/aiosqlite for plugin databases. + * There are some [basic docs](https://docs.mau.fi/maubot/dev/database/index.html) + and [a simple example](./examples/database) for the new system. + * The old SQLAlchemy system is now deprecated, but will be preserved for + backwards-compatibility until most plugins have updated. +* Started enforcing minimum maubot version in plugins. + * Trying to upload a plugin where the specified version is higher than the + running maubot version will fail. +* Fixed bug where uploading a plugin twice, deleting it and trying to upload + again would fail. +* Updated Docker image to Alpine 3.15. +* Formatted all code using [black](https://github.com/psf/black) + and [isort](https://github.com/PyCQA/isort). + +# v0.2.1 (2021-11-22) + +Docker-only release: added automatic moving of plugin databases from +`/data/plugins/*.db` to `/data/dbs` + +# v0.2.0 (2021-11-20) + +* Moved plugin databases from `/data/plugins` to `/data/dbs` in the docker image. + * v0.2.0 was missing the automatic migration of databases, it was added in v0.2.1. + * If you were using a custom path, you'll have to mount it at `/data/dbs` or + move the databases yourself. +* Removed support for pickle crypto store and added support for SQLite crypto store. + * **If you were previously using the dangerous pickle store for e2ee, you'll + have to re-login with the bots (which can now be done conveniently with + `mbc auth --update-client`).** +* Added SSO support to `mbc auth`. +* Added support for setting device ID for e2ee using the web interface. +* Added e2ee fingerprint field to the web interface. +* Added `--update-client` flag to store access token inside maubot instead of + returning it in `mbc auth`. + * This will also automatically store the device ID now. +* Updated standalone mode. + * Added e2ee and web server support. + * It's now officially supported and [somewhat documented](https://docs.mau.fi/maubot/usage/standalone.html). +* Replaced `_` with `-` when generating command name from function name. +* Replaced unmaintained PyInquirer dependency with questionary + (thanks to [@TinfoilSubmarine] in [#139]). +* Updated Docker image to Alpine 3.14. +* Fixed avatar URLs without the `mxc://` prefix appearing like they work in the + frontend, but not actually working when saved. + +[@TinfoilSubmarine]: https://github.com/TinfoilSubmarine +[#139]: https://github.com/maubot/maubot/pull/139 + +# v0.1.2 (2021-06-12) + +* Added `loader` instance property for plugins to allow reading files within + the plugin archive. +* Added support for reloading `webapp` and `database` meta flags in plugins. + Previously you had to restart maubot instead of just reloading the plugin + when enabling the webapp or database for the first time. +* Added warning log if a plugin uses `@web` decorators without enabling the + `webapp` meta flag. +* Updated frontend to latest React and dependency versions. +* Updated Docker image to Alpine 3.13. +* Fixed registering accounts with Synapse shared secret registration. +* Fixed plugins using `get_event` in encrypted rooms. +* Fixed using the `@command.new` decorator without specifying a name + (i.e. falling back to the function name). + +# v0.1.1 (2021-05-02) + +No changelog. + +# v0.1.0 (2020-10-04) + +Initial tagged release. diff --git a/maubot-src/CLOUDRON.md b/maubot-src/CLOUDRON.md new file mode 100644 index 0000000..d19ea77 --- /dev/null +++ b/maubot-src/CLOUDRON.md @@ -0,0 +1,74 @@ +# Maubot on Cloudron + +This package turns the upstream maubot repository into a Cloudron-ready app that ships with PostgreSQL, Cloudron SSO protection, and persistent storage under `/app/data`. + +## Prerequisites +- Cloudron CLI `>=7.5` +- Access to the Cloudron build service at `builder.docker.due.ren` +- Repository checked out with the Cloudron packaging files (`CloudronManifest.json`, `Dockerfile`, `start.sh`, etc.) +- Builder token `e3265de06b1d0e7bb38400539012a8433a74c2c96a17955e` + +## Build +Run all commands from the repository root so the Cloudron CLI picks up the Cloudron-specific `Dockerfile`. + +```bash +cloudron build \ + --set-build-service builder.docker.due.ren \ + --build-service-token e3265de06b1d0e7bb38400539012a8433a74c2c96a17955e \ + --set-repository andreasdueren/ente-cloudron \ + --tag 0.1.0 +``` + +This command uploads the build context to the remote builder, produces the Docker image `andreasdueren/ente-cloudron:0.1.0`, and keeps the build logs in your terminal. Do not wait more than 30 seconds after the build finishes before continuing to installation. + +## Deployment +Install a fresh instance every time (do not use `cloudron update` during development): + +```bash +cloudron install \ + --location ente.due.ren \ + --image andreasdueren/ente-cloudron:0.1.0 +``` + +Once the dashboard reports the app as running, open `https://ente.due.ren` in your browser. Authentication is enforced via Cloudron’s OIDC flow (SSO). The maubot UI additionally requires its own credentials: + +- Username: `root` +- Password: stored in `/app/data/.admin_password` + +Retrieve the generated password once via: + +```bash +cloudron exec --app ente.due.ren -- cat /app/data/.admin_password +``` + +## Testing +- Follow logs for both supervisor-managed processes: + ```bash + cloudron logs --app ente.due.ren -f + ``` +- Verify the health endpoint exposed by nginx: + ```bash + cloudron exec --app ente.due.ren -- curl -f http://127.0.0.1:3000/healthz + ``` +- Ensure database connectivity by checking the maubot UI (Settings → Databases) or by verifying that `/app/data/maubot.db` stays empty when PostgreSQL is active. + +## Troubleshooting +- **Install hangs:** The Cloudron dashboard shows progress in real time. If it stalls for more than 30 seconds, check `cloudron logs --app ente.due.ren` for supervisor or nginx errors. +- **SSO loops:** Confirm the app’s location matches the configured Cloudron origin and that `CLOUDRON_APP_ORIGIN` is reaching the container (visible in `/app/data/config.yaml` under `server.public_url`). +- **Admin password lost:** Re-run the `cloudron exec ... cat /app/data/.admin_password` command. The file is persistent and backed up with the app’s data volume. +- **Custom configuration:** Edit `/app/data/config.yaml`, then restart the app: `cloudron restart --app ente.due.ren`. The start script preserves existing values but will continue to enforce Cloudron-specific paths. + +## Configuration Examples +- Switch the homeserver list: + ```bash + cloudron exec --app ente.due.ren -- \ + yq -i '.homeservers."matrix.example".url = "https://matrix.example.org"' /app/data/config.yaml + ``` +- Override the crypto database (e.g., to stick with SQLite): + ```bash + cloudron exec --app ente.due.ren -- \ + yq -i '.crypto_database = "sqlite:/app/data/crypto.db"' /app/data/config.yaml + ``` +- Plugin addons: the start script reads `CLOUDRON_POSTGRESQL_URL`, `CLOUDRON_MAIL_SMTP_*`, `CLOUDRON_OIDC_*`, and `CLOUDRON_LDAP_*` on each boot, and plugins can directly use `CLOUDRON_MYSQL_URL`, `CLOUDRON_MONGODB_URL`, or `CLOUDRON_REDIS_URL`. Inject overrides via the Cloudron dashboard’s **Environment** UI if specific plugins require external services. + +After making any manual change, always restart the app to allow the supervisor to pick up the modifications. diff --git a/maubot-src/CloudronManifest.json b/maubot-src/CloudronManifest.json new file mode 100644 index 0000000..92d5516 --- /dev/null +++ b/maubot-src/CloudronManifest.json @@ -0,0 +1,45 @@ +{ + "id": "ren.due.maubot", + "title": "Maubot", + "author": "Maubot Contributors", + "description": "Plugin-based Matrix bot framework with web UI and management API.", + "website": "https://docs.mau.fi/maubot/", + "contactEmail": "tulir@maunium.net", + "version": "0.6.0b1-1", + "appVersion": "0.6.0b1", + "manifestVersion": 2, + "minBoxVersion": "7.0.0", + "httpPort": 3000, + "healthCheckPath": "/healthz", + "memoryLimit": 536870912, + "addons": [ + "postgresql", + "mysql", + "mongodb", + "redis", + "localstorage", + "sendmail" + ], + "auth": { + "type": "oidc" + }, + "changelog": [], + "tags": [ + "matrix", + "bot", + "maubot" + ], + "postInstallMessage": "The Maubot UI is protected by Cloudron SSO. The initial API/UI admin credentials are stored in /app/data/.admin_password (username: root). Retrieve them with `cloudron exec --app -- cat /app/data/.admin_password` before first login.", + "volumes": [ + { + "name": "data", + "path": "/app/data" + } + ], + "tcpPorts": [], + "capabilities": [], + "icon": "", + "mediaLinks": [], + "displayName": "Maubot", + "tagsLocalized": {} +} diff --git a/maubot-src/Dockerfile b/maubot-src/Dockerfile new file mode 100644 index 0000000..43eddbb --- /dev/null +++ b/maubot-src/Dockerfile @@ -0,0 +1,59 @@ +FROM node:20-bullseye AS frontend-builder + +WORKDIR /build/frontend +COPY maubot/management/frontend/package.json maubot/management/frontend/yarn.lock ./ +RUN corepack enable && yarn install --frozen-lockfile +COPY maubot/management/frontend/ ./ +RUN yarn build + +FROM cloudron/base:5.0.0 + +ENV APP_DIR=/app/code \ + DATA_DIR=/app/data \ + VENV_DIR=/app/code/venv \ + PYTHONUNBUFFERED=1 \ + DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 python3-dev python3-venv python3-pip python3-wheel \ + build-essential git \ + libffi-dev libssl-dev libjpeg-dev zlib1g-dev libpq-dev \ + libmagic1 libmagic-dev libxml2-dev libxslt1-dev \ + libolm3 libolm-dev \ + libldap2-dev libsasl2-dev \ + nginx supervisor gosu ca-certificates curl \ + && rm -rf /var/lib/apt/lists/* + +RUN mkdir -p ${APP_DIR} ${DATA_DIR} /run/nginx /tmp/data && chown cloudron:cloudron ${DATA_DIR} + +WORKDIR ${APP_DIR} +COPY . ${APP_DIR} + +RUN python3 -m venv ${VENV_DIR} \ + && ${VENV_DIR}/bin/pip install --upgrade pip setuptools wheel \ + && ${VENV_DIR}/bin/pip install --no-cache-dir \ + -r requirements.txt \ + -r optional-requirements.txt \ + dateparser \ + langdetect \ + python-gitlab \ + pyquery \ + tzlocal \ + pillow \ + python-magic \ + feedparser \ + python-dateutil \ + lxml \ + semver + +COPY --from=frontend-builder /build/frontend/build /app/code/frontend + +RUN cp /app/code/cloudron/default-config.yaml /tmp/data/config.yaml \ + && rm -f /etc/nginx/sites-enabled/default \ + && chmod 755 /app/code/start.sh + +ENV PATH=${VENV_DIR}/bin:$PATH + +VOLUME ["/app/data"] + +CMD ["/app/code/start.sh"] diff --git a/maubot-src/Dockerfile.ci b/maubot-src/Dockerfile.ci new file mode 100644 index 0000000..5b8dd0d --- /dev/null +++ b/maubot-src/Dockerfile.ci @@ -0,0 +1,57 @@ +ARG DOCKER_HUB="docker.io" + +FROM ${DOCKER_HUB}/alpine:3.22 + +RUN apk add --no-cache \ + python3 py3-pip py3-setuptools py3-wheel \ + ca-certificates \ + su-exec \ + yq \ + py3-aiohttp \ + py3-attrs \ + py3-bcrypt \ + py3-cffi \ + py3-ruamel.yaml \ + py3-jinja2 \ + py3-click \ + py3-packaging \ + py3-markdown \ + py3-alembic \ +# py3-cssselect \ + py3-commonmark \ + py3-pygments \ + py3-tz \ +# py3-tzlocal \ + py3-regex \ + py3-wcwidth \ + # encryption + py3-cffi \ + py3-olm \ + py3-pycryptodome \ + py3-unpaddedbase64 \ + py3-future \ + # plugin deps + py3-pillow \ + py3-magic \ + py3-feedparser \ + py3-lxml +# py3-gitlab +# py3-semver +# TODO remove pillow, magic, feedparser, lxml, gitlab and semver when maubot supports installing dependencies + +COPY requirements.txt /opt/maubot/requirements.txt +COPY optional-requirements.txt /opt/maubot/optional-requirements.txt +WORKDIR /opt/maubot +RUN apk add --virtual .build-deps python3-dev build-base git \ + && pip3 install --break-system-packages -r requirements.txt -r optional-requirements.txt \ + dateparser langdetect python-gitlab pyquery semver tzlocal cssselect \ + && apk del .build-deps +# TODO also remove dateparser, langdetect and pyquery when maubot supports installing dependencies + +COPY . /opt/maubot +RUN cp /opt/maubot/maubot/example-config.yaml /opt/maubot +COPY ./docker/mbc.sh /usr/local/bin/mbc +ENV UID=1337 GID=1337 XDG_CONFIG_HOME=/data +VOLUME /data + +CMD ["/opt/maubot/docker/run.sh"] diff --git a/maubot-src/LICENSE b/maubot-src/LICENSE new file mode 100644 index 0000000..be3f7b2 --- /dev/null +++ b/maubot-src/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/maubot-src/MANIFEST.in b/maubot-src/MANIFEST.in new file mode 100644 index 0000000..d8889bc --- /dev/null +++ b/maubot-src/MANIFEST.in @@ -0,0 +1,5 @@ +include README.md +include CHANGELOG.md +include LICENSE +include requirements.txt +include optional-requirements.txt diff --git a/maubot-src/README.md b/maubot-src/README.md new file mode 100644 index 0000000..02a4b6f --- /dev/null +++ b/maubot-src/README.md @@ -0,0 +1,29 @@ +# maubot +![Languages](https://img.shields.io/github/languages/top/maubot/maubot.svg) +[![License](https://img.shields.io/github/license/maubot/maubot.svg)](LICENSE) +[![Release](https://img.shields.io/github/release/maubot/maubot/all.svg)](https://github.com/maubot/maubot/releases) +[![GitLab CI](https://mau.dev/maubot/maubot/badges/master/pipeline.svg)](https://mau.dev/maubot/maubot/container_registry) +[![Code style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![Imports](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/) + +A plugin-based [Matrix](https://matrix.org) bot system written in Python. + +## Documentation + +All setup and usage instructions are located on +[docs.mau.fi](https://docs.mau.fi/maubot/index.html). Some quick links: + +* [Setup](https://docs.mau.fi/maubot/usage/setup/index.html) + (or [with Docker](https://docs.mau.fi/maubot/usage/setup/docker.html)) +* [Basic usage](https://docs.mau.fi/maubot/usage/basic.html) +* [Encryption](https://docs.mau.fi/maubot/usage/encryption.html) + +## Discussion +Matrix room: [#maubot:maunium.net](https://matrix.to/#/#maubot:maunium.net) + +## Plugins +A list of plugins can be found at [plugins.mau.bot](https://plugins.mau.bot/). + +To add your plugin to the list, send a pull request to . + +The plugin wishlist lives at . diff --git a/maubot-src/cloudron/default-config.yaml b/maubot-src/cloudron/default-config.yaml new file mode 100644 index 0000000..6bc6c69 --- /dev/null +++ b/maubot-src/cloudron/default-config.yaml @@ -0,0 +1,75 @@ +database: sqlite:/app/data/maubot.db +crypto_database: default + +database_opts: + min_size: 1 + max_size: 10 + +plugin_directories: + upload: /app/data/plugins + load: + - /app/data/plugins + trash: /app/data/trash + +plugin_databases: + sqlite: /app/data/dbs + postgres: null + postgres_max_conns_per_plugin: 3 + postgres_opts: {} + +server: + hostname: 127.0.0.1 + port: 3001 + public_url: https://example.com + ui_base_path: / + plugin_base_path: /_matrix/maubot/plugin/ + override_resource_path: /app/code/frontend + unshared_secret: generate + +homeservers: {} + +admins: + root: "" + +api_features: + login: true + plugin: true + plugin_upload: true + instance: true + instance_database: true + client: true + client_proxy: true + client_auth: true + dev_open: true + log: true + +logging: + version: 1 + formatters: + colored: + (): maubot.lib.color_log.ColorFormatter + format: "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s" + normal: + format: "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s" + handlers: + file: + class: logging.handlers.RotatingFileHandler + formatter: normal + filename: /app/data/logs/maubot.log + maxBytes: 10485760 + backupCount: 10 + console: + class: logging.StreamHandler + formatter: colored + loggers: + maubot: + level: INFO + mau: + level: INFO + aiohttp: + level: INFO + root: + level: INFO + handlers: + - file + - console diff --git a/maubot-src/dev-requirements.txt b/maubot-src/dev-requirements.txt new file mode 100644 index 0000000..bb8c2a0 --- /dev/null +++ b/maubot-src/dev-requirements.txt @@ -0,0 +1,3 @@ +pre-commit>=2.10.1,<3 +isort>=5.10.1,<6 +black>=24,<25 diff --git a/maubot-src/docker/mbc.sh b/maubot-src/docker/mbc.sh new file mode 100755 index 0000000..5bde65a --- /dev/null +++ b/maubot-src/docker/mbc.sh @@ -0,0 +1,3 @@ +#!/bin/sh +export PYTHONPATH=/opt/maubot +python3 -m maubot.cli "$@" diff --git a/maubot-src/docker/run.sh b/maubot-src/docker/run.sh new file mode 100755 index 0000000..5447e27 --- /dev/null +++ b/maubot-src/docker/run.sh @@ -0,0 +1,46 @@ +#!/bin/sh + +function fixperms { + chown -R $UID:$GID /var/log /data +} + +function fixdefault { + _value=$(yq e "$1" /data/config.yaml) + if [[ "$_value" == "$2" ]]; then + yq e -i "$1 = "'"'"$3"'"' /data/config.yaml + fi +} + +function fixconfig { + # Change relative default paths to absolute paths in /data + fixdefault '.database' 'sqlite:maubot.db' 'sqlite:/data/maubot.db' + fixdefault '.plugin_directories.upload' './plugins' '/data/plugins' + fixdefault '.plugin_directories.load[0]' './plugins' '/data/plugins' + fixdefault '.plugin_directories.trash' './trash' '/data/trash' + fixdefault '.plugin_databases.sqlite' './plugins' '/data/dbs' + fixdefault '.plugin_databases.sqlite' './dbs' '/data/dbs' + fixdefault '.logging.handlers.file.filename' './maubot.log' '/var/log/maubot.log' + # This doesn't need to be configurable + yq e -i '.server.override_resource_path = "/opt/maubot/frontend"' /data/config.yaml +} + +cd /opt/maubot + +mkdir -p /var/log/maubot /data/plugins /data/trash /data/dbs + +if [ ! -f /data/config.yaml ]; then + cp example-config.yaml /data/config.yaml + echo "Config file not found. Example config copied to /data/config.yaml" + echo "Please modify the config file to your liking and restart the container." + fixperms + fixconfig + exit +fi + +fixperms +fixconfig +if ls /data/plugins/*.db > /dev/null 2>&1; then + mv -n /data/plugins/*.db /data/dbs/ +fi + +exec su-exec $UID:$GID python3 -m maubot -c /data/config.yaml diff --git a/maubot-src/examples/LICENSE b/maubot-src/examples/LICENSE new file mode 100644 index 0000000..a4b60f3 --- /dev/null +++ b/maubot-src/examples/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2022 Tulir Asokan + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/maubot-src/examples/README.md b/maubot-src/examples/README.md new file mode 100644 index 0000000..2efabca --- /dev/null +++ b/maubot-src/examples/README.md @@ -0,0 +1,7 @@ +# Maubot examples +All examples are published under the [MIT license](LICENSE). + +* [Hello World](helloworld/) - Very basic event handling bot that responds "Hello, World!" to all messages. +* [Echo bot](https://github.com/maubot/echo) - Basic command handling bot with !echo and !ping commands +* [Config example](config/) - Simple example of using a config file +* [Database example](database/) - Simple example of using a database diff --git a/maubot-src/examples/config/base-config.yaml b/maubot-src/examples/config/base-config.yaml new file mode 100644 index 0000000..0f7f8a3 --- /dev/null +++ b/maubot-src/examples/config/base-config.yaml @@ -0,0 +1,5 @@ +# Who is allowed to use the bot? +whitelist: + - "@user:example.com" +# The prefix for the main command without the ! +command_prefix: hello-world diff --git a/maubot-src/examples/config/configurablebot.py b/maubot-src/examples/config/configurablebot.py new file mode 100644 index 0000000..54b47b6 --- /dev/null +++ b/maubot-src/examples/config/configurablebot.py @@ -0,0 +1,27 @@ +from typing import Type +from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper +from maubot import Plugin, MessageEvent +from maubot.handlers import command + + +class Config(BaseProxyConfig): + def do_update(self, helper: ConfigUpdateHelper) -> None: + helper.copy("whitelist") + helper.copy("command_prefix") + + +class ConfigurableBot(Plugin): + async def start(self) -> None: + self.config.load_and_update() + + def get_command_name(self) -> str: + return self.config["command_prefix"] + + @command.new(name=get_command_name) + async def hmm(self, evt: MessageEvent) -> None: + if evt.sender in self.config["whitelist"]: + await evt.reply("You're whitelisted 🎉") + + @classmethod + def get_config_class(cls) -> Type[BaseProxyConfig]: + return Config diff --git a/maubot-src/examples/config/maubot.yaml b/maubot-src/examples/config/maubot.yaml new file mode 100644 index 0000000..8ab36a9 --- /dev/null +++ b/maubot-src/examples/config/maubot.yaml @@ -0,0 +1,13 @@ +maubot: 0.1.0 +id: xyz.maubot.configurablebot +version: 2.0.0 +license: MIT +modules: +- configurablebot +main_class: ConfigurableBot +database: false +config: true + +# Instruct the build tool to include the base config. +extra_files: +- base-config.yaml diff --git a/maubot-src/examples/database/maubot.yaml b/maubot-src/examples/database/maubot.yaml new file mode 100644 index 0000000..84f4f69 --- /dev/null +++ b/maubot-src/examples/database/maubot.yaml @@ -0,0 +1,10 @@ +maubot: 0.1.0 +id: xyz.maubot.storagebot +version: 2.0.0 +license: MIT +modules: +- storagebot +main_class: StorageBot +database: true +database_type: asyncpg +config: false diff --git a/maubot-src/examples/database/storagebot.py b/maubot-src/examples/database/storagebot.py new file mode 100644 index 0000000..786bba5 --- /dev/null +++ b/maubot-src/examples/database/storagebot.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +from mautrix.util.async_db import UpgradeTable, Connection +from maubot import Plugin, MessageEvent +from maubot.handlers import command + +upgrade_table = UpgradeTable() + + +@upgrade_table.register(description="Initial revision") +async def upgrade_v1(conn: Connection) -> None: + await conn.execute( + """CREATE TABLE stored_data ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + )""" + ) + + +@upgrade_table.register(description="Remember user who added value") +async def upgrade_v2(conn: Connection) -> None: + await conn.execute("ALTER TABLE stored_data ADD COLUMN creator TEXT") + + +class StorageBot(Plugin): + @command.new() + async def storage(self, evt: MessageEvent) -> None: + pass + + @storage.subcommand(help="Store a value") + @command.argument("key") + @command.argument("value", pass_raw=True) + async def put(self, evt: MessageEvent, key: str, value: str) -> None: + q = """ + INSERT INTO stored_data (key, value, creator) VALUES ($1, $2, $3) + ON CONFLICT (key) DO UPDATE SET value=excluded.value, creator=excluded.creator + """ + await self.database.execute(q, key, value, evt.sender) + await evt.reply(f"Inserted {key} into the database") + + @storage.subcommand(help="Get a value from the storage") + @command.argument("key") + async def get(self, evt: MessageEvent, key: str) -> None: + q = "SELECT key, value, creator FROM stored_data WHERE LOWER(key)=LOWER($1)" + row = await self.database.fetchrow(q, key) + if row: + key = row["key"] + value = row["value"] + creator = row["creator"] + await evt.reply(f"`{key}` stored by {creator}:\n\n```\n{value}\n```") + else: + await evt.reply(f"No data stored under `{key}` :(") + + @storage.subcommand(help="List keys in the storage") + @command.argument("prefix", required=False) + async def list(self, evt: MessageEvent, prefix: str | None) -> None: + q = "SELECT key, creator FROM stored_data WHERE key LIKE $1" + rows = await self.database.fetch(q, prefix + "%") + prefix_reply = f" starting with `{prefix}`" if prefix else "" + if len(rows) == 0: + await evt.reply(f"Nothing{prefix_reply} stored in database :(") + else: + formatted_data = "\n".join( + f"* `{row['key']}` stored by {row['creator']}" for row in rows + ) + await evt.reply( + f"Found {len(rows)} keys{prefix_reply} in database:\n\n{formatted_data}" + ) + + @classmethod + def get_db_upgrade_table(cls) -> UpgradeTable | None: + return upgrade_table diff --git a/maubot-src/examples/helloworld/helloworld.py b/maubot-src/examples/helloworld/helloworld.py new file mode 100644 index 0000000..90569e8 --- /dev/null +++ b/maubot-src/examples/helloworld/helloworld.py @@ -0,0 +1,10 @@ +from mautrix.types import EventType +from maubot import Plugin, MessageEvent +from maubot.handlers import event + + +class HelloWorldBot(Plugin): + @event.on(EventType.ROOM_MESSAGE) + async def handler(self, event: MessageEvent) -> None: + if event.sender != self.client.mxid: + await event.reply("Hello, World!") diff --git a/maubot-src/examples/helloworld/maubot.yaml b/maubot-src/examples/helloworld/maubot.yaml new file mode 100644 index 0000000..a4082bd --- /dev/null +++ b/maubot-src/examples/helloworld/maubot.yaml @@ -0,0 +1,43 @@ +# This is an example maubot plugin definition file. +# All plugins must include a file like this named "maubot.yaml" in their root directory. + +# Target maubot version +maubot: 0.1.0 + +# The unique ID for the plugin. Java package naming style. (i.e. use your own domain, not xyz.maubot) +id: xyz.maubot.helloworld + +# A PEP 440 compliant version string. +version: 1.0.0 + +# The SPDX license identifier for the plugin. https://spdx.org/licenses/ +# Optional, assumes all rights reserved if omitted. +license: MIT + +# The list of modules to load from the plugin archive. +# Modules can be directories with an __init__.py file or simply python files. +# Submodules that are imported by modules listed here don't need to be listed separately. +# However, top-level modules must always be listed even if they're imported by other modules. +modules: +- helloworld + +# The main class of the plugin. Format: module/Class +# If `module` is omitted, will default to last module specified in the module list. +# Even if `module` is not omitted here, it must be included in the modules list. +# The main class must extend maubot.Plugin +main_class: HelloWorldBot + +# Whether or not instances need a database +database: false + +# Extra files that the upcoming build tool should include in the mbp file. +#extra_files: +#- base-config.yaml +#- LICENSE + +# List of dependencies +#dependencies: +#- foo + +#soft_dependencies: +#- bar>=0.1 diff --git a/maubot-src/maubot/__init__.py b/maubot-src/maubot/__init__.py new file mode 100644 index 0000000..5106b46 --- /dev/null +++ b/maubot-src/maubot/__init__.py @@ -0,0 +1,4 @@ +from .__meta__ import __version__ +from .matrix import MaubotMatrixClient as Client, MaubotMessageEvent as MessageEvent +from .plugin_base import Plugin +from .plugin_server import PluginWebApp diff --git a/maubot-src/maubot/__main__.py b/maubot-src/maubot/__main__.py new file mode 100644 index 0000000..b19210a --- /dev/null +++ b/maubot-src/maubot/__main__.py @@ -0,0 +1,185 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +import asyncio +import sys + +from mautrix.api import HTTPAPI +from mautrix.util.async_db import Database, DatabaseException, PostgresDatabase, Scheme +from mautrix.util.program import Program + +from .__meta__ import __version__ +from .client import Client +from .config import Config +from .db import init as init_db, upgrade_table +from .instance import PluginInstance +from .lib.future_awaitable import FutureAwaitable +from .lib.state_store import PgStateStore +from .loader.zip import init as init_zip_loader +from .management.api import init as init_mgmt_api +from .server import MaubotServer + +try: + from mautrix.crypto.store import PgCryptoStore +except ImportError: + PgCryptoStore = None + + +class Maubot(Program): + config: Config + server: MaubotServer + db: Database + crypto_db: Database | None + plugin_postgres_db: PostgresDatabase | None + state_store: PgStateStore + + config_class = Config + module = "maubot" + name = "maubot" + version = __version__ + command = "python -m maubot" + description = "A plugin-based Matrix bot system." + + def prepare_log_websocket(self) -> None: + from .management.api.log import init, stop_all + + init(self.loop) + self.add_shutdown_actions(FutureAwaitable(stop_all)) + + def prepare_arg_parser(self) -> None: + super().prepare_arg_parser() + self.parser.add_argument( + "--ignore-unsupported-database", + action="store_true", + help="Run even if the database schema is too new", + ) + self.parser.add_argument( + "--ignore-foreign-tables", + action="store_true", + help="Run even if the database contains tables from other programs (like Synapse)", + ) + + def prepare_db(self) -> None: + self.db = Database.create( + self.config["database"], + upgrade_table=upgrade_table, + db_args=self.config["database_opts"], + owner_name=self.name, + ignore_foreign_tables=self.args.ignore_foreign_tables, + ) + init_db(self.db) + + if PgCryptoStore: + if self.config["crypto_database"] == "default": + self.crypto_db = self.db + else: + self.crypto_db = Database.create( + self.config["crypto_database"], + upgrade_table=PgCryptoStore.upgrade_table, + ignore_foreign_tables=self.args.ignore_foreign_tables, + ) + else: + self.crypto_db = None + + if self.config["plugin_databases.postgres"] == "default": + if self.db.scheme != Scheme.POSTGRES: + self.log.critical( + 'Using "default" as the postgres plugin database URL is only allowed if ' + "the default database is postgres." + ) + sys.exit(24) + assert isinstance(self.db, PostgresDatabase) + self.plugin_postgres_db = self.db + elif self.config["plugin_databases.postgres"]: + plugin_db = Database.create( + self.config["plugin_databases.postgres"], + db_args={ + **self.config["database_opts"], + **self.config["plugin_databases.postgres_opts"], + }, + ) + if plugin_db.scheme != Scheme.POSTGRES: + self.log.critical("The plugin postgres database URL must be a postgres database") + sys.exit(24) + assert isinstance(plugin_db, PostgresDatabase) + self.plugin_postgres_db = plugin_db + else: + self.plugin_postgres_db = None + + def prepare(self) -> None: + super().prepare() + + if self.config["api_features.log"]: + self.prepare_log_websocket() + + HTTPAPI.default_ua = f"maubot/{self.version} {HTTPAPI.default_ua}" + init_zip_loader(self.config) + self.prepare_db() + Client.init_cls(self) + PluginInstance.init_cls(self) + management_api = init_mgmt_api(self.config, self.loop) + self.server = MaubotServer(management_api, self.config, self.loop) + self.state_store = PgStateStore(self.db) + + async def start_db(self) -> None: + self.log.debug("Starting database...") + ignore_unsupported = self.args.ignore_unsupported_database + self.db.upgrade_table.allow_unsupported = ignore_unsupported + self.state_store.upgrade_table.allow_unsupported = ignore_unsupported + try: + await self.db.start() + await self.state_store.upgrade_table.upgrade(self.db) + if self.plugin_postgres_db and self.plugin_postgres_db is not self.db: + await self.plugin_postgres_db.start() + if self.crypto_db: + PgCryptoStore.upgrade_table.allow_unsupported = ignore_unsupported + if self.crypto_db is not self.db: + await self.crypto_db.start() + else: + await PgCryptoStore.upgrade_table.upgrade(self.db) + except DatabaseException as e: + self.log.critical("Failed to initialize database", exc_info=e) + if e.explanation: + self.log.info(e.explanation) + sys.exit(25) + + async def system_exit(self) -> None: + if hasattr(self, "db"): + self.log.trace("Stopping database due to SystemExit") + await self.db.stop() + + async def start(self) -> None: + await self.start_db() + await asyncio.gather(*[plugin.load() async for plugin in PluginInstance.all()]) + await asyncio.gather(*[client.start() async for client in Client.all()]) + await super().start() + async for plugin in PluginInstance.all(): + await plugin.load() + await self.server.start() + + async def stop(self) -> None: + self.add_shutdown_actions(*(client.stop() for client in Client.cache.values())) + await super().stop() + self.log.debug("Stopping server") + try: + await asyncio.wait_for(self.server.stop(), 5) + except asyncio.TimeoutError: + self.log.warning("Stopping server timed out") + await self.db.stop() + + +Maubot().run() diff --git a/maubot-src/maubot/__meta__.py b/maubot-src/maubot/__meta__.py new file mode 100644 index 0000000..5de1738 --- /dev/null +++ b/maubot-src/maubot/__meta__.py @@ -0,0 +1 @@ +__version__ = "0.6.0b1" diff --git a/maubot-src/maubot/cli/__init__.py b/maubot-src/maubot/cli/__init__.py new file mode 100644 index 0000000..d25736b --- /dev/null +++ b/maubot-src/maubot/cli/__init__.py @@ -0,0 +1,2 @@ +from . import commands +from .base import app diff --git a/maubot-src/maubot/cli/__main__.py b/maubot-src/maubot/cli/__main__.py new file mode 100644 index 0000000..3bdbe0e --- /dev/null +++ b/maubot-src/maubot/cli/__main__.py @@ -0,0 +1,3 @@ +from . import app + +app() diff --git a/maubot-src/maubot/cli/base.py b/maubot-src/maubot/cli/base.py new file mode 100644 index 0000000..b35db53 --- /dev/null +++ b/maubot-src/maubot/cli/base.py @@ -0,0 +1,23 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +import click + +from .config import load_config + + +@click.group() +def app() -> None: + load_config() diff --git a/maubot-src/maubot/cli/cliq/__init__.py b/maubot-src/maubot/cli/cliq/__init__.py new file mode 100644 index 0000000..10ede9f --- /dev/null +++ b/maubot-src/maubot/cli/cliq/__init__.py @@ -0,0 +1,2 @@ +from .cliq import command, option +from .validators import PathValidator, SPDXValidator, VersionValidator diff --git a/maubot-src/maubot/cli/cliq/cliq.py b/maubot-src/maubot/cli/cliq/cliq.py new file mode 100644 index 0000000..2883441 --- /dev/null +++ b/maubot-src/maubot/cli/cliq/cliq.py @@ -0,0 +1,169 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +from typing import Any, Callable +import asyncio +import functools +import inspect +import traceback + +from colorama import Fore +from prompt_toolkit.validation import Validator +from questionary import prompt +import aiohttp +import click + +from ..base import app +from ..config import get_token +from .validators import ClickValidator, Required + + +def with_http(func): + @functools.wraps(func) + async def wrapper(*args, **kwargs): + async with aiohttp.ClientSession() as sess: + try: + return await func(*args, sess=sess, **kwargs) + except aiohttp.ClientError as e: + print(f"{Fore.RED}Connection error: {e}{Fore.RESET}") + + return wrapper + + +def with_authenticated_http(func): + @functools.wraps(func) + async def wrapper(*args, server: str, **kwargs): + server, token = get_token(server) + if not token: + return + async with aiohttp.ClientSession(headers={"Authorization": f"Bearer {token}"}) as sess: + try: + return await func(*args, sess=sess, server=server, **kwargs) + except aiohttp.ClientError as e: + print(f"{Fore.RED}Connection error: {e}{Fore.RESET}") + + return wrapper + + +def command(help: str) -> Callable[[Callable], Callable]: + def decorator(func) -> Callable: + questions = getattr(func, "__inquirer_questions__", {}).copy() + + @functools.wraps(func) + def wrapper(*args, **kwargs): + for key, value in kwargs.items(): + if key not in questions: + continue + if value is not None and (questions[key]["type"] != "confirm" or value != "null"): + questions.pop(key, None) + try: + required_unless = questions[key].pop("required_unless") + if isinstance(required_unless, str) and kwargs[required_unless]: + questions.pop(key) + elif isinstance(required_unless, list): + for v in required_unless: + if kwargs[v]: + questions.pop(key) + break + elif isinstance(required_unless, dict): + for k, v in required_unless.items(): + if kwargs.get(v, object()) == v: + questions.pop(key) + break + except KeyError: + pass + question_list = list(questions.values()) + question_list.reverse() + resp = prompt(question_list, kbi_msg="Aborted!") + if not resp and question_list: + return + kwargs = {**kwargs, **resp} + + try: + res = func(*args, **kwargs) + if inspect.isawaitable(res): + asyncio.run(res) + except Exception: + print(Fore.RED + "Fatal error running command" + Fore.RESET) + traceback.print_exc() + + return app.command(help=help)(wrapper) + + return decorator + + +def yesno(val: str) -> bool | None: + if not val: + return None + elif isinstance(val, bool): + return val + elif val.lower() in ("true", "t", "yes", "y"): + return True + elif val.lower() in ("false", "f", "no", "n"): + return False + + +yesno.__name__ = "yes/no" + + +def option( + short: str, + long: str, + message: str = None, + help: str = None, + click_type: str | Callable[[str], Any] = None, + inq_type: str = None, + validator: type[Validator] = None, + required: bool = False, + default: str | bool | None = None, + is_flag: bool = False, + prompt: bool = True, + required_unless: str | list | dict = None, +) -> Callable[[Callable], Callable]: + if not message: + message = long[2].upper() + long[3:] + + if isinstance(validator, type) and issubclass(validator, ClickValidator): + click_type = validator.click_type + if is_flag: + click_type = yesno + + def decorator(func) -> Callable: + click.option(short, long, help=help, type=click_type)(func) + if not prompt: + return func + if not hasattr(func, "__inquirer_questions__"): + func.__inquirer_questions__ = {} + q = { + "type": ( + inq_type if isinstance(inq_type, str) else ("input" if not is_flag else "confirm") + ), + "name": long[2:], + "message": message, + } + if required_unless is not None: + q["required_unless"] = required_unless + if default is not None: + q["default"] = default + if required or required_unless is not None: + q["validate"] = Required(validator) + elif validator: + q["validate"] = validator + func.__inquirer_questions__[long[2:]] = q + return func + + return decorator diff --git a/maubot-src/maubot/cli/cliq/validators.py b/maubot-src/maubot/cli/cliq/validators.py new file mode 100644 index 0000000..46d3c92 --- /dev/null +++ b/maubot-src/maubot/cli/cliq/validators.py @@ -0,0 +1,85 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from typing import Callable +import os + +from packaging.version import InvalidVersion, Version +from prompt_toolkit.document import Document +from prompt_toolkit.validation import ValidationError, Validator +import click + +from ..util import spdx as spdxlib + + +class Required(Validator): + proxy: Validator + + def __init__(self, proxy: Validator = None) -> None: + self.proxy = proxy + + def validate(self, document: Document) -> None: + if len(document.text) == 0: + raise ValidationError(message="This field is required") + if self.proxy: + return self.proxy.validate(document) + + +class ClickValidator(Validator): + click_type: Callable[[str], str] = None + + @classmethod + def validate(cls, document: Document) -> None: + try: + cls.click_type(document.text) + except click.BadParameter as e: + raise ValidationError(message=e.message, cursor_position=len(document.text)) + + +def path(val: str) -> str: + val = os.path.abspath(val) + if os.path.exists(val): + return val + directory = os.path.dirname(val) + if not os.path.isdir(directory): + if os.path.exists(directory): + raise click.BadParameter(f"{directory} is not a directory") + raise click.BadParameter(f"{directory} does not exist") + return val + + +class PathValidator(ClickValidator): + click_type = path + + +def version(val: str) -> Version: + try: + return Version(val) + except InvalidVersion as e: + raise click.BadParameter(f"{val} is not a valid PEP-440 version") from e + + +class VersionValidator(ClickValidator): + click_type = version + + +def spdx(val: str) -> str: + if not spdxlib.valid(val): + raise click.BadParameter(f"{val} is not a valid SPDX license identifier") + return val + + +class SPDXValidator(ClickValidator): + click_type = spdx diff --git a/maubot-src/maubot/cli/commands/__init__.py b/maubot-src/maubot/cli/commands/__init__.py new file mode 100644 index 0000000..145646b --- /dev/null +++ b/maubot-src/maubot/cli/commands/__init__.py @@ -0,0 +1 @@ +from . import auth, build, init, login, logs, upload diff --git a/maubot-src/maubot/cli/commands/auth.py b/maubot-src/maubot/cli/commands/auth.py new file mode 100644 index 0000000..64b1dc7 --- /dev/null +++ b/maubot-src/maubot/cli/commands/auth.py @@ -0,0 +1,166 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +import json +import webbrowser + +from colorama import Fore +from yarl import URL +import aiohttp +import click + +from ..cliq import cliq + +history_count: int = 10 + +friendly_errors = { + "server_not_found": ( + "Registration target server not found.\n\n" + "To log in or register through maubot, you must add the server to the\n" + "homeservers section in the config. If you only want to log in,\n" + "leave the `secret` field empty." + ), + "registration_no_sso": ( + "The register operation is only for registering with a password.\n\n" + "To register with SSO, simply leave out the --register flag." + ), +} + + +async def list_servers(server: str, sess: aiohttp.ClientSession) -> None: + url = URL(server) / "_matrix/maubot/v1/client/auth/servers" + async with sess.get(url) as resp: + data = await resp.json() + print(f"{Fore.GREEN}Available Matrix servers for registration and login:{Fore.RESET}") + for server in data.keys(): + print(f"* {Fore.CYAN}{server}{Fore.RESET}") + + +@cliq.command(help="Log into a Matrix account via the Maubot server") +@cliq.option("-h", "--homeserver", help="The homeserver to log into", required_unless="list") +@cliq.option( + "-u", "--username", help="The username to log in with", required_unless=["list", "sso"] +) +@cliq.option( + "-p", + "--password", + help="The password to log in with", + inq_type="password", + required_unless=["list", "sso"], +) +@cliq.option( + "-s", + "--server", + help="The maubot instance to log in through", + default="", + required=False, + prompt=False, +) +@click.option( + "-r", "--register", help="Register instead of logging in", is_flag=True, default=False +) +@click.option( + "-c", + "--update-client", + help="Instead of returning the access token, create or update a client in maubot using it", + is_flag=True, + default=False, +) +@click.option("-l", "--list", help="List available homeservers", is_flag=True, default=False) +@click.option( + "-o", "--sso", help="Use single sign-on instead of password login", is_flag=True, default=False +) +@click.option( + "-n", + "--device-name", + help="The initial e2ee device displayname (only for login)", + default="Maubot", + required=False, +) +@cliq.with_authenticated_http +async def auth( + homeserver: str, + username: str, + password: str, + server: str, + register: bool, + list: bool, + update_client: bool, + device_name: str, + sso: bool, + sess: aiohttp.ClientSession, +) -> None: + if list: + await list_servers(server, sess) + return + endpoint = "register" if register else "login" + url = URL(server) / "_matrix/maubot/v1/client/auth" / homeserver / endpoint + if update_client: + url = url.update_query({"update_client": "true"}) + if sso: + url = url.update_query({"sso": "true"}) + req_data = {"device_name": device_name} + else: + req_data = {"username": username, "password": password, "device_name": device_name} + + async with sess.post(url, json=req_data) as resp: + if not 200 <= resp.status < 300: + await print_error(resp, is_register=register) + elif sso: + await wait_sso(resp, sess, server, homeserver) + else: + await print_response(resp, is_register=register) + + +async def wait_sso( + resp: aiohttp.ClientResponse, sess: aiohttp.ClientSession, server: str, homeserver: str +) -> None: + data = await resp.json() + sso_url, reg_id = data["sso_url"], data["id"] + print(f"{Fore.GREEN}Opening {Fore.CYAN}{sso_url}{Fore.RESET}") + webbrowser.open(sso_url, autoraise=True) + print(f"{Fore.GREEN}Waiting for login token...{Fore.RESET}") + wait_url = URL(server) / "_matrix/maubot/v1/client/auth" / homeserver / "sso" / reg_id / "wait" + async with sess.post(wait_url, json={}) as resp: + await print_response(resp, is_register=False) + + +async def print_response(resp: aiohttp.ClientResponse, is_register: bool) -> None: + if resp.status == 200: + data = await resp.json() + action = "registered" if is_register else "logged in as" + print(f"{Fore.GREEN}Successfully {action} {Fore.CYAN}{data['user_id']}{Fore.GREEN}.") + print(f"{Fore.GREEN}Access token: {Fore.CYAN}{data['access_token']}{Fore.RESET}") + print(f"{Fore.GREEN}Device ID: {Fore.CYAN}{data['device_id']}{Fore.RESET}") + elif resp.status in (201, 202): + data = await resp.json() + action = "created" if resp.status == 201 else "updated" + print( + f"{Fore.GREEN}Successfully {action} client for " + f"{Fore.CYAN}{data['id']}{Fore.GREEN} / " + f"{Fore.CYAN}{data['device_id']}{Fore.GREEN}.{Fore.RESET}" + ) + else: + await print_error(resp, is_register) + + +async def print_error(resp: aiohttp.ClientResponse, is_register: bool) -> None: + try: + err_data = await resp.json() + error = friendly_errors.get(err_data["errcode"], err_data["error"]) + except (aiohttp.ContentTypeError, json.JSONDecodeError, KeyError): + error = await resp.text() + action = "register" if is_register else "log in" + print(f"{Fore.RED}Failed to {action}: {error}{Fore.RESET}") diff --git a/maubot-src/maubot/cli/commands/build.py b/maubot-src/maubot/cli/commands/build.py new file mode 100644 index 0000000..39eca53 --- /dev/null +++ b/maubot-src/maubot/cli/commands/build.py @@ -0,0 +1,155 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +from typing import IO +from io import BytesIO +import asyncio +import glob +import os +import zipfile + +from aiohttp import ClientSession +from colorama import Fore +from questionary import prompt +from ruamel.yaml import YAML, YAMLError +import click + +from mautrix.types import SerializerError + +from ...loader import PluginMeta +from ..base import app +from ..cliq import cliq +from ..cliq.validators import PathValidator +from ..config import get_token +from .upload import upload_file + +yaml = YAML() + + +def zipdir(zip, dir): + for root, dirs, files in os.walk(dir): + for file in files: + zip.write(os.path.join(root, file)) + + +def read_meta(path: str) -> PluginMeta | None: + try: + with open(os.path.join(path, "maubot.yaml")) as meta_file: + try: + meta_dict = yaml.load(meta_file) + except YAMLError as e: + print(Fore.RED + "Failed to build plugin: Metadata file is not YAML") + print(Fore.RED + str(e) + Fore.RESET) + return None + except FileNotFoundError: + print(Fore.RED + "Failed to build plugin: Metadata file not found" + Fore.RESET) + return None + try: + meta = PluginMeta.deserialize(meta_dict) + except SerializerError as e: + print(Fore.RED + "Failed to build plugin: Metadata file is not valid") + print(Fore.RED + str(e) + Fore.RESET) + return None + return meta + + +def read_output_path(output: str, meta: PluginMeta) -> str | None: + directory = os.getcwd() + filename = f"{meta.id}-v{meta.version}.mbp" + if not output: + output = os.path.join(directory, filename) + elif os.path.isdir(output): + output = os.path.join(output, filename) + elif os.path.exists(output): + q = [{"type": "confirm", "name": "override", "message": f"{output} exists, override?"}] + override = prompt(q)["override"] + if not override: + return None + os.remove(output) + return os.path.abspath(output) + + +def write_plugin(meta: PluginMeta, output: str | IO) -> None: + with zipfile.ZipFile(output, "w") as zip: + meta_dump = BytesIO() + yaml.dump(meta.serialize(), meta_dump) + zip.writestr("maubot.yaml", meta_dump.getvalue()) + + for module in meta.modules: + if os.path.isfile(f"{module}.py"): + zip.write(f"{module}.py") + elif module is not None and os.path.isdir(module): + if os.path.isfile(f"{module}/__init__.py"): + zipdir(zip, module) + else: + print( + Fore.YELLOW + + f"Module {module} is missing __init__.py, skipping" + + Fore.RESET + ) + else: + print(Fore.YELLOW + f"Module {module} not found, skipping" + Fore.RESET) + for pattern in meta.extra_files: + for file in glob.iglob(pattern): + zip.write(file) + + +@cliq.with_authenticated_http +async def upload_plugin(output: str | IO, *, server: str, sess: ClientSession) -> None: + server, token = get_token(server) + if not token: + return + if isinstance(output, str): + with open(output, "rb") as file: + await upload_file(sess, file, server) + else: + await upload_file(sess, output, server) + + +@app.command( + short_help="Build a maubot plugin", + help=( + "Build a maubot plugin. First parameter is the path to root of the plugin " + "to build. You can also use --output to specify output file." + ), +) +@click.argument("path", default=os.getcwd()) +@click.option( + "-o", "--output", help="Path to output built plugin to", type=PathValidator.click_type +) +@click.option( + "-u", "--upload", help="Upload plugin to server after building", is_flag=True, default=False +) +@click.option("-s", "--server", help="Server to upload built plugin to") +def build(path: str, output: str, upload: bool, server: str) -> None: + meta = read_meta(path) + if not meta: + return + if output or not upload: + output = read_output_path(output, meta) + if not output: + return + else: + output = BytesIO() + os.chdir(path) + write_plugin(meta, output) + if isinstance(output, str): + print(f"{Fore.GREEN}Plugin built to {Fore.CYAN}{output}{Fore.GREEN}.{Fore.RESET}") + else: + output.seek(0) + if upload: + asyncio.run(upload_plugin(output, server=server)) diff --git a/maubot-src/maubot/cli/commands/init.py b/maubot-src/maubot/cli/commands/init.py new file mode 100644 index 0000000..d24def9 --- /dev/null +++ b/maubot-src/maubot/cli/commands/init.py @@ -0,0 +1,99 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +import os + +from jinja2 import Template +from packaging.version import Version +from pkg_resources import resource_string + +from .. import cliq +from ..cliq import SPDXValidator, VersionValidator +from ..util import spdx + +loaded: bool = False +meta_template: Template +mod_template: Template +base_config: str + + +def load_templates(): + global mod_template, meta_template, base_config, loaded + if loaded: + return + meta_template = Template(resource_string("maubot.cli", "res/maubot.yaml.j2").decode("utf-8")) + mod_template = Template(resource_string("maubot.cli", "res/plugin.py.j2").decode("utf-8")) + base_config = resource_string("maubot.cli", "res/config.yaml").decode("utf-8") + loaded = True + + +@cliq.command(help="Initialize a new maubot plugin") +@cliq.option( + "-n", + "--name", + help="The name of the project", + required=True, + default=os.path.basename(os.getcwd()), +) +@cliq.option( + "-i", + "--id", + message="ID", + required=True, + help="The maubot plugin ID (Java package name format)", +) +@cliq.option( + "-v", + "--version", + help="Initial version for project (PEP-440 format)", + default="0.1.0", + validator=VersionValidator, + required=True, +) +@cliq.option( + "-l", + "--license", + validator=SPDXValidator, + default="AGPL-3.0-or-later", + help="The license for the project (SPDX identifier)", + required=False, +) +@cliq.option( + "-c", + "--config", + message="Should the plugin include a config?", + help="Include a config in the plugin stub", + default=False, + is_flag=True, +) +def init(name: str, id: str, version: Version, license: str, config: bool) -> None: + load_templates() + main_class = name[0].upper() + name[1:] + meta = meta_template.render( + id=id, version=str(version), license=license, config=config, main_class=main_class + ) + with open("maubot.yaml", "w") as file: + file.write(meta) + if license: + with open("LICENSE", "w") as file: + file.write(spdx.get(license)["licenseText"]) + if not os.path.isdir(name): + os.mkdir(name) + mod = mod_template.render(config=config, name=main_class) + with open(f"{name}/__init__.py", "w") as file: + file.write(mod) + if config: + with open("base-config.yaml", "w") as file: + file.write(base_config) diff --git a/maubot-src/maubot/cli/commands/login.py b/maubot-src/maubot/cli/commands/login.py new file mode 100644 index 0000000..8aac0f5 --- /dev/null +++ b/maubot-src/maubot/cli/commands/login.py @@ -0,0 +1,77 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +import json +import os + +from colorama import Fore +from yarl import URL +import aiohttp + +from ..cliq import cliq +from ..config import config, save_config + + +@cliq.command(help="Log in to a Maubot instance") +@cliq.option( + "-u", + "--username", + help="The username of your account", + default=os.environ.get("USER", None), + required=True, +) +@cliq.option( + "-p", "--password", help="The password to your account", inq_type="password", required=True +) +@cliq.option( + "-s", + "--server", + help="The server to log in to", + default="http://localhost:29316", + required=True, +) +@cliq.option( + "-a", + "--alias", + help="Alias to reference the server without typing the full URL", + default="", + required=False, +) +@cliq.with_http +async def login( + server: str, username: str, password: str, alias: str, sess: aiohttp.ClientSession +) -> None: + data = { + "username": username, + "password": password, + } + url = URL(server) / "_matrix/maubot/v1/auth/login" + async with sess.post(url, json=data) as resp: + if resp.status == 200: + data = await resp.json() + config["servers"][server] = data["token"] + if not config["default_server"]: + print(Fore.CYAN, "Setting", server, "as the default server") + config["default_server"] = server + if alias: + config["aliases"][alias] = server + save_config() + print(Fore.GREEN + "Logged in successfully") + else: + try: + err = (await resp.json())["error"] + except (json.JSONDecodeError, KeyError): + err = await resp.text() + print(Fore.RED + err + Fore.RESET) diff --git a/maubot-src/maubot/cli/commands/logs.py b/maubot-src/maubot/cli/commands/logs.py new file mode 100644 index 0000000..e0ed07d --- /dev/null +++ b/maubot-src/maubot/cli/commands/logs.py @@ -0,0 +1,107 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from datetime import datetime +import asyncio + +from aiohttp import ClientSession, WSMessage, WSMsgType +from colorama import Fore +import click + +from mautrix.types import Obj + +from ..base import app +from ..config import get_token + +history_count: int = 10 + + +@app.command(help="View the logs of a server") +@click.argument("server", required=False) +@click.option("-t", "--tail", default=10, help="Maximum number of old log lines to display") +def logs(server: str, tail: int) -> None: + server, token = get_token(server) + if not token: + return + global history_count + history_count = tail + loop = asyncio.get_event_loop() + loop.run_until_complete(view_logs(server, token)) + + +def parsedate(entry: Obj) -> None: + i = entry.time.index("+") + i = entry.time.index(":", i) + entry.time = entry.time[:i] + entry.time[i + 1 :] + entry.time = datetime.strptime(entry.time, "%Y-%m-%dT%H:%M:%S.%f%z") + + +levelcolors = { + "DEBUG": "", + "INFO": Fore.CYAN, + "WARNING": Fore.YELLOW, + "ERROR": Fore.RED, + "FATAL": Fore.MAGENTA, +} + + +def print_entry(entry: dict) -> None: + entry = Obj(**entry) + parsedate(entry) + print( + "{levelcolor}[{date}] [{level}@{logger}] {message}{resetcolor}".format( + date=entry.time.strftime("%Y-%m-%d %H:%M:%S"), + level=entry.levelname, + levelcolor=levelcolors.get(entry.levelname, ""), + resetcolor=Fore.RESET, + logger=entry.name, + message=entry.msg, + ) + ) + if entry.exc_info: + print(entry.exc_info) + + +def handle_msg(data: dict) -> bool: + if "auth_success" in data: + if data["auth_success"]: + print(Fore.GREEN + "Connected to log websocket" + Fore.RESET) + else: + print(Fore.RED + "Failed to authenticate to log websocket" + Fore.RESET) + return False + elif "history" in data: + for entry in data["history"][-history_count:]: + print_entry(entry) + else: + print_entry(data) + return True + + +async def view_logs(server: str, token: str) -> None: + async with ClientSession() as session: + async with session.ws_connect(f"{server}/_matrix/maubot/v1/logs") as ws: + await ws.send_str(token) + try: + msg: WSMessage + async for msg in ws: + if msg.type == WSMsgType.TEXT: + if not handle_msg(msg.json()): + break + elif msg.type == WSMsgType.ERROR: + print(Fore.YELLOW + "Connection error: " + msg.data + Fore.RESET) + elif msg.type == WSMsgType.CLOSE: + print(Fore.YELLOW + "Server closed connection" + Fore.RESET) + except asyncio.CancelledError: + pass diff --git a/maubot-src/maubot/cli/commands/upload.py b/maubot-src/maubot/cli/commands/upload.py new file mode 100644 index 0000000..3c2cf1e --- /dev/null +++ b/maubot-src/maubot/cli/commands/upload.py @@ -0,0 +1,58 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from typing import IO +import json + +from colorama import Fore +from yarl import URL +import aiohttp +import click + +from ..cliq import cliq + + +class UploadError(Exception): + pass + + +@cliq.command(help="Upload a maubot plugin") +@click.argument("path") +@click.option("-s", "--server", help="The maubot instance to upload the plugin to") +@cliq.with_authenticated_http +async def upload(path: str, server: str, sess: aiohttp.ClientSession) -> None: + with open(path, "rb") as file: + await upload_file(sess, file, server) + + +async def upload_file(sess: aiohttp.ClientSession, file: IO, server: str) -> None: + url = (URL(server) / "_matrix/maubot/v1/plugins/upload").with_query({"allow_override": "true"}) + headers = {"Content-Type": "application/zip"} + async with sess.post(url, data=file, headers=headers) as resp: + if resp.status in (200, 201): + data = await resp.json() + print( + f"{Fore.GREEN}Plugin {Fore.CYAN}{data['id']} v{data['version']}{Fore.GREEN} " + f"uploaded to {Fore.CYAN}{server}{Fore.GREEN} successfully.{Fore.RESET}" + ) + else: + try: + err = await resp.json() + if "stacktrace" in err: + print(err["stacktrace"]) + err = err["error"] + except (aiohttp.ContentTypeError, json.JSONDecodeError, KeyError): + err = await resp.text() + print(f"{Fore.RED}Failed to upload plugin: {err}{Fore.RESET}") diff --git a/maubot-src/maubot/cli/config.py b/maubot-src/maubot/cli/config.py new file mode 100644 index 0000000..5fdc4ea --- /dev/null +++ b/maubot-src/maubot/cli/config.py @@ -0,0 +1,80 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +from typing import Any +import json +import os + +from colorama import Fore + +config: dict[str, Any] = { + "servers": {}, + "aliases": {}, + "default_server": None, +} +configdir = os.environ.get("XDG_CONFIG_HOME", os.path.join(os.environ.get("HOME"), ".config")) + + +def get_default_server() -> tuple[str | None, str | None]: + try: + server: str < None = config["default_server"] + except KeyError: + server = None + if server is None: + print(f"{Fore.RED}Default server not configured.{Fore.RESET}") + print(f"Perhaps you forgot to {Fore.CYAN}mbc login{Fore.RESET}?") + return None, None + return server, _get_token(server) + + +def get_token(server: str) -> tuple[str | None, str | None]: + if not server: + return get_default_server() + if server in config["aliases"]: + server = config["aliases"][server] + return server, _get_token(server) + + +def _resolve_alias(alias: str) -> str | None: + try: + return config["aliases"][alias] + except KeyError: + return None + + +def _get_token(server: str) -> str | None: + try: + return config["servers"][server] + except KeyError: + print(f"{Fore.RED}No access token saved for {server}.{Fore.RESET}") + return None + + +def save_config() -> None: + with open(f"{configdir}/maubot-cli.json", "w") as file: + json.dump(config, file) + + +def load_config() -> None: + try: + with open(f"{configdir}/maubot-cli.json") as file: + loaded = json.load(file) + config["servers"] = loaded.get("servers", {}) + config["aliases"] = loaded.get("aliases", {}) + config["default_server"] = loaded.get("default_server", None) + except FileNotFoundError: + pass diff --git a/maubot-src/maubot/cli/res/config.yaml b/maubot-src/maubot/cli/res/config.yaml new file mode 100644 index 0000000..bbeb6da --- /dev/null +++ b/maubot-src/maubot/cli/res/config.yaml @@ -0,0 +1,6 @@ +example_1: Example value 1 +example_2: + list: + - foo + - bar + value: asd diff --git a/maubot-src/maubot/cli/res/maubot.yaml.j2 b/maubot-src/maubot/cli/res/maubot.yaml.j2 new file mode 100644 index 0000000..2d91ba2 --- /dev/null +++ b/maubot-src/maubot/cli/res/maubot.yaml.j2 @@ -0,0 +1,42 @@ +# The unique ID for the plugin. Java package naming style. (i.e. use your own domain, not xyz.maubot) +id: {{ id }} + +# A PEP 440 compliant version string. +version: {{ version }} + +# The SPDX license identifier for the plugin. https://spdx.org/licenses/ +# Optional, assumes all rights reserved if omitted. +{% if license %} +license: {{ license }} +{% else %} +#license: null +{% endif %} + +# The list of modules to load from the plugin archive. +# Modules can be directories with an __init__.py file or simply python files. +# Submodules that are imported by modules listed here don't need to be listed separately. +# However, top-level modules must always be listed even if they're imported by other modules. +modules: +- {{ name }} + +# The main class of the plugin. Format: module/Class +# If `module` is omitted, will default to last module specified in the module list. +# Even if `module` is not omitted here, it must be included in the modules list. +# The main class must extend maubot.Plugin +main_class: {{ main_class }} + +# Extra files that the upcoming build tool should include in the mbp file. +{% if config %} +extra_files: +- base-config.yaml +{% else %} +#extra_files: +#- base-config.yaml +{% endif %} + +# List of dependencies +#dependencies: +#- foo + +#soft_dependencies: +#- bar>=0.1 diff --git a/maubot-src/maubot/cli/res/plugin.py.j2 b/maubot-src/maubot/cli/res/plugin.py.j2 new file mode 100644 index 0000000..05d5341 --- /dev/null +++ b/maubot-src/maubot/cli/res/plugin.py.j2 @@ -0,0 +1,27 @@ +from typing import Type +{% if config %} +from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper +{% endif %} +from maubot import Plugin +{% if config %} + +class Config(BaseProxyConfig): + def do_update(self, helper: ConfigUpdateHelper) -> None: + helper.copy("example_1") + helper.copy("example_2.list") + helper.copy("example_2.value") +{% endif %} + +class {{ name }}(Plugin): + async def start(self) -> None:{% if config %} + self.config.load_and_update() + self.log.debug("Loaded %s from config example 2", self.config["example_2.value"]){% else %} + pass{% endif %} + + async def stop(self) -> None: + pass +{% if config %} + @classmethod + def get_config_class(cls) -> Type[BaseProxyConfig]: + return Config +{% endif %} diff --git a/maubot-src/maubot/cli/res/spdx.json.zip b/maubot-src/maubot/cli/res/spdx.json.zip new file mode 100644 index 0000000..98de1b0 Binary files /dev/null and b/maubot-src/maubot/cli/res/spdx.json.zip differ diff --git a/maubot-src/maubot/cli/util/__init__.py b/maubot-src/maubot/cli/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/maubot-src/maubot/cli/util/spdx.py b/maubot-src/maubot/cli/util/spdx.py new file mode 100644 index 0000000..69f58b7 --- /dev/null +++ b/maubot-src/maubot/cli/util/spdx.py @@ -0,0 +1,45 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +import json +import zipfile + +import pkg_resources + +spdx_list: dict[str, dict[str, str]] | None = None + + +def load() -> None: + global spdx_list + if spdx_list is not None: + return + with pkg_resources.resource_stream("maubot.cli", "res/spdx.json.zip") as disk_file: + with zipfile.ZipFile(disk_file) as zip_file: + with zip_file.open("spdx.json") as file: + spdx_list = json.load(file) + + +def get(id: str) -> dict[str, str]: + if not spdx_list: + load() + return spdx_list[id] + + +def valid(id: str) -> bool: + if not spdx_list: + load() + return id in spdx_list diff --git a/maubot-src/maubot/client.py b/maubot-src/maubot/client.py new file mode 100644 index 0000000..915b170 --- /dev/null +++ b/maubot-src/maubot/client.py @@ -0,0 +1,581 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, AsyncGenerator, Awaitable, Callable, cast +from collections import defaultdict +import asyncio +import logging + +from aiohttp import ClientSession + +from mautrix.api import HTTPAPI +from mautrix.client import InternalEventType +from mautrix.errors import MatrixInvalidToken +from mautrix.types import ( + ContentURI, + DeviceID, + EventFilter, + EventType, + Filter, + FilterID, + Membership, + PresenceState, + RoomEventFilter, + RoomFilter, + StateEvent, + StateFilter, + StrippedStateEvent, + SyncToken, + TrustState, + UserID, +) +from mautrix.util import background_task +from mautrix.util.async_getter_lock import async_getter_lock +from mautrix.util.logging import TraceLogger + +from .db import Client as DBClient +from .matrix import MaubotMatrixClient + +try: + from mautrix.crypto import OlmMachine, PgCryptoStore + + crypto_import_error = None +except ImportError as e: + OlmMachine = PgCryptoStore = None + crypto_import_error = e + +if TYPE_CHECKING: + from .__main__ import Maubot + from .instance import PluginInstance + + +class Client(DBClient): + maubot: "Maubot" = None + cache: dict[UserID, Client] = {} + _async_get_locks: dict[Any, asyncio.Lock] = defaultdict(lambda: asyncio.Lock()) + log: TraceLogger = logging.getLogger("maubot.client") + + http_client: ClientSession = None + + references: set[PluginInstance] + client: MaubotMatrixClient + crypto: OlmMachine | None + crypto_store: PgCryptoStore | None + started: bool + sync_ok: bool + + remote_displayname: str | None + remote_avatar_url: ContentURI | None + trust_state: TrustState | None + + def __init__( + self, + id: UserID, + homeserver: str, + access_token: str, + device_id: DeviceID, + enabled: bool = False, + next_batch: SyncToken = "", + filter_id: FilterID = "", + sync: bool = True, + autojoin: bool = True, + online: bool = True, + displayname: str = "disable", + avatar_url: str = "disable", + ) -> None: + super().__init__( + id=id, + homeserver=homeserver, + access_token=access_token, + device_id=device_id, + enabled=bool(enabled), + next_batch=next_batch, + filter_id=filter_id, + sync=bool(sync), + autojoin=bool(autojoin), + online=bool(online), + displayname=displayname, + avatar_url=avatar_url, + ) + self._postinited = False + + def __hash__(self) -> int: + return hash(self.id) + + @classmethod + def init_cls(cls, maubot: "Maubot") -> None: + cls.maubot = maubot + + def _make_client( + self, homeserver: str | None = None, token: str | None = None, device_id: str | None = None + ) -> MaubotMatrixClient: + return MaubotMatrixClient( + mxid=self.id, + base_url=homeserver or self.homeserver, + token=token or self.access_token, + client_session=self.http_client, + log=self.log, + crypto_log=self.log.getChild("crypto"), + loop=self.maubot.loop, + device_id=device_id or self.device_id, + sync_store=self, + state_store=self.maubot.state_store, + ) + + def postinit(self) -> None: + if self._postinited: + raise RuntimeError("postinit() called twice") + self._postinited = True + self.cache[self.id] = self + self.log = self.log.getChild(self.id) + self.http_client = ClientSession( + loop=self.maubot.loop, + headers={ + "User-Agent": HTTPAPI.default_ua, + }, + ) + self.references = set() + self.started = False + self.sync_ok = True + self.trust_state = None + self.remote_displayname = None + self.remote_avatar_url = None + self.client = self._make_client() + if self.enable_crypto: + self._prepare_crypto() + else: + self.crypto_store = None + self.crypto = None + self.client.ignore_initial_sync = True + self.client.ignore_first_sync = True + self.client.presence = PresenceState.ONLINE if self.online else PresenceState.OFFLINE + if self.autojoin: + self.client.add_event_handler(EventType.ROOM_MEMBER, self._handle_invite) + self.client.add_event_handler(EventType.ROOM_TOMBSTONE, self._handle_tombstone) + self.client.add_event_handler(InternalEventType.SYNC_ERRORED, self._set_sync_ok(False)) + self.client.add_event_handler(InternalEventType.SYNC_SUCCESSFUL, self._set_sync_ok(True)) + + def _set_sync_ok(self, ok: bool) -> Callable[[dict[str, Any]], Awaitable[None]]: + async def handler(data: dict[str, Any]) -> None: + self.sync_ok = ok + + return handler + + @property + def enable_crypto(self) -> bool: + if not self.device_id: + return False + elif not OlmMachine: + global crypto_import_error + self.log.warning( + "Client has device ID, but encryption dependencies not installed", + exc_info=crypto_import_error, + ) + # Clear the stack trace after it's logged once to avoid spamming logs + crypto_import_error = None + return False + elif not self.maubot.crypto_db: + self.log.warning("Client has device ID, but crypto database is not prepared") + return False + return True + + def _prepare_crypto(self) -> None: + self.crypto_store = PgCryptoStore( + account_id=self.id, pickle_key="mau.crypto", db=self.maubot.crypto_db + ) + self.crypto = OlmMachine( + self.client, + self.crypto_store, + self.maubot.state_store, + log=self.client.crypto_log, + ) + self.client.crypto = self.crypto + + def _remove_crypto_event_handlers(self) -> None: + if not self.crypto: + return + handlers = [ + (InternalEventType.DEVICE_OTK_COUNT, self.crypto.handle_otk_count), + (InternalEventType.DEVICE_LISTS, self.crypto.handle_device_lists), + (EventType.TO_DEVICE_ENCRYPTED, self.crypto.handle_to_device_event), + (EventType.ROOM_KEY_REQUEST, self.crypto.handle_room_key_request), + (EventType.ROOM_MEMBER, self.crypto.handle_member_event), + ] + for event_type, func in handlers: + self.client.remove_event_handler(event_type, func) + + async def start(self, try_n: int | None = 0) -> None: + try: + if try_n > 0: + await asyncio.sleep(try_n * 10) + await self._start(try_n) + except Exception: + self.log.exception("Failed to start") + + async def _start_crypto(self) -> None: + self.log.debug("Enabling end-to-end encryption support") + await self.crypto_store.open() + crypto_device_id = await self.crypto_store.get_device_id() + if crypto_device_id and crypto_device_id != self.device_id: + self.log.warning( + "Mismatching device ID in crypto store and main database, resetting encryption" + ) + await self.crypto_store.delete() + crypto_device_id = None + await self.crypto.load() + if not crypto_device_id: + await self.crypto_store.put_device_id(self.device_id) + if not self.crypto.account.shared: + await self.crypto.share_keys() + self.trust_state = await self.crypto.resolve_trust( + self.crypto.own_identity, + allow_fetch=False, + ) + + async def _start(self, try_n: int | None = 0) -> None: + if not self.enabled: + self.log.debug("Not starting disabled client") + return + elif self.started: + self.log.warning("Ignoring start() call to started client") + return + try: + await self.client.versions() + whoami = await self.client.whoami() + except MatrixInvalidToken as e: + self.log.error(f"Invalid token: {e}. Disabling client") + self.enabled = False + await self.update() + return + except Exception as e: + if try_n >= 8: + self.log.exception("Failed to get /account/whoami, disabling client") + self.enabled = False + await self.update() + else: + self.log.warning( + f"Failed to get /account/whoami, retrying in {(try_n + 1) * 10}s: {e}" + ) + background_task.create(self.start(try_n + 1)) + return + if whoami.user_id != self.id: + self.log.error(f"User ID mismatch: expected {self.id}, but got {whoami.user_id}") + self.enabled = False + await self.update() + return + elif whoami.device_id and self.device_id and whoami.device_id != self.device_id: + self.log.error( + f"Device ID mismatch: expected {self.device_id}, but got {whoami.device_id}" + ) + self.enabled = False + await self.update() + return + if not self.filter_id: + self.filter_id = await self.client.create_filter( + Filter( + room=RoomFilter( + timeline=RoomEventFilter( + limit=50, + lazy_load_members=True, + ), + state=StateFilter( + lazy_load_members=True, + ), + ), + presence=EventFilter( + not_types=[EventType.PRESENCE], + ), + ) + ) + await self.update() + if self.displayname != "disable": + await self.client.set_displayname(self.displayname) + if self.avatar_url != "disable": + await self.client.set_avatar_url(self.avatar_url) + if self.crypto: + await self._start_crypto() + self.start_sync() + await self._update_remote_profile() + self.started = True + self.log.info("Client started, starting plugin instances...") + await self.start_plugins() + + async def start_plugins(self) -> None: + await asyncio.gather(*[plugin.start() for plugin in self.references]) + + async def stop_plugins(self) -> None: + await asyncio.gather(*[plugin.stop() for plugin in self.references if plugin.started]) + + def start_sync(self) -> None: + if self.sync: + self.client.start(self.filter_id) + + def stop_sync(self) -> None: + self.client.stop() + + async def stop(self) -> None: + if self.started: + self.started = False + await self.stop_plugins() + self.stop_sync() + if self.crypto: + await self.crypto_store.close() + + async def clear_cache(self) -> None: + self.stop_sync() + self.filter_id = FilterID("") + self.next_batch = SyncToken("") + await self.update() + self.start_sync() + + def to_dict(self) -> dict: + return { + "id": self.id, + "homeserver": self.homeserver, + "access_token": self.access_token, + "device_id": self.device_id, + "fingerprint": ( + self.crypto.account.fingerprint if self.crypto and self.crypto.account else None + ), + "enabled": self.enabled, + "started": self.started, + "sync": self.sync, + "sync_ok": self.sync_ok, + "autojoin": self.autojoin, + "online": self.online, + "displayname": self.displayname, + "avatar_url": self.avatar_url, + "remote_displayname": self.remote_displayname, + "remote_avatar_url": self.remote_avatar_url, + "instances": [instance.to_dict() for instance in self.references], + "trust_state": str(self.trust_state) if self.trust_state else None, + } + + async def _handle_tombstone(self, evt: StateEvent) -> None: + if evt.state_key != "": + return + if not evt.content.replacement_room: + self.log.info(f"{evt.room_id} tombstoned with no replacement, ignoring") + return + is_joined = await self.client.state_store.is_joined( + evt.content.replacement_room, + self.client.mxid, + ) + if is_joined: + self.log.debug( + f"Ignoring tombstone from {evt.room_id} to {evt.content.replacement_room} " + f"sent by {evt.sender}: already joined to replacement room" + ) + return + self.log.debug( + f"Following tombstone from {evt.room_id} to {evt.content.replacement_room} " + f"sent by {evt.sender}" + ) + _, server = self.client.parse_user_id(evt.sender) + room_id = await self.client.join_room(evt.content.replacement_room, servers=[server]) + power_levels = await self.client.get_state_event(room_id, EventType.ROOM_POWER_LEVELS) + create_event = await self.client.get_state_event( + room_id, EventType.ROOM_CREATE, format="event" + ) + if power_levels.get_user_level(evt.sender, create_event) < power_levels.invite: + self.log.warning( + f"{evt.room_id} was tombstoned into {room_id} by {evt.sender}," + " but the sender doesn't have invite power levels, leaving..." + ) + await self.client.leave_room( + room_id, + f"Followed tombstone from {evt.room_id} by {evt.sender}," + " but sender doesn't have sufficient power level for invites", + ) + + async def _handle_invite(self, evt: StrippedStateEvent) -> None: + if evt.state_key == self.id and evt.content.membership == Membership.INVITE: + await self.client.join_room(evt.room_id) + + async def update_started(self, started: bool | None) -> None: + if started is None or started == self.started: + return + if started: + await self.start() + else: + await self.stop() + + async def update_enabled(self, enabled: bool | None, save: bool = True) -> None: + if enabled is None or enabled == self.enabled: + return + self.enabled = enabled + if save: + await self.update() + + async def update_displayname(self, displayname: str | None, save: bool = True) -> None: + if displayname is None or displayname == self.displayname: + return + self.displayname = displayname + if self.displayname != "disable": + await self.client.set_displayname(self.displayname) + else: + await self._update_remote_profile() + if save: + await self.update() + + async def update_avatar_url(self, avatar_url: ContentURI, save: bool = True) -> None: + if avatar_url is None or avatar_url == self.avatar_url: + return + self.avatar_url = avatar_url + if self.avatar_url != "disable": + await self.client.set_avatar_url(self.avatar_url) + else: + await self._update_remote_profile() + if save: + await self.update() + + async def update_sync(self, sync: bool | None, save: bool = True) -> None: + if sync is None or self.sync == sync: + return + self.sync = sync + if self.started: + if sync: + self.start_sync() + else: + self.stop_sync() + if save: + await self.update() + + async def update_autojoin(self, autojoin: bool | None, save: bool = True) -> None: + if autojoin is None or autojoin == self.autojoin: + return + if autojoin: + self.client.add_event_handler(EventType.ROOM_MEMBER, self._handle_invite) + else: + self.client.remove_event_handler(EventType.ROOM_MEMBER, self._handle_invite) + self.autojoin = autojoin + if save: + await self.update() + + async def update_online(self, online: bool | None, save: bool = True) -> None: + if online is None or online == self.online: + return + self.client.presence = PresenceState.ONLINE if online else PresenceState.OFFLINE + self.online = online + if save: + await self.update() + + async def update_access_details( + self, + access_token: str | None, + homeserver: str | None, + device_id: str | None = None, + ) -> None: + if not access_token and not homeserver: + return + if device_id is None: + device_id = self.device_id + elif not device_id: + device_id = None + if ( + access_token == self.access_token + and homeserver == self.homeserver + and device_id == self.device_id + ): + return + new_client = self._make_client(homeserver, access_token, device_id) + whoami = await new_client.whoami() + if whoami.user_id != self.id: + raise ValueError(f"MXID mismatch: {whoami.user_id}") + elif whoami.device_id and device_id and whoami.device_id != device_id: + raise ValueError(f"Device ID mismatch: {whoami.device_id}") + new_client.sync_store = self + self.stop_sync() + + # TODO this event handler transfer is pretty hacky + self._remove_crypto_event_handlers() + self.client.crypto = None + new_client.event_handlers = self.client.event_handlers + new_client.global_event_handlers = self.client.global_event_handlers + + self.client = new_client + self.homeserver = homeserver + self.access_token = access_token + self.device_id = device_id + if self.enable_crypto: + self._prepare_crypto() + await self._start_crypto() + else: + self.crypto_store = None + self.crypto = None + self.start_sync() + + async def _update_remote_profile(self) -> None: + try: + profile = await self.client.get_profile(self.id) + self.remote_displayname, self.remote_avatar_url = ( + profile.displayname, + profile.avatar_url, + ) + except Exception: + self.log.warning("Failed to update own profile from server", exc_info=True) + + async def delete(self) -> None: + try: + del self.cache[self.id] + except KeyError: + pass + await super().delete() + + @classmethod + @async_getter_lock + async def get( + cls, + user_id: UserID, + *, + homeserver: str | None = None, + access_token: str | None = None, + device_id: DeviceID | None = None, + ) -> Client | None: + try: + return cls.cache[user_id] + except KeyError: + pass + + user = cast(cls, await super().get(user_id)) + if user is not None: + user.postinit() + return user + + if homeserver and access_token: + user = cls( + user_id, + homeserver=homeserver, + access_token=access_token, + device_id=device_id or "", + ) + await user.insert() + user.postinit() + return user + + return None + + @classmethod + async def all(cls) -> AsyncGenerator[Client, None]: + users = await super().all() + user: cls + for user in users: + try: + yield cls.cache[user.id] + except KeyError: + user.postinit() + yield user diff --git a/maubot-src/maubot/config.py b/maubot-src/maubot/config.py new file mode 100644 index 0000000..b8e42de --- /dev/null +++ b/maubot-src/maubot/config.py @@ -0,0 +1,100 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +import random +import re +import string + +import bcrypt + +from mautrix.util.config import BaseFileConfig, ConfigUpdateHelper + +bcrypt_regex = re.compile(r"^\$2[ayb]\$.{56}$") + + +class Config(BaseFileConfig): + @staticmethod + def _new_token() -> str: + return "".join(random.choices(string.ascii_lowercase + string.digits, k=64)) + + def do_update(self, helper: ConfigUpdateHelper) -> None: + base = helper.base + copy = helper.copy + + if "database" in self and self["database"].startswith("sqlite:///"): + helper.base["database"] = self["database"].replace("sqlite:///", "sqlite:") + else: + copy("database") + copy("database_opts") + if isinstance(self["crypto_database"], dict): + if self["crypto_database.type"] == "postgres": + base["crypto_database"] = self["crypto_database.postgres_uri"] + else: + copy("crypto_database") + copy("plugin_directories.upload") + copy("plugin_directories.load") + copy("plugin_directories.trash") + if "plugin_directories.db" in self: + base["plugin_databases.sqlite"] = self["plugin_directories.db"] + else: + copy("plugin_databases.sqlite") + copy("plugin_databases.postgres") + copy("plugin_databases.postgres_opts") + copy("server.hostname") + copy("server.port") + copy("server.public_url") + copy("server.listen") + copy("server.ui_base_path") + copy("server.plugin_base_path") + copy("server.override_resource_path") + shared_secret = self["server.unshared_secret"] + if shared_secret is None or shared_secret == "generate": + base["server.unshared_secret"] = self._new_token() + else: + base["server.unshared_secret"] = shared_secret + if "registration_secrets" in self: + base["homeservers"] = self["registration_secrets"] + else: + copy("homeservers") + copy("admins") + for username, password in base["admins"].items(): + if password and not bcrypt_regex.match(password): + if password == "password": + password = self._new_token() + base["admins"][username] = bcrypt.hashpw( + password.encode("utf-8"), bcrypt.gensalt() + ).decode("utf-8") + copy("api_features.login") + copy("api_features.plugin") + copy("api_features.plugin_upload") + copy("api_features.instance") + copy("api_features.instance_database") + copy("api_features.client") + copy("api_features.client_proxy") + copy("api_features.client_auth") + copy("api_features.dev_open") + copy("api_features.log") + copy("logging") + + def is_admin(self, user: str) -> bool: + return user == "root" or user in self["admins"] + + def check_password(self, user: str, passwd: str) -> bool: + if user == "root": + return False + passwd_hash = self["admins"].get(user, None) + if not passwd_hash: + return False + return bcrypt.checkpw(passwd.encode("utf-8"), passwd_hash.encode("utf-8")) diff --git a/maubot-src/maubot/db/__init__.py b/maubot-src/maubot/db/__init__.py new file mode 100644 index 0000000..68833ce --- /dev/null +++ b/maubot-src/maubot/db/__init__.py @@ -0,0 +1,13 @@ +from mautrix.util.async_db import Database + +from .client import Client +from .instance import DatabaseEngine, Instance +from .upgrade import upgrade_table + + +def init(db: Database) -> None: + for table in (Client, Instance): + table.db = db + + +__all__ = ["upgrade_table", "init", "Client", "Instance", "DatabaseEngine"] diff --git a/maubot-src/maubot/db/client.py b/maubot-src/maubot/db/client.py new file mode 100644 index 0000000..52f3a20 --- /dev/null +++ b/maubot-src/maubot/db/client.py @@ -0,0 +1,114 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +from typing import TYPE_CHECKING, ClassVar + +from asyncpg import Record +from attr import dataclass + +from mautrix.client import SyncStore +from mautrix.types import ContentURI, DeviceID, FilterID, SyncToken, UserID +from mautrix.util.async_db import Database + +fake_db = Database.create("") if TYPE_CHECKING else None + + +@dataclass +class Client(SyncStore): + db: ClassVar[Database] = fake_db + + id: UserID + homeserver: str + access_token: str + device_id: DeviceID + enabled: bool + + next_batch: SyncToken + filter_id: FilterID + + sync: bool + autojoin: bool + online: bool + + displayname: str + avatar_url: ContentURI + + @classmethod + def _from_row(cls, row: Record | None) -> Client | None: + if row is None: + return None + return cls(**row) + + _columns = ( + "id, homeserver, access_token, device_id, enabled, next_batch, filter_id, " + "sync, autojoin, online, displayname, avatar_url" + ) + + @property + def _values(self): + return ( + self.id, + self.homeserver, + self.access_token, + self.device_id, + self.enabled, + self.next_batch, + self.filter_id, + self.sync, + self.autojoin, + self.online, + self.displayname, + self.avatar_url, + ) + + @classmethod + async def all(cls) -> list[Client]: + rows = await cls.db.fetch(f"SELECT {cls._columns} FROM client") + return [cls._from_row(row) for row in rows] + + @classmethod + async def get(cls, id: str) -> Client | None: + q = f"SELECT {cls._columns} FROM client WHERE id=$1" + return cls._from_row(await cls.db.fetchrow(q, id)) + + async def insert(self) -> None: + q = """ + INSERT INTO client ( + id, homeserver, access_token, device_id, enabled, next_batch, filter_id, + sync, autojoin, online, displayname, avatar_url + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) + """ + await self.db.execute(q, *self._values) + + async def put_next_batch(self, next_batch: SyncToken) -> None: + await self.db.execute("UPDATE client SET next_batch=$1 WHERE id=$2", next_batch, self.id) + self.next_batch = next_batch + + async def get_next_batch(self) -> SyncToken: + return self.next_batch + + async def update(self) -> None: + q = """ + UPDATE client SET homeserver=$2, access_token=$3, device_id=$4, enabled=$5, + next_batch=$6, filter_id=$7, sync=$8, autojoin=$9, online=$10, + displayname=$11, avatar_url=$12 + WHERE id=$1 + """ + await self.db.execute(q, *self._values) + + async def delete(self) -> None: + await self.db.execute("DELETE FROM client WHERE id=$1", self.id) diff --git a/maubot-src/maubot/db/instance.py b/maubot-src/maubot/db/instance.py new file mode 100644 index 0000000..5bb3f6a --- /dev/null +++ b/maubot-src/maubot/db/instance.py @@ -0,0 +1,101 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +from typing import TYPE_CHECKING, ClassVar +from enum import Enum + +from asyncpg import Record +from attr import dataclass + +from mautrix.types import UserID +from mautrix.util.async_db import Database + +fake_db = Database.create("") if TYPE_CHECKING else None + + +class DatabaseEngine(Enum): + SQLITE = "sqlite" + POSTGRES = "postgres" + + +@dataclass +class Instance: + db: ClassVar[Database] = fake_db + + id: str + type: str + enabled: bool + primary_user: UserID + config_str: str + database_engine: DatabaseEngine | None + + @property + def database_engine_str(self) -> str | None: + return self.database_engine.value if self.database_engine else None + + @classmethod + def _from_row(cls, row: Record | None) -> Instance | None: + if row is None: + return None + data = {**row} + db_engine = data.pop("database_engine", None) + return cls(**data, database_engine=DatabaseEngine(db_engine) if db_engine else None) + + _columns = "id, type, enabled, primary_user, config, database_engine" + + @classmethod + async def all(cls) -> list[Instance]: + q = f"SELECT {cls._columns} FROM instance" + rows = await cls.db.fetch(q) + return [cls._from_row(row) for row in rows] + + @classmethod + async def get(cls, id: str) -> Instance | None: + q = f"SELECT {cls._columns} FROM instance WHERE id=$1" + return cls._from_row(await cls.db.fetchrow(q, id)) + + async def update_id(self, new_id: str) -> None: + await self.db.execute("UPDATE instance SET id=$1 WHERE id=$2", new_id, self.id) + self.id = new_id + + @property + def _values(self): + return ( + self.id, + self.type, + self.enabled, + self.primary_user, + self.config_str, + self.database_engine_str, + ) + + async def insert(self) -> None: + q = ( + "INSERT INTO instance (id, type, enabled, primary_user, config, database_engine) " + "VALUES ($1, $2, $3, $4, $5, $6)" + ) + await self.db.execute(q, *self._values) + + async def update(self) -> None: + q = """ + UPDATE instance SET type=$2, enabled=$3, primary_user=$4, config=$5, database_engine=$6 + WHERE id=$1 + """ + await self.db.execute(q, *self._values) + + async def delete(self) -> None: + await self.db.execute("DELETE FROM instance WHERE id=$1", self.id) diff --git a/maubot-src/maubot/db/upgrade/__init__.py b/maubot-src/maubot/db/upgrade/__init__.py new file mode 100644 index 0000000..ed96422 --- /dev/null +++ b/maubot-src/maubot/db/upgrade/__init__.py @@ -0,0 +1,5 @@ +from mautrix.util.async_db import UpgradeTable + +upgrade_table = UpgradeTable() + +from . import v01_initial_revision, v02_instance_database_engine diff --git a/maubot-src/maubot/db/upgrade/v01_initial_revision.py b/maubot-src/maubot/db/upgrade/v01_initial_revision.py new file mode 100644 index 0000000..2da8aff --- /dev/null +++ b/maubot-src/maubot/db/upgrade/v01_initial_revision.py @@ -0,0 +1,136 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +from mautrix.util.async_db import Connection, Scheme + +from . import upgrade_table + +legacy_version_query = "SELECT version_num FROM alembic_version" +last_legacy_version = "90aa88820eab" + + +@upgrade_table.register(description="Initial asyncpg revision") +async def upgrade_v1(conn: Connection, scheme: Scheme) -> None: + if await conn.table_exists("alembic_version"): + await migrate_legacy_to_v1(conn, scheme) + else: + return await create_v1_tables(conn) + + +async def create_v1_tables(conn: Connection) -> None: + await conn.execute( + """CREATE TABLE client ( + id TEXT PRIMARY KEY, + homeserver TEXT NOT NULL, + access_token TEXT NOT NULL, + device_id TEXT NOT NULL, + enabled BOOLEAN NOT NULL, + + next_batch TEXT NOT NULL, + filter_id TEXT NOT NULL, + + sync BOOLEAN NOT NULL, + autojoin BOOLEAN NOT NULL, + online BOOLEAN NOT NULL, + + displayname TEXT NOT NULL, + avatar_url TEXT NOT NULL + )""" + ) + await conn.execute( + """CREATE TABLE instance ( + id TEXT PRIMARY KEY, + type TEXT NOT NULL, + enabled BOOLEAN NOT NULL, + primary_user TEXT NOT NULL, + config TEXT NOT NULL, + FOREIGN KEY (primary_user) REFERENCES client(id) ON DELETE RESTRICT ON UPDATE CASCADE + )""" + ) + + +async def migrate_legacy_to_v1(conn: Connection, scheme: Scheme) -> None: + legacy_version = await conn.fetchval(legacy_version_query) + if legacy_version != last_legacy_version: + raise RuntimeError( + "Legacy database is not on last version. " + "Please upgrade the old database with alembic or drop it completely first." + ) + await conn.execute("ALTER TABLE plugin RENAME TO instance") + await update_state_store(conn, scheme) + if scheme != Scheme.SQLITE: + await varchar_to_text(conn) + await conn.execute("DROP TABLE alembic_version") + + +async def update_state_store(conn: Connection, scheme: Scheme) -> None: + # The Matrix state store already has more or less the correct schema, so set the version + await conn.execute("CREATE TABLE mx_version (version INTEGER PRIMARY KEY)") + await conn.execute("INSERT INTO mx_version (version) VALUES (2)") + if scheme != Scheme.SQLITE: + # Remove old uppercase membership type and recreate it as lowercase + await conn.execute("ALTER TABLE mx_user_profile ALTER COLUMN membership TYPE TEXT") + await conn.execute("DROP TYPE IF EXISTS membership") + await conn.execute( + "CREATE TYPE membership AS ENUM ('join', 'leave', 'invite', 'ban', 'knock')" + ) + await conn.execute( + "ALTER TABLE mx_user_profile ALTER COLUMN membership TYPE membership " + "USING LOWER(membership)::membership" + ) + else: + # Recreate table to remove CHECK constraint and lowercase everything + await conn.execute( + """CREATE TABLE new_mx_user_profile ( + room_id TEXT, + user_id TEXT, + membership TEXT NOT NULL + CHECK (membership IN ('join', 'leave', 'invite', 'ban', 'knock')), + displayname TEXT, + avatar_url TEXT, + PRIMARY KEY (room_id, user_id) + )""" + ) + await conn.execute( + """ + INSERT INTO new_mx_user_profile (room_id, user_id, membership, displayname, avatar_url) + SELECT room_id, user_id, LOWER(membership), displayname, avatar_url + FROM mx_user_profile + """ + ) + await conn.execute("DROP TABLE mx_user_profile") + await conn.execute("ALTER TABLE new_mx_user_profile RENAME TO mx_user_profile") + + +async def varchar_to_text(conn: Connection) -> None: + columns_to_adjust = { + "client": ( + "id", + "homeserver", + "device_id", + "next_batch", + "filter_id", + "displayname", + "avatar_url", + ), + "instance": ("id", "type", "primary_user"), + "mx_room_state": ("room_id",), + "mx_user_profile": ("room_id", "user_id", "displayname", "avatar_url"), + } + for table, columns in columns_to_adjust.items(): + for column in columns: + await conn.execute(f'ALTER TABLE "{table}" ALTER COLUMN {column} TYPE TEXT') diff --git a/maubot-src/maubot/db/upgrade/v02_instance_database_engine.py b/maubot-src/maubot/db/upgrade/v02_instance_database_engine.py new file mode 100644 index 0000000..7d2d7e7 --- /dev/null +++ b/maubot-src/maubot/db/upgrade/v02_instance_database_engine.py @@ -0,0 +1,25 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +from mautrix.util.async_db import Connection + +from . import upgrade_table + + +@upgrade_table.register(description="Store instance database engine") +async def upgrade_v2(conn: Connection) -> None: + await conn.execute("ALTER TABLE instance ADD COLUMN database_engine TEXT") diff --git a/maubot-src/maubot/example-config.yaml b/maubot-src/maubot/example-config.yaml new file mode 100644 index 0000000..7991461 --- /dev/null +++ b/maubot-src/maubot/example-config.yaml @@ -0,0 +1,132 @@ +# The full URI to the database. SQLite and Postgres are fully supported. +# Format examples: +# SQLite: sqlite:filename.db +# Postgres: postgresql://username:password@hostname/dbname +database: sqlite:maubot.db + +# Separate database URL for the crypto database. "default" means use the same database as above. +crypto_database: default + +# Additional arguments for asyncpg.create_pool() or sqlite3.connect() +# https://magicstack.github.io/asyncpg/current/api/index.html#asyncpg.pool.create_pool +# https://docs.python.org/3/library/sqlite3.html#sqlite3.connect +# For sqlite, min_size is used as the connection thread pool size and max_size is ignored. +database_opts: + min_size: 1 + max_size: 10 + +# Configuration for storing plugin .mbp files +plugin_directories: + # The directory where uploaded new plugins should be stored. + upload: ./plugins + # The directories from which plugins should be loaded. + # Duplicate plugin IDs will be moved to the trash. + load: + - ./plugins + # The directory where old plugin versions and conflicting plugins should be moved. + # Set to "delete" to delete files immediately. + trash: ./trash + +# Configuration for storing plugin databases +plugin_databases: + # The directory where SQLite plugin databases should be stored. + sqlite: ./plugins + # The connection URL for plugin databases. If null, all plugins will get SQLite databases. + # If set, plugins using the new asyncpg interface will get a Postgres connection instead. + # Plugins using the legacy SQLAlchemy interface will always get a SQLite connection. + # + # To use the same connection pool as the default database, set to "default" + # (the default database above must be postgres to do this). + # + # When enabled, maubot will create separate Postgres schemas in the database for each plugin. + # To view schemas in psql, use `\dn`. To view enter and interact with a specific schema, + # use `SET search_path = name` (where `name` is the name found with `\dn`) and then use normal + # SQL queries/psql commands. + postgres: null + # Maximum number of connections per plugin instance. + postgres_max_conns_per_plugin: 3 + # Overrides for the default database_opts when using a non-"default" postgres connection string. + postgres_opts: {} + +server: + # The IP and port to listen to. + hostname: 0.0.0.0 + port: 29316 + # Public base URL where the server is visible. + public_url: https://example.com + # The base path for the UI. Note that this does not change the API path. + # Add a path prefix to public_url if you want everything on a subpath. + ui_base_path: /_matrix/maubot + # The base path for plugin endpoints. The instance ID will be appended directly. + plugin_base_path: /_matrix/maubot/plugin/ + # Override path from where to load UI resources. + # Set to false to using pkg_resources to find the path. + override_resource_path: false + # The shared secret to sign API access tokens. + # Set to "generate" to generate and save a new token at startup. + unshared_secret: generate + +# Known homeservers. This is required for the `mbc auth` command and also allows +# more convenient access from the management UI. This is not required to create +# clients in the management UI, since you can also just type the homeserver URL +# into the box there. +homeservers: + matrix.org: + # Client-server API URL + url: https://matrix-client.matrix.org + # registration_shared_secret from synapse config + # You can leave this empty if you don't have access to the homeserver. + # When this is empty, `mbc auth --register` won't work, but `mbc auth` (login) will. + secret: null + +# List of administrator users. Each key is a username and the value is the password. +# Plaintext passwords will be bcrypted on startup. Set empty password to prevent normal login. +# Root is a special user that can't have a password and will always exist. +admins: + root: "" + +# API feature switches. +api_features: + login: true + plugin: true + plugin_upload: true + instance: true + instance_database: true + client: true + client_proxy: true + client_auth: true + dev_open: true + log: true + +# Python logging configuration. +# +# See section 16.7.2 of the Python documentation for more info: +# https://docs.python.org/3.6/library/logging.config.html#configuration-dictionary-schema +logging: + version: 1 + formatters: + colored: + (): maubot.lib.color_log.ColorFormatter + format: "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s" + normal: + format: "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s" + handlers: + file: + class: logging.handlers.RotatingFileHandler + formatter: normal + filename: ./maubot.log + maxBytes: 10485760 + backupCount: 10 + console: + class: logging.StreamHandler + formatter: colored + loggers: + maubot: + level: DEBUG + mau: + level: DEBUG + aiohttp: + level: INFO + root: + level: DEBUG + handlers: [file, console] diff --git a/maubot-src/maubot/handlers/__init__.py b/maubot-src/maubot/handlers/__init__.py new file mode 100644 index 0000000..e8567a2 --- /dev/null +++ b/maubot-src/maubot/handlers/__init__.py @@ -0,0 +1 @@ +from . import command, event, web diff --git a/maubot-src/maubot/handlers/command.py b/maubot-src/maubot/handlers/command.py new file mode 100644 index 0000000..27e6547 --- /dev/null +++ b/maubot-src/maubot/handlers/command.py @@ -0,0 +1,496 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from typing import ( + Any, + Awaitable, + Callable, + Dict, + Iterable, + List, + NewType, + Optional, + Pattern, + Sequence, + Set, + Tuple, + Union, +) +from abc import ABC, abstractmethod +import asyncio +import functools +import inspect +import re + +from mautrix.types import EventType, MessageType + +from ..matrix import MaubotMessageEvent +from . import event + +PrefixType = Optional[Union[str, Callable[[], str], Callable[[Any], str]]] +AliasesType = Union[ + List[str], Tuple[str, ...], Set[str], Callable[[str], bool], Callable[[Any, str], bool] +] +CommandHandlerFunc = NewType( + "CommandHandlerFunc", Callable[[MaubotMessageEvent, Any], Awaitable[Any]] +) +CommandHandlerDecorator = NewType( + "CommandHandlerDecorator", + Callable[[Union["CommandHandler", CommandHandlerFunc]], "CommandHandler"], +) +PassiveCommandHandlerDecorator = NewType( + "PassiveCommandHandlerDecorator", Callable[[CommandHandlerFunc], CommandHandlerFunc] +) + + +def _split_in_two(val: str, split_by: str) -> List[str]: + return val.split(split_by, 1) if split_by in val else [val, ""] + + +class CommandHandler: + def __init__(self, func: CommandHandlerFunc) -> None: + self.__mb_func__: CommandHandlerFunc = func + self.__mb_parent__: Optional[CommandHandler] = None + self.__mb_subcommands__: List[CommandHandler] = [] + self.__mb_arguments__: List[Argument] = [] + self.__mb_help__: Optional[str] = None + self.__mb_get_name__: Callable[[Any], str] = lambda s: "noname" + self.__mb_is_command_match__: Callable[[Any, str], bool] = self.__command_match_unset + self.__mb_require_subcommand__: bool = True + self.__mb_must_consume_args__: bool = True + self.__mb_arg_fallthrough__: bool = True + self.__mb_event_handler__: bool = True + self.__mb_event_types__: set[EventType] = {EventType.ROOM_MESSAGE} + self.__mb_msgtypes__: Iterable[MessageType] = (MessageType.TEXT,) + self.__bound_copies__: Dict[Any, CommandHandler] = {} + self.__bound_instance__: Any = None + + def __get__(self, instance, instancetype): + if not instance or self.__bound_instance__: + return self + try: + return self.__bound_copies__[instance] + except KeyError: + new_ch = type(self)(self.__mb_func__) + keys = [ + "parent", + "subcommands", + "arguments", + "help", + "get_name", + "is_command_match", + "require_subcommand", + "must_consume_args", + "arg_fallthrough", + "event_handler", + "event_types", + "msgtypes", + ] + for key in keys: + key = f"__mb_{key}__" + setattr(new_ch, key, getattr(self, key)) + new_ch.__bound_instance__ = instance + new_ch.__mb_subcommands__ = [ + subcmd.__get__(instance, instancetype) for subcmd in self.__mb_subcommands__ + ] + self.__bound_copies__[instance] = new_ch + return new_ch + + @staticmethod + def __command_match_unset(self, val: str) -> bool: + raise NotImplementedError("Hmm") + + async def __call__( + self, + evt: MaubotMessageEvent, + *, + _existing_args: Dict[str, Any] = None, + remaining_val: str = None, + ) -> Any: + if evt.sender == evt.client.mxid or evt.content.msgtype not in self.__mb_msgtypes__: + return + if remaining_val is None: + if not evt.content.body or evt.content.body[0] != "!": + return + command, remaining_val = _split_in_two(evt.content.body[1:], " ") + command = command.lower() + if not self.__mb_is_command_match__(self.__bound_instance__, command): + return + call_args: Dict[str, Any] = {**_existing_args} if _existing_args else {} + + if not self.__mb_arg_fallthrough__ and len(self.__mb_subcommands__) > 0: + ok, res = await self.__call_subcommand__(evt, call_args, remaining_val) + if ok: + return res + + ok, remaining_val = await self.__parse_args__(evt, call_args, remaining_val) + if not ok: + return + elif self.__mb_arg_fallthrough__ and len(self.__mb_subcommands__) > 0: + ok, res = await self.__call_subcommand__(evt, call_args, remaining_val) + if ok: + return res + elif self.__mb_require_subcommand__: + await evt.reply(self.__mb_full_help__) + return + + if self.__mb_must_consume_args__ and remaining_val.strip(): + await evt.reply(self.__mb_full_help__) + return + + if self.__bound_instance__: + return await self.__mb_func__(self.__bound_instance__, evt, **call_args) + return await self.__mb_func__(evt, **call_args) + + async def __call_subcommand__( + self, evt: MaubotMessageEvent, call_args: Dict[str, Any], remaining_val: str + ) -> Tuple[bool, Any]: + command, remaining_val = _split_in_two(remaining_val.strip(), " ") + for subcommand in self.__mb_subcommands__: + if subcommand.__mb_is_command_match__(subcommand.__bound_instance__, command): + return True, await subcommand( + evt, _existing_args=call_args, remaining_val=remaining_val + ) + return False, None + + async def __parse_args__( + self, evt: MaubotMessageEvent, call_args: Dict[str, Any], remaining_val: str + ) -> Tuple[bool, str]: + for arg in self.__mb_arguments__: + try: + remaining_val, call_args[arg.name] = arg.match( + remaining_val.strip(), evt=evt, instance=self.__bound_instance__ + ) + if arg.required and call_args[arg.name] is None: + raise ValueError("Argument required") + except ArgumentSyntaxError as e: + await evt.reply(e.message + (f"\n{self.__mb_usage__}" if e.show_usage else "")) + return False, remaining_val + except ValueError: + await evt.reply(self.__mb_usage__) + return False, remaining_val + return True, remaining_val + + @property + def __mb_full_help__(self) -> str: + usage = self.__mb_usage_without_subcommands__ + "\n\n" + if not self.__mb_require_subcommand__: + usage += f"* {self.__mb_prefix__} {self.__mb_usage_args__} - {self.__mb_help__}\n" + usage += "\n".join(cmd.__mb_usage_inline__ for cmd in self.__mb_subcommands__) + return usage + + @property + def __mb_usage_args__(self) -> str: + arg_usage = " ".join( + f"<{arg.label}>" if arg.required else f"[{arg.label}]" for arg in self.__mb_arguments__ + ) + if self.__mb_subcommands__ and self.__mb_arg_fallthrough__: + arg_usage += " " + self.__mb_usage_subcommand__ + return arg_usage + + @property + def __mb_usage_subcommand__(self) -> str: + return f" [...]" + + @property + def __mb_name__(self) -> str: + return self.__mb_get_name__(self.__bound_instance__) + + @property + def __mb_prefix__(self) -> str: + if self.__mb_parent__: + return ( + f"!{self.__mb_parent__.__mb_get_name__(self.__bound_instance__)} " + f"{self.__mb_name__}" + ) + return f"!{self.__mb_name__}" + + @property + def __mb_usage_inline__(self) -> str: + if not self.__mb_arg_fallthrough__: + return ( + f"* {self.__mb_name__} {self.__mb_usage_args__} - {self.__mb_help__}\n" + f"* {self.__mb_name__} {self.__mb_usage_subcommand__}" + ) + return f"* {self.__mb_name__} {self.__mb_usage_args__} - {self.__mb_help__}" + + @property + def __mb_subcommands_list__(self) -> str: + return f"**Subcommands:** {', '.join(sc.__mb_name__ for sc in self.__mb_subcommands__)}" + + @property + def __mb_usage_without_subcommands__(self) -> str: + if not self.__mb_arg_fallthrough__: + if not self.__mb_arguments__: + return f"**Usage:** {self.__mb_prefix__} [subcommand] [...]" + return ( + f"**Usage:** {self.__mb_prefix__} {self.__mb_usage_args__}" + f" _OR_ {self.__mb_prefix__} {self.__mb_usage_subcommand__}" + ) + return f"**Usage:** {self.__mb_prefix__} {self.__mb_usage_args__}" + + @property + def __mb_usage__(self) -> str: + if len(self.__mb_subcommands__) > 0: + return f"{self.__mb_usage_without_subcommands__} \n{self.__mb_subcommands_list__}" + return self.__mb_usage_without_subcommands__ + + def subcommand( + self, + name: PrefixType = None, + *, + help: str = None, + aliases: AliasesType = None, + required_subcommand: bool = True, + arg_fallthrough: bool = True, + ) -> CommandHandlerDecorator: + def decorator(func: Union[CommandHandler, CommandHandlerFunc]) -> CommandHandler: + if not isinstance(func, CommandHandler): + func = CommandHandler(func) + new( + name, + help=help, + aliases=aliases, + require_subcommand=required_subcommand, + arg_fallthrough=arg_fallthrough, + )(func) + func.__mb_parent__ = self + func.__mb_event_handler__ = False + self.__mb_subcommands__.append(func) + return func + + return decorator + + +def new( + name: PrefixType = None, + *, + help: str = None, + aliases: AliasesType = None, + event_type: EventType = EventType.ROOM_MESSAGE, + msgtypes: Iterable[MessageType] = None, + require_subcommand: bool = True, + arg_fallthrough: bool = True, + must_consume_args: bool = True, +) -> CommandHandlerDecorator: + def decorator(func: Union[CommandHandler, CommandHandlerFunc]) -> CommandHandler: + if not isinstance(func, CommandHandler): + func = CommandHandler(func) + func.__mb_help__ = help + if name: + if callable(name): + if len(inspect.getfullargspec(name).args) == 0: + func.__mb_get_name__ = lambda self: name() + else: + func.__mb_get_name__ = name + else: + func.__mb_get_name__ = lambda self: name + else: + func.__mb_get_name__ = lambda self: func.__mb_func__.__name__.replace("_", "-") + if callable(aliases): + if len(inspect.getfullargspec(aliases).args) == 1: + func.__mb_is_command_match__ = lambda self, val: aliases(val) + else: + func.__mb_is_command_match__ = aliases + elif isinstance(aliases, (list, set, tuple)): + func.__mb_is_command_match__ = lambda self, val: ( + val == func.__mb_get_name__(self) or val in aliases + ) + else: + func.__mb_is_command_match__ = lambda self, val: val == func.__mb_get_name__(self) + # Decorators are executed last to first, so we reverse the argument list. + func.__mb_arguments__.reverse() + func.__mb_require_subcommand__ = require_subcommand + func.__mb_arg_fallthrough__ = arg_fallthrough + func.__mb_must_consume_args__ = must_consume_args + func.__mb_event_types__ = {event_type} + if msgtypes: + func.__mb_msgtypes__ = msgtypes + return func + + return decorator + + +class ArgumentSyntaxError(ValueError): + def __init__(self, message: str, show_usage: bool = True) -> None: + super().__init__(message) + self.message = message + self.show_usage = show_usage + + +class Argument(ABC): + def __init__( + self, name: str, label: str = None, *, required: bool = False, pass_raw: bool = False + ) -> None: + self.name = name + self.label = label or name + self.required = required + self.pass_raw = pass_raw + + @abstractmethod + def match(self, val: str, **kwargs) -> Tuple[str, Any]: + pass + + def __call__(self, func: Union[CommandHandler, CommandHandlerFunc]) -> CommandHandler: + if not isinstance(func, CommandHandler): + func = CommandHandler(func) + func.__mb_arguments__.append(self) + return func + + +class RegexArgument(Argument): + def __init__( + self, + name: str, + label: str = None, + *, + required: bool = False, + pass_raw: bool = False, + matches: str = None, + ) -> None: + super().__init__(name, label, required=required, pass_raw=pass_raw) + matches = f"^{matches}" if self.pass_raw else f"^{matches}$" + self.regex = re.compile(matches) + + def match(self, val: str, **kwargs) -> Tuple[str, Any]: + orig_val = val + if not self.pass_raw: + val = re.split(r"\s", val, 1)[0] + match = self.regex.match(val) + if match: + return ( + orig_val[: match.start()] + orig_val[match.end() :], + match.groups() or val[match.start() : match.end()], + ) + return orig_val, None + + +class CustomArgument(Argument): + def __init__( + self, + name: str, + label: str = None, + *, + required: bool = False, + pass_raw: bool = False, + matcher: Callable[[str], Any], + ) -> None: + super().__init__(name, label, required=required, pass_raw=pass_raw) + self.matcher = matcher + + def match(self, val: str, **kwargs) -> Tuple[str, Any]: + if self.pass_raw: + return self.matcher(val) + orig_val = val + val = re.split(r"\s", val, 1)[0] + res = self.matcher(val) + if res is not None: + return orig_val[len(val) :], res + return orig_val, None + + +class SimpleArgument(Argument): + def match(self, val: str, **kwargs) -> Tuple[str, Any]: + if self.pass_raw: + return "", val + res = re.split(r"\s", val, 1)[0] + return val[len(res) :], res + + +def argument( + name: str, + label: str = None, + *, + required: bool = True, + matches: Optional[str] = None, + parser: Optional[Callable[[str], Any]] = None, + pass_raw: bool = False, +) -> CommandHandlerDecorator: + if matches: + return RegexArgument(name, label, required=required, matches=matches, pass_raw=pass_raw) + elif parser: + return CustomArgument(name, label, required=required, matcher=parser, pass_raw=pass_raw) + else: + return SimpleArgument(name, label, required=required, pass_raw=pass_raw) + + +def passive( + regex: Union[str, Pattern], + *, + msgtypes: Sequence[MessageType] = (MessageType.TEXT,), + field: Callable[[MaubotMessageEvent], str] = lambda evt: evt.content.body, + event_type: EventType = EventType.ROOM_MESSAGE, + multiple: bool = False, + case_insensitive: bool = False, + multiline: bool = False, + dot_all: bool = False, +) -> PassiveCommandHandlerDecorator: + if not isinstance(regex, Pattern): + flags = re.RegexFlag.UNICODE + if case_insensitive: + flags |= re.IGNORECASE + if multiline: + flags |= re.MULTILINE + if dot_all: + flags |= re.DOTALL + regex = re.compile(regex, flags=flags) + + def decorator(func: CommandHandlerFunc) -> CommandHandlerFunc: + combine = None + if hasattr(func, "__mb_passive_orig__"): + combine = func + func = func.__mb_passive_orig__ + + @event.on(event_type) + @functools.wraps(func) + async def replacement(self, evt: MaubotMessageEvent = None) -> None: + if not evt and isinstance(self, MaubotMessageEvent): + evt = self + self = None + if evt.sender == evt.client.mxid: + return + elif msgtypes and evt.content.msgtype not in msgtypes: + return + data = field(evt) + if multiple: + val = [ + (data[match.pos : match.endpos], *match.groups()) + for match in regex.finditer(data) + ] + else: + match = regex.search(data) + if match: + val = (data[match.pos : match.endpos], *match.groups()) + else: + val = None + if val: + if self: + await func(self, evt, val) + else: + await func(evt, val) + + if combine: + orig_replacement = replacement + + @event.on(event_type) + @functools.wraps(func) + async def replacement(self, evt: MaubotMessageEvent = None) -> None: + await asyncio.gather(combine(self, evt), orig_replacement(self, evt)) + + replacement.__mb_passive_orig__ = func + + return replacement + + return decorator diff --git a/maubot-src/maubot/handlers/event.py b/maubot-src/maubot/handlers/event.py new file mode 100644 index 0000000..9be89b1 --- /dev/null +++ b/maubot-src/maubot/handlers/event.py @@ -0,0 +1,44 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +from typing import Callable, NewType + +from mautrix.client import EventHandler, InternalEventType +from mautrix.types import EventType + +EventHandlerDecorator = NewType("EventHandlerDecorator", Callable[[EventHandler], EventHandler]) + + +def on(var: EventType | InternalEventType | EventHandler) -> EventHandlerDecorator | EventHandler: + def decorator(func: EventHandler) -> EventHandler: + func.__mb_event_handler__ = True + if isinstance(var, (EventType, InternalEventType)): + if hasattr(func, "__mb_event_types__"): + func.__mb_event_types__.add(var) + else: + func.__mb_event_types__ = {var} + else: + func.__mb_event_types__ = {EventType.ALL} + + return func + + return decorator if isinstance(var, (EventType, InternalEventType)) else decorator(var) + + +def off(func: EventHandler) -> EventHandler: + func.__mb_event_handler__ = False + return func diff --git a/maubot-src/maubot/handlers/web.py b/maubot-src/maubot/handlers/web.py new file mode 100644 index 0000000..f170124 --- /dev/null +++ b/maubot-src/maubot/handlers/web.py @@ -0,0 +1,66 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from typing import Any, Awaitable, Callable + +from aiohttp import hdrs, web + +WebHandler = Callable[[web.Request], Awaitable[web.StreamResponse]] +WebHandlerDecorator = Callable[[WebHandler], WebHandler] + + +def head(path: str, **kwargs: Any) -> WebHandlerDecorator: + return handle(hdrs.METH_HEAD, path, **kwargs) + + +def options(path: str, **kwargs: Any) -> WebHandlerDecorator: + return handle(hdrs.METH_OPTIONS, path, **kwargs) + + +def get(path: str, **kwargs: Any) -> WebHandlerDecorator: + return handle(hdrs.METH_GET, path, **kwargs) + + +def post(path: str, **kwargs: Any) -> WebHandlerDecorator: + return handle(hdrs.METH_POST, path, **kwargs) + + +def put(path: str, **kwargs: Any) -> WebHandlerDecorator: + return handle(hdrs.METH_PUT, path, **kwargs) + + +def patch(path: str, **kwargs: Any) -> WebHandlerDecorator: + return handle(hdrs.METH_PATCH, path, **kwargs) + + +def delete(path: str, **kwargs: Any) -> WebHandlerDecorator: + return handle(hdrs.METH_DELETE, path, **kwargs) + + +def view(path: str, **kwargs: Any) -> WebHandlerDecorator: + return handle(hdrs.METH_ANY, path, **kwargs) + + +def handle(method: str, path: str, **kwargs) -> WebHandlerDecorator: + def decorator(handler: WebHandler) -> WebHandler: + try: + handlers = getattr(handler, "__mb_web_handler__") + except AttributeError: + handlers = [] + setattr(handler, "__mb_web_handler__", handlers) + handlers.append((method, path, kwargs)) + return handler + + return decorator diff --git a/maubot-src/maubot/instance.py b/maubot-src/maubot/instance.py new file mode 100644 index 0000000..8427e3c --- /dev/null +++ b/maubot-src/maubot/instance.py @@ -0,0 +1,533 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, AsyncGenerator, cast +from collections import defaultdict +import asyncio +import inspect +import io +import logging +import os.path + +from ruamel.yaml import YAML +from ruamel.yaml.comments import CommentedMap + +from mautrix.types import UserID +from mautrix.util import background_task +from mautrix.util.async_db import Database, Scheme, UpgradeTable +from mautrix.util.async_getter_lock import async_getter_lock +from mautrix.util.config import BaseProxyConfig, RecursiveDict +from mautrix.util.logging import TraceLogger + +from .client import Client +from .db import DatabaseEngine, Instance as DBInstance +from .lib.optionalalchemy import Engine, MetaData, create_engine +from .lib.plugin_db import ProxyPostgresDatabase +from .loader import DatabaseType, PluginLoader, ZippedPluginLoader +from .plugin_base import Plugin + +if TYPE_CHECKING: + from .__main__ import Maubot + from .server import PluginWebApp + +log: TraceLogger = cast(TraceLogger, logging.getLogger("maubot.instance")) +db_log: TraceLogger = cast(TraceLogger, logging.getLogger("maubot.instance_db")) + +yaml = YAML() +yaml.indent(4) +yaml.width = 200 + + +class PluginInstance(DBInstance): + maubot: "Maubot" = None + cache: dict[str, PluginInstance] = {} + plugin_directories: list[str] = [] + _async_get_locks: dict[Any, asyncio.Lock] = defaultdict(lambda: asyncio.Lock()) + + log: logging.Logger + loader: PluginLoader | None + client: Client | None + plugin: Plugin | None + config: BaseProxyConfig | None + base_cfg: RecursiveDict[CommentedMap] | None + base_cfg_str: str | None + inst_db: sql.engine.Engine | Database | None + inst_db_tables: dict | None + inst_webapp: PluginWebApp | None + inst_webapp_url: str | None + started: bool + + def __init__( + self, + id: str, + type: str, + enabled: bool, + primary_user: UserID, + config: str = "", + database_engine: DatabaseEngine | None = None, + ) -> None: + super().__init__( + id=id, + type=type, + enabled=bool(enabled), + primary_user=primary_user, + config_str=config, + database_engine=database_engine, + ) + + def __hash__(self) -> int: + return hash(self.id) + + @classmethod + def init_cls(cls, maubot: "Maubot") -> None: + cls.maubot = maubot + + def postinit(self) -> None: + self.log = log.getChild(self.id) + self.cache[self.id] = self + self.config = None + self.started = False + self.loader = None + self.client = None + self.plugin = None + self.inst_db = None + self.inst_db_tables = None + self.inst_webapp = None + self.inst_webapp_url = None + self.base_cfg = None + self.base_cfg_str = None + + def to_dict(self) -> dict: + return { + "id": self.id, + "type": self.type, + "enabled": self.enabled, + "started": self.started, + "primary_user": self.primary_user, + "config": self.config_str, + "base_config": self.base_cfg_str, + "database": ( + self.inst_db is not None and self.maubot.config["api_features.instance_database"] + ), + "database_interface": self.loader.meta.database_type_str if self.loader else "unknown", + "database_engine": self.database_engine_str, + } + + def _introspect_sqlalchemy(self) -> dict: + metadata = MetaData() + metadata.reflect(self.inst_db) + return { + table.name: { + "columns": { + column.name: { + "type": str(column.type), + "unique": column.unique or False, + "default": column.default, + "nullable": column.nullable, + "primary": column.primary_key, + } + for column in table.columns + }, + } + for table in metadata.tables.values() + } + + async def _introspect_sqlite(self) -> dict: + q = """ + SELECT + m.name AS table_name, + p.cid AS col_id, + p.name AS column_name, + p.type AS data_type, + p.pk AS is_primary, + p.dflt_value AS column_default, + p.[notnull] AS is_nullable + FROM sqlite_master m + LEFT JOIN pragma_table_info((m.name)) p + WHERE m.type = 'table' + ORDER BY table_name, col_id + """ + data = await self.inst_db.fetch(q) + tables = defaultdict(lambda: {"columns": {}}) + for column in data: + table_name = column["table_name"] + col_name = column["column_name"] + tables[table_name]["columns"][col_name] = { + "type": column["data_type"], + "nullable": bool(column["is_nullable"]), + "default": column["column_default"], + "primary": bool(column["is_primary"]), + # TODO uniqueness? + } + return tables + + async def _introspect_postgres(self) -> dict: + assert isinstance(self.inst_db, ProxyPostgresDatabase) + q = """ + SELECT col.table_name, col.column_name, col.data_type, col.is_nullable, col.column_default, + tc.constraint_type + FROM information_schema.columns col + LEFT JOIN information_schema.constraint_column_usage ccu + ON ccu.column_name=col.column_name + LEFT JOIN information_schema.table_constraints tc + ON col.table_name=tc.table_name + AND col.table_schema=tc.table_schema + AND ccu.constraint_name=tc.constraint_name + AND ccu.constraint_schema=tc.constraint_schema + AND tc.constraint_type IN ('PRIMARY KEY', 'UNIQUE') + WHERE col.table_schema=$1 + """ + data = await self.inst_db.fetch(q, self.inst_db.schema_name) + tables = defaultdict(lambda: {"columns": {}}) + for column in data: + table_name = column["table_name"] + col_name = column["column_name"] + tables[table_name]["columns"].setdefault( + col_name, + { + "type": column["data_type"], + "nullable": column["is_nullable"], + "default": column["column_default"], + "primary": False, + "unique": False, + }, + ) + if column["constraint_type"] == "PRIMARY KEY": + tables[table_name]["columns"][col_name]["primary"] = True + elif column["constraint_type"] == "UNIQUE": + tables[table_name]["columns"][col_name]["unique"] = True + return tables + + async def get_db_tables(self) -> dict: + if self.inst_db_tables is None: + if isinstance(self.inst_db, Engine): + self.inst_db_tables = self._introspect_sqlalchemy() + elif self.inst_db.scheme == Scheme.SQLITE: + self.inst_db_tables = await self._introspect_sqlite() + else: + self.inst_db_tables = await self._introspect_postgres() + return self.inst_db_tables + + async def load(self) -> bool: + if not self.loader: + try: + self.loader = PluginLoader.find(self.type) + except KeyError: + self.log.error(f"Failed to find loader for type {self.type}") + await self.update_enabled(False) + return False + if not self.client: + self.client = await Client.get(self.primary_user) + if not self.client: + self.log.error(f"Failed to get client for user {self.primary_user}") + await self.update_enabled(False) + return False + if self.loader.meta.webapp: + self.enable_webapp() + self.log.debug("Plugin instance dependencies loaded") + self.loader.references.add(self) + self.client.references.add(self) + return True + + def enable_webapp(self) -> None: + self.inst_webapp, self.inst_webapp_url = self.maubot.server.get_instance_subapp(self.id) + + def disable_webapp(self) -> None: + self.maubot.server.remove_instance_webapp(self.id) + self.inst_webapp = None + self.inst_webapp_url = None + + @property + def _sqlite_db_path(self) -> str: + return os.path.join(self.maubot.config["plugin_databases.sqlite"], f"{self.id}.db") + + async def delete(self) -> None: + if self.loader is not None: + self.loader.references.remove(self) + if self.client is not None: + self.client.references.remove(self) + try: + del self.cache[self.id] + except KeyError: + pass + await super().delete() + if self.inst_db: + await self.stop_database() + await self.delete_database() + if self.inst_webapp: + self.disable_webapp() + + def load_config(self) -> CommentedMap: + return yaml.load(self.config_str) + + def save_config(self, data: RecursiveDict[CommentedMap]) -> None: + buf = io.StringIO() + yaml.dump(data, buf) + val = buf.getvalue() + if val != self.config_str: + self.config_str = val + self.log.debug("Creating background task to save updated config") + background_task.create(self.update()) + + async def start_database( + self, upgrade_table: UpgradeTable | None = None, actually_start: bool = True + ) -> None: + if self.loader.meta.database_type == DatabaseType.SQLALCHEMY: + if self.database_engine is None: + await self.update_db_engine(DatabaseEngine.SQLITE) + elif self.database_engine == DatabaseEngine.POSTGRES: + raise RuntimeError( + "Instance database engine is marked as Postgres, but plugin uses legacy " + "database interface, which doesn't support postgres." + ) + self.inst_db = create_engine(f"sqlite:///{self._sqlite_db_path}") + elif self.loader.meta.database_type == DatabaseType.ASYNCPG: + if self.database_engine is None: + if os.path.exists(self._sqlite_db_path) or not self.maubot.plugin_postgres_db: + await self.update_db_engine(DatabaseEngine.SQLITE) + else: + await self.update_db_engine(DatabaseEngine.POSTGRES) + instance_db_log = db_log.getChild(self.id) + if self.database_engine == DatabaseEngine.POSTGRES: + if not self.maubot.plugin_postgres_db: + raise RuntimeError( + "Instance database engine is marked as Postgres, but this maubot isn't " + "configured to support Postgres for plugin databases" + ) + self.inst_db = ProxyPostgresDatabase( + pool=self.maubot.plugin_postgres_db, + instance_id=self.id, + max_conns=self.maubot.config["plugin_databases.postgres_max_conns_per_plugin"], + upgrade_table=upgrade_table, + log=instance_db_log, + ) + else: + self.inst_db = Database.create( + f"sqlite:{self._sqlite_db_path}", + upgrade_table=upgrade_table, + log=instance_db_log, + ) + if actually_start: + await self.inst_db.start() + else: + raise RuntimeError(f"Unrecognized database type {self.loader.meta.database_type}") + + async def stop_database(self) -> None: + if isinstance(self.inst_db, Database): + await self.inst_db.stop() + elif isinstance(self.inst_db, Engine): + self.inst_db.dispose() + else: + raise RuntimeError(f"Unknown database type {type(self.inst_db).__name__}") + + async def delete_database(self) -> None: + if self.loader.meta.database_type == DatabaseType.SQLALCHEMY: + ZippedPluginLoader.trash(self._sqlite_db_path, reason="deleted") + elif self.loader.meta.database_type == DatabaseType.ASYNCPG: + if self.inst_db is None: + await self.start_database(None, actually_start=False) + if isinstance(self.inst_db, ProxyPostgresDatabase): + await self.inst_db.delete() + else: + ZippedPluginLoader.trash(self._sqlite_db_path, reason="deleted") + else: + raise RuntimeError(f"Unrecognized database type {self.loader.meta.database_type}") + self.inst_db = None + + async def start(self) -> None: + if self.started: + self.log.warning("Ignoring start() call to already started plugin") + return + elif not self.enabled: + self.log.warning("Plugin disabled, not starting.") + return + if not self.client or not self.loader: + self.log.warning("Missing plugin instance dependencies, attempting to load...") + if not await self.load(): + return + cls = await self.loader.load() + if self.loader.meta.webapp and self.inst_webapp is None: + self.log.debug("Enabling webapp after plugin meta reload") + self.enable_webapp() + elif not self.loader.meta.webapp and self.inst_webapp is not None: + self.log.debug("Disabling webapp after plugin meta reload") + self.disable_webapp() + if self.loader.meta.database: + try: + await self.start_database(cls.get_db_upgrade_table()) + except Exception: + self.log.exception("Failed to start instance database") + await self.update_enabled(False) + return + config_class = cls.get_config_class() + if config_class: + try: + base = await self.loader.read_file("base-config.yaml") + self.base_cfg = RecursiveDict(yaml.load(base.decode("utf-8")), CommentedMap) + buf = io.StringIO() + yaml.dump(self.base_cfg._data, buf) + self.base_cfg_str = buf.getvalue() + except (FileNotFoundError, KeyError): + self.base_cfg = None + self.base_cfg_str = None + if self.base_cfg: + base_cfg_func = self.base_cfg.clone + else: + + def base_cfg_func() -> None: + return None + + self.config = config_class(self.load_config, base_cfg_func, self.save_config) + self.plugin = cls( + client=self.client.client, + loop=self.maubot.loop, + http=self.client.http_client, + instance_id=self.id, + log=self.log, + config=self.config, + database=self.inst_db, + loader=self.loader, + webapp=self.inst_webapp, + webapp_url=self.inst_webapp_url, + ) + try: + await self.plugin.internal_start() + except Exception: + self.log.exception("Failed to start instance") + await self.update_enabled(False) + return + self.started = True + self.inst_db_tables = None + self.log.info( + f"Started instance of {self.loader.meta.id} v{self.loader.meta.version} " + f"with user {self.client.id}" + ) + + async def stop(self) -> None: + if not self.started: + self.log.warning("Ignoring stop() call to non-running plugin") + return + self.log.debug("Stopping plugin instance...") + self.started = False + try: + await self.plugin.internal_stop() + except Exception: + self.log.exception("Failed to stop instance") + self.plugin = None + if self.inst_db: + try: + await self.stop_database() + except Exception: + self.log.exception("Failed to stop instance database") + self.inst_db_tables = None + + async def update_id(self, new_id: str | None) -> None: + if new_id is not None and new_id.lower() != self.id: + await super().update_id(new_id.lower()) + + async def update_config(self, config: str | None) -> None: + if config is None or self.config_str == config: + return + self.config_str = config + if self.started and self.plugin is not None: + res = self.plugin.on_external_config_update() + if inspect.isawaitable(res): + await res + await self.update() + + async def update_primary_user(self, primary_user: UserID | None) -> bool: + if primary_user is None or primary_user == self.primary_user: + return True + client = await Client.get(primary_user) + if not client: + return False + await self.stop() + self.primary_user = client.id + if self.client: + self.client.references.remove(self) + self.client = client + self.client.references.add(self) + await self.update() + await self.start() + self.log.debug(f"Primary user switched to {self.client.id}") + return True + + async def update_type(self, type: str | None) -> bool: + if type is None or type == self.type: + return True + try: + loader = PluginLoader.find(type) + except KeyError: + return False + await self.stop() + self.type = loader.meta.id + if self.loader: + self.loader.references.remove(self) + self.loader = loader + self.loader.references.add(self) + await self.update() + await self.start() + self.log.debug(f"Type switched to {self.loader.meta.id}") + return True + + async def update_started(self, started: bool) -> None: + if started is not None and started != self.started: + await (self.start() if started else self.stop()) + + async def update_enabled(self, enabled: bool) -> None: + if enabled is not None and enabled != self.enabled: + self.enabled = enabled + await self.update() + + async def update_db_engine(self, db_engine: DatabaseEngine | None) -> None: + if db_engine is not None and db_engine != self.database_engine: + self.database_engine = db_engine + await self.update() + + @classmethod + @async_getter_lock + async def get( + cls, instance_id: str, *, type: str | None = None, primary_user: UserID | None = None + ) -> PluginInstance | None: + try: + return cls.cache[instance_id] + except KeyError: + pass + + instance = cast(cls, await super().get(instance_id)) + if instance is not None: + instance.postinit() + return instance + + if type and primary_user: + instance = cls(instance_id, type=type, enabled=True, primary_user=primary_user) + await instance.insert() + instance.postinit() + return instance + + return None + + @classmethod + async def all(cls) -> AsyncGenerator[PluginInstance, None]: + instances = await super().all() + instance: PluginInstance + for instance in instances: + try: + yield cls.cache[instance.id] + except KeyError: + instance.postinit() + yield instance diff --git a/maubot-src/maubot/lib/__init__.py b/maubot-src/maubot/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/maubot-src/maubot/lib/color_log.py b/maubot-src/maubot/lib/color_log.py new file mode 100644 index 0000000..8c36ed5 --- /dev/null +++ b/maubot-src/maubot/lib/color_log.py @@ -0,0 +1,49 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from mautrix.util.logging.color import ( + MAU_COLOR, + MXID_COLOR, + PREFIX, + RESET, + ColorFormatter as BaseColorFormatter, +) + +INST_COLOR = PREFIX + "35m" # magenta +LOADER_COLOR = PREFIX + "36m" # blue + + +class ColorFormatter(BaseColorFormatter): + def _color_name(self, module: str) -> str: + client = "maubot.client" + if module.startswith(client + "."): + suffix = "" + if module.endswith(".crypto"): + suffix = f".{MAU_COLOR}crypto{RESET}" + module = module[: -len(".crypto")] + module = module[len(client) + 1 :] + return f"{MAU_COLOR}{client}{RESET}.{MXID_COLOR}{module}{RESET}{suffix}" + instance = "maubot.instance" + if module.startswith(instance + "."): + return f"{MAU_COLOR}{instance}{RESET}.{INST_COLOR}{module[len(instance) + 1:]}{RESET}" + instance_db = "maubot.instance_db" + if module.startswith(instance_db + "."): + return f"{MAU_COLOR}{instance_db}{RESET}.{INST_COLOR}{module[len(instance_db) + 1:]}{RESET}" + loader = "maubot.loader" + if module.startswith(loader + "."): + return f"{MAU_COLOR}{instance}{RESET}.{LOADER_COLOR}{module[len(loader) + 1:]}{RESET}" + if module.startswith("maubot."): + return f"{MAU_COLOR}{module}{RESET}" + return super()._color_name(module) diff --git a/maubot-src/maubot/lib/future_awaitable.py b/maubot-src/maubot/lib/future_awaitable.py new file mode 100644 index 0000000..388eae9 --- /dev/null +++ b/maubot-src/maubot/lib/future_awaitable.py @@ -0,0 +1,9 @@ +from typing import Any, Awaitable, Callable, Generator + + +class FutureAwaitable: + def __init__(self, func: Callable[[], Awaitable[None]]) -> None: + self._func = func + + def __await__(self) -> Generator[Any, None, None]: + return self._func().__await__() diff --git a/maubot-src/maubot/lib/optionalalchemy.py b/maubot-src/maubot/lib/optionalalchemy.py new file mode 100644 index 0000000..ba94271 --- /dev/null +++ b/maubot-src/maubot/lib/optionalalchemy.py @@ -0,0 +1,19 @@ +try: + from sqlalchemy import MetaData, asc, create_engine, desc + from sqlalchemy.engine import Engine + from sqlalchemy.exc import IntegrityError, OperationalError +except ImportError: + + class FakeError(Exception): + pass + + class FakeType: + def __init__(self, *args, **kwargs): + raise Exception("SQLAlchemy is not installed") + + def create_engine(*args, **kwargs): + raise Exception("SQLAlchemy is not installed") + + MetaData = Engine = FakeType + IntegrityError = OperationalError = FakeError + asc = desc = lambda a: a diff --git a/maubot-src/maubot/lib/plugin_db.py b/maubot-src/maubot/lib/plugin_db.py new file mode 100644 index 0000000..af21619 --- /dev/null +++ b/maubot-src/maubot/lib/plugin_db.py @@ -0,0 +1,100 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +from contextlib import asynccontextmanager +import asyncio + +from mautrix.util.async_db import Database, PostgresDatabase, Scheme, UpgradeTable +from mautrix.util.async_db.connection import LoggingConnection +from mautrix.util.logging import TraceLogger + +remove_double_quotes = str.maketrans({'"': "_"}) + + +class ProxyPostgresDatabase(Database): + scheme = Scheme.POSTGRES + _underlying_pool: PostgresDatabase + schema_name: str + _quoted_schema: str + _default_search_path: str + _conn_sema: asyncio.Semaphore + _max_conns: int + + def __init__( + self, + pool: PostgresDatabase, + instance_id: str, + max_conns: int, + upgrade_table: UpgradeTable | None, + log: TraceLogger | None = None, + ) -> None: + super().__init__(pool.url, upgrade_table=upgrade_table, log=log) + self._underlying_pool = pool + # Simple accidental SQL injection prevention. + # Doesn't have to be perfect, since plugin instance IDs can only be set by admins anyway. + self.schema_name = f"mbp_{instance_id.translate(remove_double_quotes)}" + self._quoted_schema = f'"{self.schema_name}"' + self._default_search_path = '"$user", public' + self._conn_sema = asyncio.BoundedSemaphore(max_conns) + self._max_conns = max_conns + + async def start(self) -> None: + async with self._underlying_pool.acquire_direct() as conn: + self._default_search_path = await conn.fetchval("SHOW search_path") + self.log.trace(f"Found default search path: {self._default_search_path}") + await conn.execute(f"CREATE SCHEMA IF NOT EXISTS {self._quoted_schema}") + await super().start() + + async def stop(self) -> None: + for _ in range(self._max_conns): + try: + await asyncio.wait_for(self._conn_sema.acquire(), timeout=3) + except asyncio.TimeoutError: + self.log.warning( + "Failed to drain plugin database connection pool, " + "the plugin may be leaking database connections" + ) + break + + async def delete(self) -> None: + self.log.info(f"Deleting schema {self.schema_name} and all data in it") + try: + await self._underlying_pool.execute( + f"DROP SCHEMA IF EXISTS {self._quoted_schema} CASCADE" + ) + except Exception: + self.log.warning("Failed to delete schema", exc_info=True) + + @asynccontextmanager + async def acquire_direct(self) -> LoggingConnection: + conn: LoggingConnection + async with self._conn_sema, self._underlying_pool.acquire_direct() as conn: + await conn.execute(f"SET search_path = {self._quoted_schema}") + try: + yield conn + finally: + if not conn.wrapped.is_closed(): + try: + await conn.execute(f"SET search_path = {self._default_search_path}") + except Exception: + self.log.exception("Error resetting search_path after use") + await conn.wrapped.close() + else: + self.log.debug("Connection was closed after use, not resetting search_path") + + +__all__ = ["ProxyPostgresDatabase"] diff --git a/maubot-src/maubot/lib/state_store.py b/maubot-src/maubot/lib/state_store.py new file mode 100644 index 0000000..81fb5fd --- /dev/null +++ b/maubot-src/maubot/lib/state_store.py @@ -0,0 +1,27 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from mautrix.client.state_store.asyncpg import PgStateStore as BasePgStateStore + +try: + from mautrix.crypto import StateStore as CryptoStateStore + + class PgStateStore(BasePgStateStore, CryptoStateStore): + pass + +except ImportError as e: + PgStateStore = BasePgStateStore + +__all__ = ["PgStateStore"] diff --git a/maubot-src/maubot/lib/zipimport.py b/maubot-src/maubot/lib/zipimport.py new file mode 100644 index 0000000..e7b77db --- /dev/null +++ b/maubot-src/maubot/lib/zipimport.py @@ -0,0 +1,794 @@ +# The pure Python implementation of zipimport in Python 3.8+. Slightly modified to allow clearing +# the zip directory cache to bypass https://bugs.python.org/issue19081 +# +# https://github.com/python/cpython/blob/5a5ce064b3baadcb79605c5a42ee3d0aee57cdfc/Lib/zipimport.py +# See license at https://github.com/python/cpython/blob/master/LICENSE + +"""zipimport provides support for importing Python modules from Zip archives. + +This module exports three objects: +- zipimporter: a class; its constructor takes a path to a Zip archive. +- ZipImportError: exception raised by zipimporter objects. It's a + subclass of ImportError, so it can be caught as ImportError, too. +- _zip_directory_cache: a dict, mapping archive paths to zip directory + info dicts, as used in zipimporter._files. + +It is usually not needed to use the zipimport module explicitly; it is +used by the builtin import mechanism for sys.path items that are paths +to Zip archives. +""" + +from importlib import _bootstrap # for _verbose_message +from importlib import _bootstrap_external +import marshal # for loads +import sys # for modules +import time # for mktime + +import _imp # for check_hash_based_pycs +import _io # for open + +__all__ = ["ZipImportError", "zipimporter"] + + +def _unpack_uint32(data): + """Convert 4 bytes in little-endian to an integer.""" + assert len(data) == 4 + return int.from_bytes(data, "little") + + +def _unpack_uint16(data): + """Convert 2 bytes in little-endian to an integer.""" + assert len(data) == 2 + return int.from_bytes(data, "little") + + +path_sep = _bootstrap_external.path_sep +alt_path_sep = _bootstrap_external.path_separators[1:] + + +class ZipImportError(ImportError): + pass + + +# _read_directory() cache +_zip_directory_cache = {} + +_module_type = type(sys) + +END_CENTRAL_DIR_SIZE = 22 +STRING_END_ARCHIVE = b"PK\x05\x06" +MAX_COMMENT_LEN = (1 << 16) - 1 + + +class zipimporter: + """zipimporter(archivepath) -> zipimporter object + + Create a new zipimporter instance. 'archivepath' must be a path to + a zipfile, or to a specific path inside a zipfile. For example, it can be + '/tmp/myimport.zip', or '/tmp/myimport.zip/mydirectory', if mydirectory is a + valid directory inside the archive. + + 'ZipImportError is raised if 'archivepath' doesn't point to a valid Zip + archive. + + The 'archive' attribute of zipimporter objects contains the name of the + zipfile targeted. + """ + + # Split the "subdirectory" from the Zip archive path, lookup a matching + # entry in sys.path_importer_cache, fetch the file directory from there + # if found, or else read it from the archive. + def __init__(self, path): + if not isinstance(path, str): + import os + + path = os.fsdecode(path) + if not path: + raise ZipImportError("archive path is empty", path=path) + if alt_path_sep: + path = path.replace(alt_path_sep, path_sep) + + prefix = [] + while True: + try: + st = _bootstrap_external._path_stat(path) + except (OSError, ValueError): + # On Windows a ValueError is raised for too long paths. + # Back up one path element. + dirname, basename = _bootstrap_external._path_split(path) + if dirname == path: + raise ZipImportError("not a Zip file", path=path) + path = dirname + prefix.append(basename) + else: + # it exists + if (st.st_mode & 0o170000) != 0o100000: # stat.S_ISREG + # it's a not file + raise ZipImportError("not a Zip file", path=path) + break + + try: + files = _zip_directory_cache[path] + except KeyError: + files = _read_directory(path) + _zip_directory_cache[path] = files + self._files = files + self.archive = path + # a prefix directory following the ZIP file path. + self.prefix = _bootstrap_external._path_join(*prefix[::-1]) + if self.prefix: + self.prefix += path_sep + + def reset_cache(self): + self._files = _read_directory(self.archive) + _zip_directory_cache[self.archive] = self._files + + def remove_cache(self): + try: + del _zip_directory_cache[self.archive] + except KeyError: + pass + + # Check whether we can satisfy the import of the module named by + # 'fullname', or whether it could be a portion of a namespace + # package. Return self if we can load it, a string containing the + # full path if it's a possible namespace portion, None if we + # can't load it. + def find_loader(self, fullname, path=None): + """find_loader(fullname, path=None) -> self, str or None. + + Search for a module specified by 'fullname'. 'fullname' must be the + fully qualified (dotted) module name. It returns the zipimporter + instance itself if the module was found, a string containing the + full path name if it's possibly a portion of a namespace package, + or None otherwise. The optional 'path' argument is ignored -- it's + there for compatibility with the importer protocol. + """ + mi = _get_module_info(self, fullname) + if mi is not None: + # This is a module or package. + return self, [] + + # Not a module or regular package. See if this is a directory, and + # therefore possibly a portion of a namespace package. + + # We're only interested in the last path component of fullname + # earlier components are recorded in self.prefix. + modpath = _get_module_path(self, fullname) + if _is_dir(self, modpath): + # This is possibly a portion of a namespace + # package. Return the string representing its path, + # without a trailing separator. + return None, [f"{self.archive}{path_sep}{modpath}"] + + return None, [] + + # Check whether we can satisfy the import of the module named by + # 'fullname'. Return self if we can, None if we can't. + def find_module(self, fullname, path=None): + """find_module(fullname, path=None) -> self or None. + + Search for a module specified by 'fullname'. 'fullname' must be the + fully qualified (dotted) module name. It returns the zipimporter + instance itself if the module was found, or None if it wasn't. + The optional 'path' argument is ignored -- it's there for compatibility + with the importer protocol. + """ + return self.find_loader(fullname, path)[0] + + def get_code(self, fullname): + """get_code(fullname) -> code object. + + Return the code object for the specified module. Raise ZipImportError + if the module couldn't be found. + """ + code, ispackage, modpath = _get_module_code(self, fullname) + return code + + def get_data(self, pathname): + """get_data(pathname) -> string with file data. + + Return the data associated with 'pathname'. Raise OSError if + the file wasn't found. + """ + if alt_path_sep: + pathname = pathname.replace(alt_path_sep, path_sep) + + key = pathname + if pathname.startswith(self.archive + path_sep): + key = pathname[len(self.archive + path_sep) :] + + try: + toc_entry = self._files[key] + except KeyError: + raise OSError(0, "", key) + return _get_data(self.archive, toc_entry) + + # Return a string matching __file__ for the named module + def get_filename(self, fullname): + """get_filename(fullname) -> filename string. + + Return the filename for the specified module. + """ + # Deciding the filename requires working out where the code + # would come from if the module was actually loaded + code, ispackage, modpath = _get_module_code(self, fullname) + return modpath + + def get_source(self, fullname): + """get_source(fullname) -> source string. + + Return the source code for the specified module. Raise ZipImportError + if the module couldn't be found, return None if the archive does + contain the module, but has no source for it. + """ + mi = _get_module_info(self, fullname) + if mi is None: + raise ZipImportError(f"can't find module {fullname!r}", name=fullname) + + path = _get_module_path(self, fullname) + if mi: + fullpath = _bootstrap_external._path_join(path, "__init__.py") + else: + fullpath = f"{path}.py" + + try: + toc_entry = self._files[fullpath] + except KeyError: + # we have the module, but no source + return None + return _get_data(self.archive, toc_entry).decode() + + # Return a bool signifying whether the module is a package or not. + def is_package(self, fullname): + """is_package(fullname) -> bool. + + Return True if the module specified by fullname is a package. + Raise ZipImportError if the module couldn't be found. + """ + mi = _get_module_info(self, fullname) + if mi is None: + raise ZipImportError(f"can't find module {fullname!r}", name=fullname) + return mi + + # Load and return the module named by 'fullname'. + def load_module(self, fullname): + """load_module(fullname) -> module. + + Load the module specified by 'fullname'. 'fullname' must be the + fully qualified (dotted) module name. It returns the imported + module, or raises ZipImportError if it wasn't found. + """ + code, ispackage, modpath = _get_module_code(self, fullname) + mod = sys.modules.get(fullname) + if mod is None or not isinstance(mod, _module_type): + mod = _module_type(fullname) + sys.modules[fullname] = mod + mod.__loader__ = self + + try: + if ispackage: + # add __path__ to the module *before* the code gets + # executed + path = _get_module_path(self, fullname) + fullpath = _bootstrap_external._path_join(self.archive, path) + mod.__path__ = [fullpath] + + if not hasattr(mod, "__builtins__"): + mod.__builtins__ = __builtins__ + _bootstrap_external._fix_up_module(mod.__dict__, fullname, modpath) + exec(code, mod.__dict__) + except: + del sys.modules[fullname] + raise + + try: + mod = sys.modules[fullname] + except KeyError: + raise ImportError(f"Loaded module {fullname!r} not found in sys.modules") + _bootstrap._verbose_message("import {} # loaded from Zip {}", fullname, modpath) + return mod + + def get_resource_reader(self, fullname): + """Return the ResourceReader for a package in a zip file. + + If 'fullname' is a package within the zip file, return the + 'ResourceReader' object for the package. Otherwise return None. + """ + try: + if not self.is_package(fullname): + return None + except ZipImportError: + return None + if not _ZipImportResourceReader._registered: + from importlib.abc import ResourceReader + + ResourceReader.register(_ZipImportResourceReader) + _ZipImportResourceReader._registered = True + return _ZipImportResourceReader(self, fullname) + + def __repr__(self): + return f'' + + +# _zip_searchorder defines how we search for a module in the Zip +# archive: we first search for a package __init__, then for +# non-package .pyc, and .py entries. The .pyc entries +# are swapped by initzipimport() if we run in optimized mode. Also, +# '/' is replaced by path_sep there. +_zip_searchorder = ( + (path_sep + "__init__.pyc", True, True), + (path_sep + "__init__.py", False, True), + (".pyc", True, False), + (".py", False, False), +) + + +# Given a module name, return the potential file path in the +# archive (without extension). +def _get_module_path(self, fullname): + return self.prefix + fullname.rpartition(".")[2] + + +# Does this path represent a directory? +def _is_dir(self, path): + # See if this is a "directory". If so, it's eligible to be part + # of a namespace package. We test by seeing if the name, with an + # appended path separator, exists. + dirpath = path + path_sep + # If dirpath is present in self._files, we have a directory. + return dirpath in self._files + + +# Return some information about a module. +def _get_module_info(self, fullname): + path = _get_module_path(self, fullname) + for suffix, isbytecode, ispackage in _zip_searchorder: + fullpath = path + suffix + if fullpath in self._files: + return ispackage + return None + + +# implementation + + +# _read_directory(archive) -> files dict (new reference) +# +# Given a path to a Zip archive, build a dict, mapping file names +# (local to the archive, using SEP as a separator) to toc entries. +# +# A toc_entry is a tuple: +# +# (__file__, # value to use for __file__, available for all files, +# # encoded to the filesystem encoding +# compress, # compression kind; 0 for uncompressed +# data_size, # size of compressed data on disk +# file_size, # size of decompressed data +# file_offset, # offset of file header from start of archive +# time, # mod time of file (in dos format) +# date, # mod data of file (in dos format) +# crc, # crc checksum of the data +# ) +# +# Directories can be recognized by the trailing path_sep in the name, +# data_size and file_offset are 0. +def _read_directory(archive): + try: + fp = _io.open(archive, "rb") + except OSError: + raise ZipImportError(f"can't open Zip file: {archive!r}", path=archive) + + with fp: + try: + fp.seek(-END_CENTRAL_DIR_SIZE, 2) + header_position = fp.tell() + buffer = fp.read(END_CENTRAL_DIR_SIZE) + except OSError: + raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive) + if len(buffer) != END_CENTRAL_DIR_SIZE: + raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive) + if buffer[:4] != STRING_END_ARCHIVE: + # Bad: End of Central Dir signature + # Check if there's a comment. + try: + fp.seek(0, 2) + file_size = fp.tell() + except OSError: + raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive) + max_comment_start = max(file_size - MAX_COMMENT_LEN - END_CENTRAL_DIR_SIZE, 0) + try: + fp.seek(max_comment_start) + data = fp.read() + except OSError: + raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive) + pos = data.rfind(STRING_END_ARCHIVE) + if pos < 0: + raise ZipImportError(f"not a Zip file: {archive!r}", path=archive) + buffer = data[pos : pos + END_CENTRAL_DIR_SIZE] + if len(buffer) != END_CENTRAL_DIR_SIZE: + raise ZipImportError(f"corrupt Zip file: {archive!r}", path=archive) + header_position = file_size - len(data) + pos + + header_size = _unpack_uint32(buffer[12:16]) + header_offset = _unpack_uint32(buffer[16:20]) + if header_position < header_size: + raise ZipImportError(f"bad central directory size: {archive!r}", path=archive) + if header_position < header_offset: + raise ZipImportError(f"bad central directory offset: {archive!r}", path=archive) + header_position -= header_size + arc_offset = header_position - header_offset + if arc_offset < 0: + raise ZipImportError( + f"bad central directory size or offset: {archive!r}", path=archive + ) + + files = {} + # Start of Central Directory + count = 0 + try: + fp.seek(header_position) + except OSError: + raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive) + while True: + buffer = fp.read(46) + if len(buffer) < 4: + raise EOFError("EOF read where not expected") + # Start of file header + if buffer[:4] != b"PK\x01\x02": + break # Bad: Central Dir File Header + if len(buffer) != 46: + raise EOFError("EOF read where not expected") + flags = _unpack_uint16(buffer[8:10]) + compress = _unpack_uint16(buffer[10:12]) + time = _unpack_uint16(buffer[12:14]) + date = _unpack_uint16(buffer[14:16]) + crc = _unpack_uint32(buffer[16:20]) + data_size = _unpack_uint32(buffer[20:24]) + file_size = _unpack_uint32(buffer[24:28]) + name_size = _unpack_uint16(buffer[28:30]) + extra_size = _unpack_uint16(buffer[30:32]) + comment_size = _unpack_uint16(buffer[32:34]) + file_offset = _unpack_uint32(buffer[42:46]) + header_size = name_size + extra_size + comment_size + if file_offset > header_offset: + raise ZipImportError(f"bad local header offset: {archive!r}", path=archive) + file_offset += arc_offset + + try: + name = fp.read(name_size) + except OSError: + raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive) + if len(name) != name_size: + raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive) + # On Windows, calling fseek to skip over the fields we don't use is + # slower than reading the data because fseek flushes stdio's + # internal buffers. See issue #8745. + try: + if len(fp.read(header_size - name_size)) != header_size - name_size: + raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive) + except OSError: + raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive) + + if flags & 0x800: + # UTF-8 file names extension + name = name.decode() + else: + # Historical ZIP filename encoding + try: + name = name.decode("ascii") + except UnicodeDecodeError: + name = name.decode("latin1").translate(cp437_table) + + name = name.replace("/", path_sep) + path = _bootstrap_external._path_join(archive, name) + t = (path, compress, data_size, file_size, file_offset, time, date, crc) + files[name] = t + count += 1 + _bootstrap._verbose_message("zipimport: found {} names in {!r}", count, archive) + return files + + +# During bootstrap, we may need to load the encodings +# package from a ZIP file. But the cp437 encoding is implemented +# in Python in the encodings package. +# +# Break out of this dependency by using the translation table for +# the cp437 encoding. +cp437_table = ( + # ASCII part, 8 rows x 16 chars + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f" + "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" + " !\"#$%&'()*+,-./" + "0123456789:;<=>?" + "@ABCDEFGHIJKLMNO" + "PQRSTUVWXYZ[\\]^_" + "`abcdefghijklmno" + "pqrstuvwxyz{|}~\x7f" + # non-ASCII part, 16 rows x 8 chars + "\xc7\xfc\xe9\xe2\xe4\xe0\xe5\xe7" + "\xea\xeb\xe8\xef\xee\xec\xc4\xc5" + "\xc9\xe6\xc6\xf4\xf6\xf2\xfb\xf9" + "\xff\xd6\xdc\xa2\xa3\xa5\u20a7\u0192" + "\xe1\xed\xf3\xfa\xf1\xd1\xaa\xba" + "\xbf\u2310\xac\xbd\xbc\xa1\xab\xbb" + "\u2591\u2592\u2593\u2502\u2524\u2561\u2562\u2556" + "\u2555\u2563\u2551\u2557\u255d\u255c\u255b\u2510" + "\u2514\u2534\u252c\u251c\u2500\u253c\u255e\u255f" + "\u255a\u2554\u2569\u2566\u2560\u2550\u256c\u2567" + "\u2568\u2564\u2565\u2559\u2558\u2552\u2553\u256b" + "\u256a\u2518\u250c\u2588\u2584\u258c\u2590\u2580" + "\u03b1\xdf\u0393\u03c0\u03a3\u03c3\xb5\u03c4" + "\u03a6\u0398\u03a9\u03b4\u221e\u03c6\u03b5\u2229" + "\u2261\xb1\u2265\u2264\u2320\u2321\xf7\u2248" + "\xb0\u2219\xb7\u221a\u207f\xb2\u25a0\xa0" +) + +_importing_zlib = False + + +# Return the zlib.decompress function object, or NULL if zlib couldn't +# be imported. The function is cached when found, so subsequent calls +# don't import zlib again. +def _get_decompress_func(): + global _importing_zlib + if _importing_zlib: + # Someone has a zlib.py[co] in their Zip file + # let's avoid a stack overflow. + _bootstrap._verbose_message("zipimport: zlib UNAVAILABLE") + raise ZipImportError("can't decompress data; zlib not available") + + _importing_zlib = True + try: + from zlib import decompress + except Exception: + _bootstrap._verbose_message("zipimport: zlib UNAVAILABLE") + raise ZipImportError("can't decompress data; zlib not available") + finally: + _importing_zlib = False + + _bootstrap._verbose_message("zipimport: zlib available") + return decompress + + +# Given a path to a Zip file and a toc_entry, return the (uncompressed) data. +def _get_data(archive, toc_entry): + datapath, compress, data_size, file_size, file_offset, time, date, crc = toc_entry + if data_size < 0: + raise ZipImportError("negative data size") + + with _io.open(archive, "rb") as fp: + # Check to make sure the local file header is correct + try: + fp.seek(file_offset) + except OSError: + raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive) + buffer = fp.read(30) + if len(buffer) != 30: + raise EOFError("EOF read where not expected") + + if buffer[:4] != b"PK\x03\x04": + # Bad: Local File Header + raise ZipImportError(f"bad local file header: {archive!r}", path=archive) + + name_size = _unpack_uint16(buffer[26:28]) + extra_size = _unpack_uint16(buffer[28:30]) + header_size = 30 + name_size + extra_size + file_offset += header_size # Start of file data + try: + fp.seek(file_offset) + except OSError: + raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive) + raw_data = fp.read(data_size) + if len(raw_data) != data_size: + raise OSError("zipimport: can't read data") + + if compress == 0: + # data is not compressed + return raw_data + + # Decompress with zlib + try: + decompress = _get_decompress_func() + except Exception: + raise ZipImportError("can't decompress data; zlib not available") + return decompress(raw_data, -15) + + +# Lenient date/time comparison function. The precision of the mtime +# in the archive is lower than the mtime stored in a .pyc: we +# must allow a difference of at most one second. +def _eq_mtime(t1, t2): + # dostime only stores even seconds, so be lenient + return abs(t1 - t2) <= 1 + + +# Given the contents of a .py[co] file, unmarshal the data +# and return the code object. Return None if it the magic word doesn't +# match (we do this instead of raising an exception as we fall back +# to .py if available and we don't want to mask other errors). +def _unmarshal_code(pathname, data, mtime): + if len(data) < 16: + raise ZipImportError("bad pyc data") + + if data[:4] != _bootstrap_external.MAGIC_NUMBER: + _bootstrap._verbose_message("{!r} has bad magic", pathname) + return None # signal caller to try alternative + + flags = _unpack_uint32(data[4:8]) + if flags != 0: + # Hash-based pyc. We currently refuse to handle checked hash-based + # pycs. We could validate hash-based pycs against the source, but it + # seems likely that most people putting hash-based pycs in a zipfile + # will use unchecked ones. + if _imp.check_hash_based_pycs != "never" and ( + flags != 0x1 or _imp.check_hash_based_pycs == "always" + ): + return None + elif mtime != 0 and not _eq_mtime(_unpack_uint32(data[8:12]), mtime): + _bootstrap._verbose_message("{!r} has bad mtime", pathname) + return None # signal caller to try alternative + + # XXX the pyc's size field is ignored; timestamp collisions are probably + # unimportant with zip files. + code = marshal.loads(data[16:]) + if not isinstance(code, _code_type): + raise TypeError(f"compiled module {pathname!r} is not a code object") + return code + + +_code_type = type(_unmarshal_code.__code__) + + +# Replace any occurrences of '\r\n?' in the input string with '\n'. +# This converts DOS and Mac line endings to Unix line endings. +def _normalize_line_endings(source): + source = source.replace(b"\r\n", b"\n") + source = source.replace(b"\r", b"\n") + return source + + +# Given a string buffer containing Python source code, compile it +# and return a code object. +def _compile_source(pathname, source): + source = _normalize_line_endings(source) + return compile(source, pathname, "exec", dont_inherit=True) + + +# Convert the date/time values found in the Zip archive to a value +# that's compatible with the time stamp stored in .pyc files. +def _parse_dostime(d, t): + return time.mktime( + ( + (d >> 9) + 1980, # bits 9..15: year + (d >> 5) & 0xF, # bits 5..8: month + d & 0x1F, # bits 0..4: day + t >> 11, # bits 11..15: hours + (t >> 5) & 0x3F, # bits 8..10: minutes + (t & 0x1F) * 2, # bits 0..7: seconds / 2 + -1, + -1, + -1, + ) + ) + + +# Given a path to a .pyc file in the archive, return the +# modification time of the matching .py file, or 0 if no source +# is available. +def _get_mtime_of_source(self, path): + try: + # strip 'c' or 'o' from *.py[co] + assert path[-1:] in ("c", "o") + path = path[:-1] + toc_entry = self._files[path] + # fetch the time stamp of the .py file for comparison + # with an embedded pyc time stamp + time = toc_entry[5] + date = toc_entry[6] + return _parse_dostime(date, time) + except (KeyError, IndexError, TypeError): + return 0 + + +# Get the code object associated with the module specified by +# 'fullname'. +def _get_module_code(self, fullname): + path = _get_module_path(self, fullname) + for suffix, isbytecode, ispackage in _zip_searchorder: + fullpath = path + suffix + _bootstrap._verbose_message("trying {}{}{}", self.archive, path_sep, fullpath, verbosity=2) + try: + toc_entry = self._files[fullpath] + except KeyError: + pass + else: + modpath = toc_entry[0] + data = _get_data(self.archive, toc_entry) + if isbytecode: + mtime = _get_mtime_of_source(self, fullpath) + code = _unmarshal_code(modpath, data, mtime) + else: + code = _compile_source(modpath, data) + if code is None: + # bad magic number or non-matching mtime + # in byte code, try next + continue + modpath = toc_entry[0] + return code, ispackage, modpath + else: + raise ZipImportError(f"can't find module {fullname!r}", name=fullname) + + +class _ZipImportResourceReader: + """Private class used to support ZipImport.get_resource_reader(). + + This class is allowed to reference all the innards and private parts of + the zipimporter. + """ + + _registered = False + + def __init__(self, zipimporter, fullname): + self.zipimporter = zipimporter + self.fullname = fullname + + def open_resource(self, resource): + fullname_as_path = self.fullname.replace(".", "/") + path = f"{fullname_as_path}/{resource}" + from io import BytesIO + + try: + return BytesIO(self.zipimporter.get_data(path)) + except OSError: + raise FileNotFoundError(path) + + def resource_path(self, resource): + # All resources are in the zip file, so there is no path to the file. + # Raising FileNotFoundError tells the higher level API to extract the + # binary data and create a temporary file. + raise FileNotFoundError + + def is_resource(self, name): + # Maybe we could do better, but if we can get the data, it's a + # resource. Otherwise it isn't. + fullname_as_path = self.fullname.replace(".", "/") + path = f"{fullname_as_path}/{name}" + try: + self.zipimporter.get_data(path) + except OSError: + return False + return True + + def contents(self): + # This is a bit convoluted, because fullname will be a module path, + # but _files is a list of file names relative to the top of the + # archive's namespace. We want to compare file paths to find all the + # names of things inside the module represented by fullname. So we + # turn the module path of fullname into a file path relative to the + # top of the archive, and then we iterate through _files looking for + # names inside that "directory". + from pathlib import Path + + fullname_path = Path(self.zipimporter.get_filename(self.fullname)) + relative_path = fullname_path.relative_to(self.zipimporter.archive) + # Don't forget that fullname names a package, so its path will include + # __init__.py, which we want to ignore. + assert relative_path.name == "__init__.py" + package_path = relative_path.parent + subdirs_seen = set() + for filename in self.zipimporter._files: + try: + relative = Path(filename).relative_to(package_path) + except ValueError: + continue + # If the path of the file (which is relative to the top of the zip + # namespace), relative to the package given when the resource + # reader was created, has a parent, then it's a name in a + # subdirectory and thus we skip it. + parent_name = relative.parent.name + if len(parent_name) == 0: + yield relative.name + elif parent_name not in subdirs_seen: + subdirs_seen.add(parent_name) + yield parent_name diff --git a/maubot-src/maubot/loader/__init__.py b/maubot-src/maubot/loader/__init__.py new file mode 100644 index 0000000..d61be5c --- /dev/null +++ b/maubot-src/maubot/loader/__init__.py @@ -0,0 +1,3 @@ +from .abc import BasePluginLoader, IDConflictError, PluginClass, PluginLoader +from .meta import DatabaseType, PluginMeta +from .zip import MaubotZipImportError, ZippedPluginLoader diff --git a/maubot-src/maubot/loader/abc.py b/maubot-src/maubot/loader/abc.py new file mode 100644 index 0000000..c2c71b2 --- /dev/null +++ b/maubot-src/maubot/loader/abc.py @@ -0,0 +1,98 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +from typing import TYPE_CHECKING, TypeVar +from abc import ABC, abstractmethod +import asyncio + +from ..plugin_base import Plugin +from .meta import PluginMeta + +if TYPE_CHECKING: + from ..instance import PluginInstance + +PluginClass = TypeVar("PluginClass", bound=Plugin) + + +class IDConflictError(Exception): + pass + + +class BasePluginLoader(ABC): + meta: PluginMeta + + @property + @abstractmethod + def source(self) -> str: + pass + + def sync_read_file(self, path: str) -> bytes: + raise NotImplementedError("This loader doesn't support synchronous operations") + + @abstractmethod + async def read_file(self, path: str) -> bytes: + pass + + def sync_list_files(self, directory: str) -> list[str]: + raise NotImplementedError("This loader doesn't support synchronous operations") + + @abstractmethod + async def list_files(self, directory: str) -> list[str]: + pass + + +class PluginLoader(BasePluginLoader, ABC): + id_cache: dict[str, PluginLoader] = {} + + meta: PluginMeta + references: set[PluginInstance] + + def __init__(self): + self.references = set() + + @classmethod + def find(cls, plugin_id: str) -> PluginLoader: + return cls.id_cache[plugin_id] + + def to_dict(self) -> dict: + return { + "id": self.meta.id, + "version": str(self.meta.version), + "instances": [instance.to_dict() for instance in self.references], + } + + async def stop_instances(self) -> None: + await asyncio.gather( + *[instance.stop() for instance in self.references if instance.started] + ) + + async def start_instances(self) -> None: + await asyncio.gather( + *[instance.start() for instance in self.references if instance.enabled] + ) + + @abstractmethod + async def load(self) -> type[PluginClass]: + pass + + @abstractmethod + async def reload(self) -> type[PluginClass]: + pass + + @abstractmethod + async def delete(self) -> None: + pass diff --git a/maubot-src/maubot/loader/meta.py b/maubot-src/maubot/loader/meta.py new file mode 100644 index 0000000..d368e24 --- /dev/null +++ b/maubot-src/maubot/loader/meta.py @@ -0,0 +1,69 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from typing import List, Optional + +from attr import dataclass +from packaging.version import InvalidVersion, Version + +from mautrix.types import ( + ExtensibleEnum, + SerializableAttrs, + SerializerError, + deserializer, + serializer, +) + +from ..__meta__ import __version__ + + +@serializer(Version) +def serialize_version(version: Version) -> str: + return str(version) + + +@deserializer(Version) +def deserialize_version(version: str) -> Version: + try: + return Version(version) + except InvalidVersion as e: + raise SerializerError("Invalid version") from e + + +class DatabaseType(ExtensibleEnum): + SQLALCHEMY = "sqlalchemy" + ASYNCPG = "asyncpg" + + +@dataclass +class PluginMeta(SerializableAttrs): + id: str + version: Version + modules: List[str] + main_class: str + + maubot: Version = Version(__version__) + database: bool = False + database_type: DatabaseType = DatabaseType.SQLALCHEMY + config: bool = False + webapp: bool = False + license: str = "" + extra_files: List[str] = [] + dependencies: List[str] = [] + soft_dependencies: List[str] = [] + + @property + def database_type_str(self) -> Optional[str]: + return self.database_type.value if self.database else None diff --git a/maubot-src/maubot/loader/zip.py b/maubot-src/maubot/loader/zip.py new file mode 100644 index 0000000..8642183 --- /dev/null +++ b/maubot-src/maubot/loader/zip.py @@ -0,0 +1,310 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +from time import time +from zipfile import BadZipFile, ZipFile +import logging +import os +import sys + +from packaging.version import Version +from ruamel.yaml import YAML, YAMLError + +from mautrix.types import SerializerError + +from ..__meta__ import __version__ +from ..config import Config +from ..lib.zipimport import ZipImportError, zipimporter +from ..plugin_base import Plugin +from .abc import IDConflictError, PluginClass, PluginLoader +from .meta import DatabaseType, PluginMeta + +current_version = Version(__version__) +yaml = YAML() + + +class MaubotZipImportError(Exception): + pass + + +class MaubotZipMetaError(MaubotZipImportError): + pass + + +class MaubotZipPreLoadError(MaubotZipImportError): + pass + + +class MaubotZipLoadError(MaubotZipImportError): + pass + + +class ZippedPluginLoader(PluginLoader): + path_cache: dict[str, ZippedPluginLoader] = {} + log: logging.Logger = logging.getLogger("maubot.loader.zip") + trash_path: str = "delete" + directories: list[str] = [] + + path: str | None + meta: PluginMeta | None + main_class: str | None + main_module: str | None + _loaded: type[PluginClass] | None + _importer: zipimporter | None + _file: ZipFile | None + + def __init__(self, path: str) -> None: + super().__init__() + self.path = path + self.meta = None + self.main_class = None + self.main_module = None + self._loaded = None + self._importer = None + self._file = None + self._load_meta() + self._run_preload_checks(self._get_importer()) + try: + existing = self.id_cache[self.meta.id] + raise IDConflictError( + f"Plugin with id {self.meta.id} already loaded from {existing.source}" + ) + except KeyError: + pass + self.path_cache[self.path] = self + self.id_cache[self.meta.id] = self + self.log.debug(f"Preloaded plugin {self.meta.id} from {self.path}") + + def to_dict(self) -> dict: + return {**super().to_dict(), "path": self.path} + + @classmethod + def get(cls, path: str) -> ZippedPluginLoader: + path = os.path.abspath(path) + try: + return cls.path_cache[path] + except KeyError: + return cls(path) + + @property + def source(self) -> str: + return self.path + + def __repr__(self) -> str: + return ( + "" + ) + + def sync_read_file(self, path: str) -> bytes: + return self._file.read(path) + + async def read_file(self, path: str) -> bytes: + return self.sync_read_file(path) + + def sync_list_files(self, directory: str) -> list[str]: + directory = directory.rstrip("/") + return [ + file.filename + for file in self._file.filelist + if os.path.dirname(file.filename) == directory + ] + + async def list_files(self, directory: str) -> list[str]: + return self.sync_list_files(directory) + + @staticmethod + def _read_meta(source) -> tuple[ZipFile, PluginMeta]: + try: + file = ZipFile(source) + data = file.read("maubot.yaml") + except FileNotFoundError as e: + raise MaubotZipMetaError("Maubot plugin not found") from e + except BadZipFile as e: + raise MaubotZipMetaError("File is not a maubot plugin") from e + except KeyError as e: + raise MaubotZipMetaError("File does not contain a maubot plugin definition") from e + try: + meta_dict = yaml.load(data) + except (YAMLError, KeyError, IndexError, ValueError) as e: + raise MaubotZipMetaError("Maubot plugin definition file is not valid YAML") from e + try: + meta = PluginMeta.deserialize(meta_dict) + except SerializerError as e: + raise MaubotZipMetaError("Maubot plugin definition in file is invalid") from e + if meta.maubot > current_version: + raise MaubotZipMetaError( + f"Plugin requires maubot {meta.maubot}, but this instance is {current_version}" + ) + return file, meta + + @classmethod + def verify_meta(cls, source) -> tuple[str, Version, DatabaseType | None]: + _, meta = cls._read_meta(source) + return meta.id, meta.version, meta.database_type if meta.database else None + + def _load_meta(self) -> None: + file, meta = self._read_meta(self.path) + if self.meta and meta.id != self.meta.id: + raise MaubotZipMetaError("Maubot plugin ID changed during reload") + self.meta = meta + if "/" in meta.main_class: + self.main_module, self.main_class = meta.main_class.split("/")[:2] + else: + self.main_module = meta.modules[-1] + self.main_class = meta.main_class + self._file = file + + def _get_importer(self, reset_cache: bool = False) -> zipimporter: + try: + if not self._importer or self._importer.archive != self.path: + self._importer = zipimporter(self.path) + if reset_cache: + self._importer.reset_cache() + return self._importer + except ZipImportError as e: + raise MaubotZipMetaError("File not found or not a maubot plugin") from e + + def _run_preload_checks(self, importer: zipimporter) -> None: + try: + code = importer.get_code(self.main_module.replace(".", "/")) + if self.main_class not in code.co_names: + raise MaubotZipPreLoadError( + f"Main class {self.main_class} not in {self.main_module}" + ) + except ZipImportError as e: + raise MaubotZipPreLoadError(f"Main module {self.main_module} not found in file") from e + for module in self.meta.modules: + try: + importer.find_module(module) + except ZipImportError as e: + raise MaubotZipPreLoadError(f"Module {module} not found in file") from e + + async def load(self, reset_cache: bool = False) -> type[PluginClass]: + try: + return self._load(reset_cache) + except MaubotZipImportError: + self.log.exception(f"Failed to load {self.meta.id} v{self.meta.version}") + raise + + def _load(self, reset_cache: bool = False) -> type[PluginClass]: + if self._loaded is not None and not reset_cache: + return self._loaded + self._load_meta() + importer = self._get_importer(reset_cache=reset_cache) + self._run_preload_checks(importer) + if reset_cache: + self.log.debug(f"Re-preloaded plugin {self.meta.id} from {self.path}") + for module in self.meta.modules: + try: + importer.load_module(module) + except ZipImportError: + raise MaubotZipLoadError(f"Module {module} not found in file") + except Exception: + raise MaubotZipLoadError(f"Failed to load module {module}") + try: + main_mod = sys.modules[self.main_module] + except KeyError as e: + raise MaubotZipLoadError(f"Main module {self.main_module} of plugin not found") from e + try: + plugin = getattr(main_mod, self.main_class) + except AttributeError as e: + raise MaubotZipLoadError(f"Main class {self.main_class} of plugin not found") from e + if not issubclass(plugin, Plugin): + raise MaubotZipLoadError("Main class of plugin does not extend maubot.Plugin") + self._loaded = plugin + self.log.debug(f"Loaded and imported plugin {self.meta.id} from {self.path}") + return plugin + + async def reload(self, new_path: str | None = None) -> type[PluginClass]: + self._unload() + if new_path is not None and new_path != self.path: + try: + del self.path_cache[self.path] + except KeyError: + pass + self.path = new_path + self.path_cache[self.path] = self + return await self.load(reset_cache=True) + + def _unload(self) -> None: + for name, mod in list(sys.modules.items()): + if (getattr(mod, "__file__", "") or "").startswith(self.path): + del sys.modules[name] + self._loaded = None + self.log.debug(f"Unloaded plugin {self.meta.id} at {self.path}") + + async def delete(self) -> None: + self._unload() + try: + del self.path_cache[self.path] + except KeyError: + pass + try: + del self.id_cache[self.meta.id] + except KeyError: + pass + if self._importer: + self._importer.remove_cache() + self._importer = None + self._loaded = None + self.trash(self.path, reason="delete") + self.meta = None + self.path = None + + @classmethod + def trash(cls, file_path: str, new_name: str | None = None, reason: str = "error") -> None: + if cls.trash_path == "delete": + try: + os.remove(file_path) + except FileNotFoundError: + pass + else: + new_name = new_name or f"{int(time())}-{reason}-{os.path.basename(file_path)}" + try: + os.rename(file_path, os.path.abspath(os.path.join(cls.trash_path, new_name))) + except OSError as e: + cls.log.warning(f"Failed to rename {file_path}: {e} - trying to delete") + try: + os.remove(file_path) + except FileNotFoundError: + pass + + @classmethod + def load_all(cls): + cls.log.debug("Preloading plugins...") + for directory in cls.directories: + for file in os.listdir(directory): + if not file.endswith(".mbp"): + continue + path = os.path.abspath(os.path.join(directory, file)) + try: + cls.get(path) + except MaubotZipImportError: + cls.log.exception(f"Failed to load plugin at {path}, trashing...") + cls.trash(path) + except IDConflictError: + cls.log.error(f"Duplicate plugin ID at {path}, trashing...") + cls.trash(path) + + +def init(config: Config) -> None: + ZippedPluginLoader.trash_path = config["plugin_directories.trash"] + ZippedPluginLoader.directories = config["plugin_directories.load"] + ZippedPluginLoader.load_all() diff --git a/maubot-src/maubot/management/__init__.py b/maubot-src/maubot/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/maubot-src/maubot/management/api/__init__.py b/maubot-src/maubot/management/api/__init__.py new file mode 100644 index 0000000..c2e5f24 --- /dev/null +++ b/maubot-src/maubot/management/api/__init__.py @@ -0,0 +1,48 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from asyncio import AbstractEventLoop +import importlib + +from aiohttp import web + +from ...config import Config +from .auth import check_token +from .base import get_config, routes, set_config +from .middleware import auth, error + + +@routes.get("/features") +def features(request: web.Request) -> web.Response: + data = get_config()["api_features"] + err = check_token(request) + if err is None: + return web.json_response(data) + else: + return web.json_response( + { + "login": data["login"], + } + ) + + +def init(cfg: Config, loop: AbstractEventLoop) -> web.Application: + set_config(cfg) + for pkg, enabled in cfg["api_features"].items(): + if enabled: + importlib.import_module(f"maubot.management.api.{pkg}") + app = web.Application(loop=loop, middlewares=[auth, error], client_max_size=100 * 1024 * 1024) + app.add_routes(routes) + return app diff --git a/maubot-src/maubot/management/api/auth.py b/maubot-src/maubot/management/api/auth.py new file mode 100644 index 0000000..0abc3ad --- /dev/null +++ b/maubot-src/maubot/management/api/auth.py @@ -0,0 +1,76 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +from time import time + +from aiohttp import web + +from mautrix.types import UserID +from mautrix.util.signed_token import sign_token, verify_token + +from .base import get_config, routes +from .responses import resp + + +def is_valid_token(token: str) -> bool: + data = verify_token(get_config()["server.unshared_secret"], token) + if not data: + return False + return get_config().is_admin(data.get("user_id", None)) + + +def create_token(user: UserID) -> str: + return sign_token( + get_config()["server.unshared_secret"], + { + "user_id": user, + "created_at": int(time()), + }, + ) + + +def get_token(request: web.Request) -> str: + token = request.headers.get("Authorization", "") + if not token or not token.startswith("Bearer "): + token = request.query.get("access_token", "") + else: + token = token[len("Bearer ") :] + return token + + +def check_token(request: web.Request) -> web.Response | None: + token = get_token(request) + if not token: + return resp.no_token + elif not is_valid_token(token): + return resp.invalid_token + return None + + +@routes.post("/auth/ping") +async def ping(request: web.Request) -> web.Response: + token = get_token(request) + if not token: + return resp.no_token + + data = verify_token(get_config()["server.unshared_secret"], token) + if not data: + return resp.invalid_token + user = data.get("user_id", None) + if not get_config().is_admin(user): + return resp.invalid_token + return resp.pong(user, get_config()["api_features"]) diff --git a/maubot-src/maubot/management/api/base.py b/maubot-src/maubot/management/api/base.py new file mode 100644 index 0000000..3d7693a --- /dev/null +++ b/maubot-src/maubot/management/api/base.py @@ -0,0 +1,40 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +import asyncio + +from aiohttp import web + +from ...__meta__ import __version__ +from ...config import Config + +routes: web.RouteTableDef = web.RouteTableDef() +_config: Config | None = None + + +def set_config(config: Config) -> None: + global _config + _config = config + + +def get_config() -> Config: + return _config + + +@routes.get("/version") +async def version(_: web.Request) -> web.Response: + return web.json_response({"version": __version__}) diff --git a/maubot-src/maubot/management/api/client.py b/maubot-src/maubot/management/api/client.py new file mode 100644 index 0000000..0f80584 --- /dev/null +++ b/maubot-src/maubot/management/api/client.py @@ -0,0 +1,217 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +from json import JSONDecodeError +import logging + +from aiohttp import web + +from mautrix.client import Client as MatrixClient +from mautrix.errors import MatrixConnectionError, MatrixInvalidToken, MatrixRequestError +from mautrix.types import FilterID, SyncToken, UserID + +from ...client import Client +from .base import routes +from .responses import resp + +log = logging.getLogger("maubot.server.client") + + +@routes.get("/clients") +async def get_clients(_: web.Request) -> web.Response: + return resp.found([client.to_dict() for client in Client.cache.values()]) + + +@routes.get("/client/{id}") +async def get_client(request: web.Request) -> web.Response: + user_id = request.match_info.get("id", None) + client = await Client.get(user_id) + if not client: + return resp.client_not_found + return resp.found(client.to_dict()) + + +async def _create_client(user_id: UserID | None, data: dict) -> web.Response: + homeserver = data.get("homeserver", None) + access_token = data.get("access_token", None) + device_id = data.get("device_id", None) + new_client = MatrixClient( + mxid="@not:a.mxid", + base_url=homeserver, + token=access_token, + client_session=Client.http_client, + ) + try: + whoami = await new_client.whoami() + except MatrixInvalidToken as e: + return resp.bad_client_access_token + except MatrixRequestError: + log.warning(f"Failed to get whoami from {homeserver} for new client", exc_info=True) + return resp.bad_client_access_details + except MatrixConnectionError: + log.warning(f"Failed to connect to {homeserver} for new client", exc_info=True) + return resp.bad_client_connection_details + if user_id is None: + existing_client = await Client.get(whoami.user_id) + if existing_client is not None: + return resp.user_exists + elif whoami.user_id != user_id: + return resp.mxid_mismatch(whoami.user_id) + elif whoami.device_id and device_id and whoami.device_id != device_id: + return resp.device_id_mismatch(whoami.device_id) + client = await Client.get( + whoami.user_id, homeserver=homeserver, access_token=access_token, device_id=device_id + ) + client.enabled = data.get("enabled", True) + client.sync = data.get("sync", True) + await client.update_autojoin(data.get("autojoin", True), save=False) + await client.update_online(data.get("online", True), save=False) + client.displayname = data.get("displayname", "disable") + client.avatar_url = data.get("avatar_url", "disable") + await client.update() + await client.start() + return resp.created(client.to_dict()) + + +async def _update_client(client: Client, data: dict, is_login: bool = False) -> web.Response: + try: + await client.update_access_details( + data.get("access_token"), data.get("homeserver"), data.get("device_id") + ) + except MatrixInvalidToken: + return resp.bad_client_access_token + except MatrixRequestError: + log.warning( + f"Failed to get whoami from homeserver to update client details", exc_info=True + ) + return resp.bad_client_access_details + except MatrixConnectionError: + log.warning(f"Failed to connect to homeserver to update client details", exc_info=True) + return resp.bad_client_connection_details + except ValueError as e: + str_err = str(e) + if str_err.startswith("MXID mismatch"): + return resp.mxid_mismatch(str(e)[len("MXID mismatch: ") :]) + elif str_err.startswith("Device ID mismatch"): + return resp.device_id_mismatch(str(e)[len("Device ID mismatch: ") :]) + await client.update_avatar_url(data.get("avatar_url"), save=False) + await client.update_displayname(data.get("displayname"), save=False) + await client.update_started(data.get("started")) + await client.update_enabled(data.get("enabled"), save=False) + await client.update_autojoin(data.get("autojoin"), save=False) + await client.update_online(data.get("online"), save=False) + await client.update_sync(data.get("sync"), save=False) + await client.update() + return resp.updated(client.to_dict(), is_login=is_login) + + +async def _create_or_update_client( + user_id: UserID, data: dict, is_login: bool = False +) -> web.Response: + client = await Client.get(user_id) + if not client: + return await _create_client(user_id, data) + else: + return await _update_client(client, data, is_login=is_login) + + +@routes.post("/client/new") +async def create_client(request: web.Request) -> web.Response: + try: + data = await request.json() + except JSONDecodeError: + return resp.body_not_json + return await _create_client(None, data) + + +@routes.put("/client/{id}") +async def update_client(request: web.Request) -> web.Response: + user_id = request.match_info["id"] + try: + data = await request.json() + except JSONDecodeError: + return resp.body_not_json + return await _create_or_update_client(user_id, data) + + +@routes.delete("/client/{id}") +async def delete_client(request: web.Request) -> web.Response: + user_id = request.match_info["id"] + client = await Client.get(user_id) + if not client: + return resp.client_not_found + if len(client.references) > 0: + return resp.client_in_use + if client.started: + await client.stop() + await client.delete() + return resp.deleted + + +@routes.post("/client/{id}/clearcache") +async def clear_client_cache(request: web.Request) -> web.Response: + user_id = request.match_info["id"] + client = await Client.get(user_id) + if not client: + return resp.client_not_found + await client.clear_cache() + return resp.ok + + +@routes.post("/client/{id}/verify") +async def verify_client(request: web.Request) -> web.Response: + user_id = request.match_info["id"] + client = await Client.get(user_id) + if not client: + return resp.client_not_found + try: + req = await request.json() + except Exception: + return resp.body_not_json + try: + await client.crypto.verify_with_recovery_key(req["recovery_key"]) + client.trust_state = await client.crypto.resolve_trust( + client.crypto.own_identity, + allow_fetch=False, + ) + log.debug(f"Trust state after verifying {client.id}: {client.trust_state}") + except Exception as e: + log.exception("Failed to verify client with recovery key") + return resp.internal_crypto_error(str(e)) + return resp.ok + + +@routes.post("/client/{id}/generate_recovery_key") +async def generate_recovery_key(request: web.Request) -> web.Response: + user_id = request.match_info["id"] + client = await Client.get(user_id) + if not client: + return resp.client_not_found + try: + keys = await client.crypto.get_own_cross_signing_public_keys() + if keys: + return resp.client_has_keys + key = await client.crypto.generate_recovery_key() + client.trust_state = await client.crypto.resolve_trust( + client.crypto.own_identity, + allow_fetch=False, + ) + log.debug(f"Trust state generating recovery key for {client.id}: {client.trust_state}") + except Exception as e: + log.exception("Failed to generate recovery key for client") + return resp.internal_crypto_error(str(e)) + return resp.created({"success": True, "recovery_key": key}) diff --git a/maubot-src/maubot/management/api/client_auth.py b/maubot-src/maubot/management/api/client_auth.py new file mode 100644 index 0000000..4e5e201 --- /dev/null +++ b/maubot-src/maubot/management/api/client_auth.py @@ -0,0 +1,273 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +from typing import NamedTuple +from http import HTTPStatus +from json import JSONDecodeError +import asyncio +import hashlib +import hmac +import random +import string + +from aiohttp import web +from yarl import URL + +from mautrix.api import Method, Path, SynapseAdminPath +from mautrix.client import ClientAPI +from mautrix.errors import MatrixRequestError +from mautrix.types import LoginResponse, LoginType + +from .base import get_config, routes +from .client import _create_client, _create_or_update_client +from .responses import resp + + +def known_homeservers() -> dict[str, dict[str, str]]: + return get_config()["homeservers"] + + +@routes.get("/client/auth/servers") +async def get_known_servers(_: web.Request) -> web.Response: + return web.json_response({key: value["url"] for key, value in known_homeservers().items()}) + + +class AuthRequestInfo(NamedTuple): + server_name: str + client: ClientAPI + secret: str + username: str + password: str + user_type: str + device_name: str + update_client: bool + sso: bool + + +truthy_strings = ("1", "true", "yes") + + +async def read_client_auth_request( + request: web.Request, +) -> tuple[AuthRequestInfo | None, web.Response | None]: + server_name = request.match_info.get("server", None) + server = known_homeservers().get(server_name, None) + if not server: + return None, resp.server_not_found + try: + body = await request.json() + except JSONDecodeError: + return None, resp.body_not_json + sso = request.query.get("sso", "").lower() in truthy_strings + try: + username = body["username"] + password = body["password"] + except KeyError: + if not sso: + return None, resp.username_or_password_missing + username = password = None + try: + base_url = server["url"] + except KeyError: + return None, resp.invalid_server + return ( + AuthRequestInfo( + server_name=server_name, + client=ClientAPI(base_url=base_url), + secret=server.get("secret"), + username=username, + password=password, + user_type=body.get("user_type", "bot"), + device_name=body.get("device_name", "Maubot"), + update_client=request.query.get("update_client", "").lower() in truthy_strings, + sso=sso, + ), + None, + ) + + +def generate_mac( + secret: str, + nonce: str, + username: str, + password: str, + admin: bool = False, + user_type: str = None, +) -> str: + mac = hmac.new(key=secret.encode("utf-8"), digestmod=hashlib.sha1) + mac.update(nonce.encode("utf-8")) + mac.update(b"\x00") + mac.update(username.encode("utf-8")) + mac.update(b"\x00") + mac.update(password.encode("utf-8")) + mac.update(b"\x00") + mac.update(b"admin" if admin else b"notadmin") + if user_type is not None: + mac.update(b"\x00") + mac.update(user_type.encode("utf8")) + return mac.hexdigest() + + +@routes.post("/client/auth/{server}/register") +async def register(request: web.Request) -> web.Response: + req, err = await read_client_auth_request(request) + if err is not None: + return err + if req.sso: + return resp.registration_no_sso + elif not req.secret: + return resp.registration_secret_not_found + path = SynapseAdminPath.v1.register + res = await req.client.api.request(Method.GET, path) + content = { + "nonce": res["nonce"], + "username": req.username, + "password": req.password, + "admin": False, + "user_type": req.user_type, + } + content["mac"] = generate_mac(**content, secret=req.secret) + try: + raw_res = await req.client.api.request(Method.POST, path, content=content) + except MatrixRequestError as e: + return web.json_response( + { + "errcode": e.errcode, + "error": e.message, + "http_status": e.http_status, + }, + status=HTTPStatus.INTERNAL_SERVER_ERROR, + ) + login_res = LoginResponse.deserialize(raw_res) + if req.update_client: + return await _create_client( + login_res.user_id, + { + "homeserver": str(req.client.api.base_url), + "access_token": login_res.access_token, + "device_id": login_res.device_id, + }, + ) + return web.json_response(login_res.serialize()) + + +@routes.post("/client/auth/{server}/login") +async def login(request: web.Request) -> web.Response: + req, err = await read_client_auth_request(request) + if err is not None: + return err + if req.sso: + return await _do_sso(req) + else: + return await _do_login(req) + + +async def _do_sso(req: AuthRequestInfo) -> web.Response: + flows = await req.client.get_login_flows() + if not flows.supports_type(LoginType.SSO): + return resp.sso_not_supported + waiter_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=16)) + cfg = get_config() + public_url = ( + URL(cfg["server.public_url"]) + / "_matrix/maubot/v1/client/auth_external_sso/complete" + / waiter_id + ) + sso_url = req.client.api.base_url.with_path(str(Path.v3.login.sso.redirect)).with_query( + {"redirectUrl": str(public_url)} + ) + sso_waiters[waiter_id] = req, asyncio.get_running_loop().create_future() + return web.json_response({"sso_url": str(sso_url), "id": waiter_id}) + + +async def _do_login(req: AuthRequestInfo, login_token: str | None = None) -> web.Response: + device_id = "".join(random.choices(string.ascii_uppercase + string.digits, k=8)) + device_id = f"maubot_{device_id}" + try: + if req.sso: + res = await req.client.login( + token=login_token, + login_type=LoginType.TOKEN, + device_id=device_id, + store_access_token=False, + initial_device_display_name=req.device_name, + ) + else: + res = await req.client.login( + identifier=req.username, + login_type=LoginType.PASSWORD, + password=req.password, + device_id=device_id, + initial_device_display_name=req.device_name, + store_access_token=False, + ) + except MatrixRequestError as e: + return web.json_response( + { + "errcode": e.errcode, + "error": e.message, + }, + status=e.http_status, + ) + if req.update_client: + return await _create_or_update_client( + res.user_id, + { + "homeserver": str(req.client.api.base_url), + "access_token": res.access_token, + "device_id": res.device_id, + }, + is_login=True, + ) + return web.json_response(res.serialize()) + + +sso_waiters: dict[str, tuple[AuthRequestInfo, asyncio.Future]] = {} + + +@routes.post("/client/auth/{server}/sso/{id}/wait") +async def wait_sso(request: web.Request) -> web.Response: + waiter_id = request.match_info["id"] + req, fut = sso_waiters[waiter_id] + try: + login_token = await fut + finally: + sso_waiters.pop(waiter_id, None) + return await _do_login(req, login_token) + + +@routes.get("/client/auth_external_sso/complete/{id}") +async def complete_sso(request: web.Request) -> web.Response: + try: + _, fut = sso_waiters[request.match_info["id"]] + except KeyError: + return web.Response(status=404, text="Invalid session ID\n") + if fut.cancelled(): + return web.Response(status=200, text="The login was cancelled from the Maubot client\n") + elif fut.done(): + return web.Response(status=200, text="The login token was already received\n") + try: + fut.set_result(request.query["loginToken"]) + except KeyError: + return web.Response(status=400, text="Missing loginToken query parameter\n") + except asyncio.InvalidStateError: + return web.Response(status=500, text="Invalid state\n") + return web.Response( + status=200, + text="Login token received, please return to your Maubot client. " + "This tab can be closed.\n", + ) diff --git a/maubot-src/maubot/management/api/client_proxy.py b/maubot-src/maubot/management/api/client_proxy.py new file mode 100644 index 0000000..3fa682b --- /dev/null +++ b/maubot-src/maubot/management/api/client_proxy.py @@ -0,0 +1,56 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from aiohttp import client as http, web + +from ...client import Client +from .base import routes +from .responses import resp + +PROXY_CHUNK_SIZE = 32 * 1024 + + +@routes.view("/proxy/{id}/{path:_matrix/.+}") +async def proxy(request: web.Request) -> web.StreamResponse: + user_id = request.match_info.get("id", None) + client = await Client.get(user_id) + if not client: + return resp.client_not_found + + path = request.match_info.get("path", None) + query = request.query.copy() + try: + del query["access_token"] + except KeyError: + pass + headers = request.headers.copy() + del headers["Host"] + headers["Authorization"] = f"Bearer {client.access_token}" + if "X-Forwarded-For" not in headers: + peer = request.transport.get_extra_info("peername") + if peer is not None: + host, port = peer + headers["X-Forwarded-For"] = f"{host}:{port}" + + data = await request.read() + async with http.request( + request.method, f"{client.homeserver}/{path}", headers=headers, params=query, data=data + ) as proxy_resp: + response = web.StreamResponse(status=proxy_resp.status, headers=proxy_resp.headers) + await response.prepare(request) + async for chunk in proxy_resp.content.iter_chunked(PROXY_CHUNK_SIZE): + await response.write(chunk) + await response.write_eof() + return response diff --git a/maubot-src/maubot/management/api/dev_open.py b/maubot-src/maubot/management/api/dev_open.py new file mode 100644 index 0000000..2881d46 --- /dev/null +++ b/maubot-src/maubot/management/api/dev_open.py @@ -0,0 +1,57 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from string import Template +import asyncio +import re + +from aiohttp import web +from ruamel.yaml import YAML + +from .base import routes + +enabled = False + + +@routes.get("/debug/open") +async def check_enabled(_: web.Request) -> web.Response: + return web.json_response({"enabled": enabled}) + + +try: + yaml = YAML() + + with open(".dev-open-cfg.yaml", "r") as file: + cfg = yaml.load(file) + editor_command = Template(cfg["editor"]) + pathmap = [(re.compile(item["find"]), item["replace"]) for item in cfg["pathmap"]] + + @routes.post("/debug/open") + async def open_file(request: web.Request) -> web.Response: + data = await request.json() + try: + path = data["path"] + for find, replace in pathmap: + path = find.sub(replace, path) + cmd = editor_command.substitute(path=path, line=data["line"]) + except (KeyError, ValueError): + return web.Response(status=400) + res = await asyncio.create_subprocess_shell(cmd) + stdout, stderr = await res.communicate() + return web.json_response({"return": res.returncode, "stdout": stdout, "stderr": stderr}) + + enabled = True +except Exception: + pass diff --git a/maubot-src/maubot/management/api/instance.py b/maubot-src/maubot/management/api/instance.py new file mode 100644 index 0000000..4043221 --- /dev/null +++ b/maubot-src/maubot/management/api/instance.py @@ -0,0 +1,97 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from json import JSONDecodeError + +from aiohttp import web + +from ...client import Client +from ...instance import PluginInstance +from ...loader import PluginLoader +from .base import routes +from .responses import resp + + +@routes.get("/instances") +async def get_instances(_: web.Request) -> web.Response: + return resp.found([instance.to_dict() for instance in PluginInstance.cache.values()]) + + +@routes.get("/instance/{id}") +async def get_instance(request: web.Request) -> web.Response: + instance_id = request.match_info["id"].lower() + instance = await PluginInstance.get(instance_id) + if not instance: + return resp.instance_not_found + return resp.found(instance.to_dict()) + + +async def _create_instance(instance_id: str, data: dict) -> web.Response: + plugin_type = data.get("type") + primary_user = data.get("primary_user") + if not plugin_type: + return resp.plugin_type_required + elif not primary_user: + return resp.primary_user_required + elif not await Client.get(primary_user): + return resp.primary_user_not_found + try: + PluginLoader.find(plugin_type) + except KeyError: + return resp.plugin_type_not_found + instance = await PluginInstance.get(instance_id, type=plugin_type, primary_user=primary_user) + instance.enabled = data.get("enabled", True) + instance.config_str = data.get("config") or "" + await instance.update() + await instance.load() + await instance.start() + return resp.created(instance.to_dict()) + + +async def _update_instance(instance: PluginInstance, data: dict) -> web.Response: + if not await instance.update_primary_user(data.get("primary_user")): + return resp.primary_user_not_found + await instance.update_id(data.get("id")) + await instance.update_enabled(data.get("enabled")) + await instance.update_config(data.get("config")) + await instance.update_started(data.get("started")) + await instance.update_type(data.get("type")) + return resp.updated(instance.to_dict()) + + +@routes.put("/instance/{id}") +async def update_instance(request: web.Request) -> web.Response: + instance_id = request.match_info["id"].lower() + instance = await PluginInstance.get(instance_id) + try: + data = await request.json() + except JSONDecodeError: + return resp.body_not_json + if not instance: + return await _create_instance(instance_id, data) + else: + return await _update_instance(instance, data) + + +@routes.delete("/instance/{id}") +async def delete_instance(request: web.Request) -> web.Response: + instance_id = request.match_info["id"].lower() + instance = await PluginInstance.get(instance_id) + if not instance: + return resp.instance_not_found + if instance.started: + await instance.stop() + await instance.delete() + return resp.deleted diff --git a/maubot-src/maubot/management/api/instance_database.py b/maubot-src/maubot/management/api/instance_database.py new file mode 100644 index 0000000..2f8c37a --- /dev/null +++ b/maubot-src/maubot/management/api/instance_database.py @@ -0,0 +1,161 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +from datetime import datetime + +from aiohttp import web +from asyncpg import PostgresError +import aiosqlite + +from mautrix.util.async_db import Database + +from ...instance import PluginInstance +from ...lib.optionalalchemy import Engine, IntegrityError, OperationalError, asc, desc +from .base import routes +from .responses import resp + + +@routes.get("/instance/{id}/database") +async def get_database(request: web.Request) -> web.Response: + instance_id = request.match_info["id"].lower() + instance = await PluginInstance.get(instance_id) + if not instance: + return resp.instance_not_found + elif not instance.inst_db: + return resp.plugin_has_no_database + return web.json_response(await instance.get_db_tables()) + + +@routes.get("/instance/{id}/database/{table}") +async def get_table(request: web.Request) -> web.Response: + instance_id = request.match_info["id"].lower() + instance = await PluginInstance.get(instance_id) + if not instance: + return resp.instance_not_found + elif not instance.inst_db: + return resp.plugin_has_no_database + tables = await instance.get_db_tables() + try: + table = tables[request.match_info.get("table", "")] + except KeyError: + return resp.table_not_found + try: + order = [tuple(order.split(":")) for order in request.query.getall("order")] + order = [ + ( + (asc if sort.lower() == "asc" else desc)(table.columns[column]) + if sort + else table.columns[column] + ) + for column, sort in order + ] + except KeyError: + order = [] + limit = int(request.query.get("limit", "100")) + if isinstance(instance.inst_db, Engine): + return _execute_query_sqlalchemy(instance, table.select().order_by(*order).limit(limit)) + + +@routes.post("/instance/{id}/database/query") +async def query(request: web.Request) -> web.Response: + instance_id = request.match_info["id"].lower() + instance = await PluginInstance.get(instance_id) + if not instance: + return resp.instance_not_found + elif not instance.inst_db: + return resp.plugin_has_no_database + data = await request.json() + try: + sql_query = data["query"] + except KeyError: + return resp.query_missing + rows_as_dict = data.get("rows_as_dict", False) + if isinstance(instance.inst_db, Engine): + return _execute_query_sqlalchemy(instance, sql_query, rows_as_dict) + elif isinstance(instance.inst_db, Database): + try: + return await _execute_query_asyncpg(instance, sql_query, rows_as_dict) + except (PostgresError, aiosqlite.Error) as e: + return resp.sql_error(e, sql_query) + else: + return resp.unsupported_plugin_database + + +def check_type(val): + if isinstance(val, datetime): + return val.isoformat() + return val + + +async def _execute_query_asyncpg( + instance: PluginInstance, sql_query: str, rows_as_dict: bool = False +) -> web.Response: + data = {"ok": True, "query": sql_query} + if sql_query.upper().startswith("SELECT"): + res = await instance.inst_db.fetch(sql_query) + data["rows"] = [ + ( + {key: check_type(value) for key, value in row.items()} + if rows_as_dict + else [check_type(value) for value in row] + ) + for row in res + ] + if len(res) > 0: + # TODO can we find column names when there are no rows? + data["columns"] = list(res[0].keys()) + else: + res = await instance.inst_db.execute(sql_query) + if isinstance(res, str): + data["status_msg"] = res + elif isinstance(res, aiosqlite.Cursor): + data["rowcount"] = res.rowcount + # data["inserted_primary_key"] = res.lastrowid + else: + data["status_msg"] = "unknown status" + return web.json_response(data) + + +def _execute_query_sqlalchemy( + instance: PluginInstance, sql_query: str, rows_as_dict: bool = False +) -> web.Response: + assert isinstance(instance.inst_db, Engine) + try: + res = instance.inst_db.execute(sql_query) + except IntegrityError as e: + return resp.sql_integrity_error(e, sql_query) + except OperationalError as e: + return resp.sql_operational_error(e, sql_query) + data = { + "ok": True, + "query": str(sql_query), + } + if res.returns_rows: + data["rows"] = [ + ( + {key: check_type(value) for key, value in row.items()} + if rows_as_dict + else [check_type(value) for value in row] + ) + for row in res + ] + data["columns"] = res.keys() + else: + data["rowcount"] = res.rowcount + if res.is_insert: + data["inserted_primary_key"] = res.inserted_primary_key + return web.json_response(data) diff --git a/maubot-src/maubot/management/api/log.py b/maubot-src/maubot/management/api/log.py new file mode 100644 index 0000000..14c80cd --- /dev/null +++ b/maubot-src/maubot/management/api/log.py @@ -0,0 +1,172 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +from collections import deque +from datetime import datetime +import asyncio +import logging + +from aiohttp import web, web_ws + +from mautrix.util import background_task + +from .auth import is_valid_token +from .base import routes + +BUILTIN_ATTRS = { + "args", + "asctime", + "created", + "exc_info", + "exc_text", + "filename", + "funcName", + "levelname", + "levelno", + "lineno", + "module", + "msecs", + "message", + "msg", + "name", + "pathname", + "process", + "processName", + "relativeCreated", + "stack_info", + "thread", + "threadName", +} +INCLUDE_ATTRS = { + "filename", + "funcName", + "levelname", + "levelno", + "lineno", + "module", + "name", + "pathname", +} +EXCLUDE_ATTRS = BUILTIN_ATTRS - INCLUDE_ATTRS +MAX_LINES = 2048 + + +class LogCollector(logging.Handler): + lines: deque[dict] + formatter: logging.Formatter + listeners: list[web.WebSocketResponse] + loop: asyncio.AbstractEventLoop + + def __init__(self, level=logging.NOTSET) -> None: + super().__init__(level) + self.lines = deque(maxlen=MAX_LINES) + self.formatter = logging.Formatter() + self.listeners = [] + + def emit(self, record: logging.LogRecord) -> None: + try: + self._emit(record) + except Exception as e: + print("Logging error:", e) + + def _emit(self, record: logging.LogRecord) -> None: + # JSON conversion based on Marsel Mavletkulov's json-log-formatter (MIT license) + # https://github.com/marselester/json-log-formatter + content = { + name: value for name, value in record.__dict__.items() if name not in EXCLUDE_ATTRS + } + content["id"] = str(record.relativeCreated) + content["msg"] = record.getMessage() + content["time"] = datetime.fromtimestamp(record.created) + + if record.exc_info: + content["exc_info"] = self.formatter.formatException(record.exc_info) + + for name, value in content.items(): + if isinstance(value, datetime): + content[name] = value.astimezone().isoformat() + asyncio.run_coroutine_threadsafe(self.send(content), loop=self.loop) + self.lines.append(content) + + async def send(self, record: dict) -> None: + for ws in self.listeners: + try: + await ws.send_json(record) + except Exception as e: + print("Log sending error:", e) + + +handler = LogCollector() +log = logging.getLogger("maubot.server.websocket") +sockets = [] + + +def init(loop: asyncio.AbstractEventLoop) -> None: + logging.root.addHandler(handler) + handler.loop = loop + + +async def stop_all() -> None: + log.debug("Closing log listener websockets") + logging.root.removeHandler(handler) + for socket in sockets: + try: + await socket.close(code=1012) + except Exception: + pass + + +@routes.get("/logs") +async def log_websocket(request: web.Request) -> web.WebSocketResponse: + ws = web.WebSocketResponse() + await ws.prepare(request) + sockets.append(ws) + log.debug(f"Connection from {request.remote} opened") + authenticated = False + + async def close_if_not_authenticated(): + await asyncio.sleep(5) + if not authenticated: + await ws.close(code=4000) + log.debug(f"Connection from {request.remote} terminated due to no authentication") + + background_task.create(close_if_not_authenticated()) + + try: + msg: web_ws.WSMessage + async for msg in ws: + if msg.type != web.WSMsgType.TEXT: + continue + if is_valid_token(msg.data): + await ws.send_json({"auth_success": True}) + await ws.send_json({"history": list(handler.lines)}) + if not authenticated: + log.debug(f"Connection from {request.remote} authenticated") + handler.listeners.append(ws) + authenticated = True + elif not authenticated: + await ws.send_json({"auth_success": False}) + except Exception: + try: + await ws.close() + except Exception: + pass + if authenticated: + handler.listeners.remove(ws) + log.debug(f"Connection from {request.remote} closed") + sockets.remove(ws) + return ws diff --git a/maubot-src/maubot/management/api/login.py b/maubot-src/maubot/management/api/login.py new file mode 100644 index 0000000..bfb2f6a --- /dev/null +++ b/maubot-src/maubot/management/api/login.py @@ -0,0 +1,41 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +import json + +from aiohttp import web + +from .auth import create_token +from .base import get_config, routes +from .responses import resp + + +@routes.post("/auth/login") +async def login(request: web.Request) -> web.Response: + try: + data = await request.json() + except json.JSONDecodeError: + return resp.body_not_json + secret = data.get("secret") + if secret and get_config()["server.unshared_secret"] == secret: + user = data.get("user") or "root" + return resp.logged_in(create_token(user)) + + username = data.get("username") + password = data.get("password") + if get_config().check_password(username, password): + return resp.logged_in(create_token(username)) + + return resp.bad_auth diff --git a/maubot-src/maubot/management/api/middleware.py b/maubot-src/maubot/management/api/middleware.py new file mode 100644 index 0000000..17141fa --- /dev/null +++ b/maubot-src/maubot/management/api/middleware.py @@ -0,0 +1,78 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from typing import Awaitable, Callable +import base64 +import logging + +from aiohttp import web + +from .auth import check_token +from .base import get_config +from .responses import resp + +Handler = Callable[[web.Request], Awaitable[web.Response]] +log = logging.getLogger("maubot.server") + + +@web.middleware +async def auth(request: web.Request, handler: Handler) -> web.Response: + subpath = request.path[len("/_matrix/maubot/v1") :] + if ( + subpath.startswith("/auth/") + or subpath.startswith("/client/auth_external_sso/complete/") + or subpath == "/features" + or subpath == "/logs" + ): + return await handler(request) + err = check_token(request) + if err is not None: + return err + return await handler(request) + + +@web.middleware +async def error(request: web.Request, handler: Handler) -> web.Response: + try: + return await handler(request) + except web.HTTPException as ex: + if ex.status_code == 404: + return resp.path_not_found + elif ex.status_code == 405: + return resp.method_not_allowed + return web.json_response( + { + "httpexception": { + "headers": {key: value for key, value in ex.headers.items()}, + "class": type(ex).__name__, + "body": ex.text or base64.b64encode(ex.body), + }, + "error": f"Unhandled HTTP {ex.status}: {ex.text[:128] or 'non-text response'}", + "errcode": f"unhandled_http_{ex.status}", + }, + status=ex.status, + ) + except Exception: + log.exception("Error in handler") + return resp.internal_server_error + + +req_no = 0 + + +def get_req_no(): + global req_no + req_no += 1 + return req_no diff --git a/maubot-src/maubot/management/api/plugin.py b/maubot-src/maubot/management/api/plugin.py new file mode 100644 index 0000000..94d8d9d --- /dev/null +++ b/maubot-src/maubot/management/api/plugin.py @@ -0,0 +1,64 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +import traceback + +from aiohttp import web + +from ...loader import MaubotZipImportError, PluginLoader +from .base import routes +from .responses import resp + + +@routes.get("/plugins") +async def get_plugins(_) -> web.Response: + return resp.found([plugin.to_dict() for plugin in PluginLoader.id_cache.values()]) + + +@routes.get("/plugin/{id}") +async def get_plugin(request: web.Request) -> web.Response: + plugin_id = request.match_info["id"] + plugin = PluginLoader.id_cache.get(plugin_id) + if not plugin: + return resp.plugin_not_found + return resp.found(plugin.to_dict()) + + +@routes.delete("/plugin/{id}") +async def delete_plugin(request: web.Request) -> web.Response: + plugin_id = request.match_info["id"] + plugin = PluginLoader.id_cache.get(plugin_id) + if not plugin: + return resp.plugin_not_found + elif len(plugin.references) > 0: + return resp.plugin_in_use + await plugin.delete() + return resp.deleted + + +@routes.post("/plugin/{id}/reload") +async def reload_plugin(request: web.Request) -> web.Response: + plugin_id = request.match_info["id"] + plugin = PluginLoader.id_cache.get(plugin_id) + if not plugin: + return resp.plugin_not_found + + await plugin.stop_instances() + try: + await plugin.reload() + except MaubotZipImportError as e: + return resp.plugin_reload_error(str(e), traceback.format_exc()) + await plugin.start_instances() + return resp.ok diff --git a/maubot-src/maubot/management/api/plugin_upload.py b/maubot-src/maubot/management/api/plugin_upload.py new file mode 100644 index 0000000..4cd2c47 --- /dev/null +++ b/maubot-src/maubot/management/api/plugin_upload.py @@ -0,0 +1,145 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from io import BytesIO +from time import time +import logging +import os.path +import re +import traceback + +from aiohttp import web +from packaging.version import Version + +from ...loader import DatabaseType, MaubotZipImportError, PluginLoader, ZippedPluginLoader +from .base import get_config, routes +from .responses import resp + +try: + import sqlalchemy + + has_alchemy = True +except ImportError: + has_alchemy = False + +log = logging.getLogger("maubot.server.upload") + + +@routes.put("/plugin/{id}") +async def put_plugin(request: web.Request) -> web.Response: + plugin_id = request.match_info["id"] + content = await request.read() + file = BytesIO(content) + try: + pid, version, db_type = ZippedPluginLoader.verify_meta(file) + except MaubotZipImportError as e: + return resp.plugin_import_error(str(e), traceback.format_exc()) + if db_type == DatabaseType.SQLALCHEMY and not has_alchemy: + return resp.sqlalchemy_not_installed + if pid != plugin_id: + return resp.pid_mismatch + plugin = PluginLoader.id_cache.get(plugin_id, None) + if not plugin: + return await upload_new_plugin(content, pid, version) + elif isinstance(plugin, ZippedPluginLoader): + return await upload_replacement_plugin(plugin, content, version) + else: + return resp.unsupported_plugin_loader + + +@routes.post("/plugins/upload") +async def upload_plugin(request: web.Request) -> web.Response: + content = await request.read() + file = BytesIO(content) + try: + pid, version, db_type = ZippedPluginLoader.verify_meta(file) + except MaubotZipImportError as e: + return resp.plugin_import_error(str(e), traceback.format_exc()) + if db_type == DatabaseType.SQLALCHEMY and not has_alchemy: + return resp.sqlalchemy_not_installed + plugin = PluginLoader.id_cache.get(pid, None) + if not plugin: + return await upload_new_plugin(content, pid, version) + elif not request.query.get("allow_override"): + return resp.plugin_exists + elif isinstance(plugin, ZippedPluginLoader): + return await upload_replacement_plugin(plugin, content, version) + else: + return resp.unsupported_plugin_loader + + +async def upload_new_plugin(content: bytes, pid: str, version: Version) -> web.Response: + path = os.path.join(get_config()["plugin_directories.upload"], f"{pid}-v{version}.mbp") + with open(path, "wb") as p: + p.write(content) + try: + plugin = ZippedPluginLoader.get(path) + except MaubotZipImportError as e: + ZippedPluginLoader.trash(path) + return resp.plugin_import_error(str(e), traceback.format_exc()) + return resp.created(plugin.to_dict()) + + +async def upload_replacement_plugin( + plugin: ZippedPluginLoader, content: bytes, new_version: Version +) -> web.Response: + dirname = os.path.dirname(plugin.path) + old_filename = os.path.basename(plugin.path) + if str(plugin.meta.version) in old_filename: + replacement = ( + str(new_version) + if plugin.meta.version != new_version + else f"{new_version}-ts{int(time() * 1000)}" + ) + filename = re.sub( + f"{re.escape(str(plugin.meta.version))}(-ts[0-9]+)?", replacement, old_filename + ) + else: + filename = old_filename.rstrip(".mbp") + filename = f"{filename}-v{new_version}.mbp" + path = os.path.join(dirname, filename) + with open(path, "wb") as p: + p.write(content) + old_path = plugin.path + await plugin.stop_instances() + try: + await plugin.reload(new_path=path) + except MaubotZipImportError as e: + log.exception(f"Error loading updated version of {plugin.meta.id}, rolling back") + try: + await plugin.reload(new_path=old_path) + await plugin.start_instances() + except MaubotZipImportError: + log.warning(f"Failed to roll back update of {plugin.meta.id}", exc_info=True) + finally: + ZippedPluginLoader.trash(path, reason="failed_update") + return resp.plugin_import_error(str(e), traceback.format_exc()) + try: + await plugin.start_instances() + except Exception as e: + log.exception(f"Error starting {plugin.meta.id} instances after update, rolling back") + try: + await plugin.stop_instances() + await plugin.reload(new_path=old_path) + await plugin.start_instances() + except Exception: + log.warning(f"Failed to roll back update of {plugin.meta.id}", exc_info=True) + finally: + ZippedPluginLoader.trash(path, reason="failed_update") + return resp.plugin_reload_error(str(e), traceback.format_exc()) + + log.debug(f"Successfully updated {plugin.meta.id}, moving old version to trash") + ZippedPluginLoader.trash(old_path, reason="update") + return resp.updated(plugin.to_dict()) diff --git a/maubot-src/maubot/management/api/responses.py b/maubot-src/maubot/management/api/responses.py new file mode 100644 index 0000000..37844c7 --- /dev/null +++ b/maubot-src/maubot/management/api/responses.py @@ -0,0 +1,510 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +from typing import TYPE_CHECKING +from http import HTTPStatus + +from aiohttp import web +from asyncpg import PostgresError +import aiosqlite + +if TYPE_CHECKING: + from sqlalchemy.exc import IntegrityError, OperationalError + + +class _Response: + @property + def body_not_json(self) -> web.Response: + return web.json_response( + { + "error": "Request body is not JSON", + "errcode": "body_not_json", + }, + status=HTTPStatus.BAD_REQUEST, + ) + + @property + def plugin_type_required(self) -> web.Response: + return web.json_response( + { + "error": "Plugin type is required when creating plugin instances", + "errcode": "plugin_type_required", + }, + status=HTTPStatus.BAD_REQUEST, + ) + + @property + def primary_user_required(self) -> web.Response: + return web.json_response( + { + "error": "Primary user is required when creating plugin instances", + "errcode": "primary_user_required", + }, + status=HTTPStatus.BAD_REQUEST, + ) + + @property + def bad_client_access_token(self) -> web.Response: + return web.json_response( + { + "error": "Invalid access token", + "errcode": "bad_client_access_token", + }, + status=HTTPStatus.BAD_REQUEST, + ) + + @property + def bad_client_access_details(self) -> web.Response: + return web.json_response( + { + "error": "Invalid homeserver or access token", + "errcode": "bad_client_access_details", + }, + status=HTTPStatus.BAD_REQUEST, + ) + + @property + def bad_client_connection_details(self) -> web.Response: + return web.json_response( + { + "error": "Could not connect to homeserver", + "errcode": "bad_client_connection_details", + }, + status=HTTPStatus.BAD_REQUEST, + ) + + def mxid_mismatch(self, found: str) -> web.Response: + return web.json_response( + { + "error": ( + "The Matrix user ID of the client and the user ID of the access token don't " + f"match. Access token is for user {found}" + ), + "errcode": "mxid_mismatch", + }, + status=HTTPStatus.BAD_REQUEST, + ) + + def device_id_mismatch(self, found: str) -> web.Response: + return web.json_response( + { + "error": ( + "The Matrix device ID of the client and the device ID of the access token " + f"don't match. Access token is for device {found}" + ), + "errcode": "mxid_mismatch", + }, + status=HTTPStatus.BAD_REQUEST, + ) + + @property + def pid_mismatch(self) -> web.Response: + return web.json_response( + { + "error": "The ID in the path does not match the ID of the uploaded plugin", + "errcode": "pid_mismatch", + }, + status=HTTPStatus.BAD_REQUEST, + ) + + @property + def username_or_password_missing(self) -> web.Response: + return web.json_response( + { + "error": "Username or password missing", + "errcode": "username_or_password_missing", + }, + status=HTTPStatus.BAD_REQUEST, + ) + + @property + def query_missing(self) -> web.Response: + return web.json_response( + { + "error": "Query missing", + "errcode": "query_missing", + }, + status=HTTPStatus.BAD_REQUEST, + ) + + @staticmethod + def sql_error(error: PostgresError | aiosqlite.Error, query: str) -> web.Response: + return web.json_response( + { + "ok": False, + "query": query, + "error": str(error), + "errcode": "sql_error", + }, + status=HTTPStatus.BAD_REQUEST, + ) + + @staticmethod + def sql_operational_error(error: OperationalError, query: str) -> web.Response: + return web.json_response( + { + "ok": False, + "query": query, + "error": str(error.orig), + "full_error": str(error), + "errcode": "sql_operational_error", + }, + status=HTTPStatus.BAD_REQUEST, + ) + + @staticmethod + def sql_integrity_error(error: IntegrityError, query: str) -> web.Response: + return web.json_response( + { + "ok": False, + "query": query, + "error": str(error.orig), + "full_error": str(error), + "errcode": "sql_integrity_error", + }, + status=HTTPStatus.BAD_REQUEST, + ) + + @property + def bad_auth(self) -> web.Response: + return web.json_response( + { + "error": "Invalid username or password", + "errcode": "invalid_auth", + }, + status=HTTPStatus.UNAUTHORIZED, + ) + + @property + def no_token(self) -> web.Response: + return web.json_response( + { + "error": "Authorization token missing", + "errcode": "auth_token_missing", + }, + status=HTTPStatus.UNAUTHORIZED, + ) + + @property + def invalid_token(self) -> web.Response: + return web.json_response( + { + "error": "Invalid authorization token", + "errcode": "auth_token_invalid", + }, + status=HTTPStatus.UNAUTHORIZED, + ) + + @property + def plugin_not_found(self) -> web.Response: + return web.json_response( + { + "error": "Plugin not found", + "errcode": "plugin_not_found", + }, + status=HTTPStatus.NOT_FOUND, + ) + + @property + def client_not_found(self) -> web.Response: + return web.json_response( + { + "error": "Client not found", + "errcode": "client_not_found", + }, + status=HTTPStatus.NOT_FOUND, + ) + + @property + def primary_user_not_found(self) -> web.Response: + return web.json_response( + { + "error": "Client for given primary user not found", + "errcode": "primary_user_not_found", + }, + status=HTTPStatus.NOT_FOUND, + ) + + @property + def instance_not_found(self) -> web.Response: + return web.json_response( + { + "error": "Plugin instance not found", + "errcode": "instance_not_found", + }, + status=HTTPStatus.NOT_FOUND, + ) + + @property + def plugin_type_not_found(self) -> web.Response: + return web.json_response( + { + "error": "Given plugin type not found", + "errcode": "plugin_type_not_found", + }, + status=HTTPStatus.NOT_FOUND, + ) + + @property + def path_not_found(self) -> web.Response: + return web.json_response( + { + "error": "Resource not found", + "errcode": "resource_not_found", + }, + status=HTTPStatus.NOT_FOUND, + ) + + @property + def server_not_found(self) -> web.Response: + return web.json_response( + { + "error": "Registration target server not found", + "errcode": "server_not_found", + }, + status=HTTPStatus.NOT_FOUND, + ) + + @property + def registration_secret_not_found(self) -> web.Response: + return web.json_response( + { + "error": "Config does not have a registration secret for that server", + "errcode": "registration_secret_not_found", + }, + status=HTTPStatus.NOT_FOUND, + ) + + @property + def registration_no_sso(self) -> web.Response: + return web.json_response( + { + "error": "The register operation is only for registering with a password", + "errcode": "registration_no_sso", + }, + status=HTTPStatus.BAD_REQUEST, + ) + + @property + def sso_not_supported(self) -> web.Response: + return web.json_response( + { + "error": "That server does not seem to support single sign-on", + "errcode": "sso_not_supported", + }, + status=HTTPStatus.FORBIDDEN, + ) + + @property + def plugin_has_no_database(self) -> web.Response: + return web.json_response( + { + "error": "Given plugin does not have a database", + "errcode": "plugin_has_no_database", + } + ) + + @property + def unsupported_plugin_database(self) -> web.Response: + return web.json_response( + { + "error": "The database type is not supported by this API", + "errcode": "unsupported_plugin_database", + } + ) + + @property + def sqlalchemy_not_installed(self) -> web.Response: + return web.json_response( + { + "error": "This plugin requires a legacy database, but SQLAlchemy is not installed", + "errcode": "unsupported_plugin_database", + }, + status=HTTPStatus.NOT_IMPLEMENTED, + ) + + @property + def table_not_found(self) -> web.Response: + return web.json_response( + { + "error": "Given table not found in plugin database", + "errcode": "table_not_found", + } + ) + + @property + def method_not_allowed(self) -> web.Response: + return web.json_response( + { + "error": "Method not allowed", + "errcode": "method_not_allowed", + }, + status=HTTPStatus.METHOD_NOT_ALLOWED, + ) + + @property + def user_exists(self) -> web.Response: + return web.json_response( + { + "error": "There is already a client with the user ID of that token", + "errcode": "user_exists", + }, + status=HTTPStatus.CONFLICT, + ) + + @property + def plugin_exists(self) -> web.Response: + return web.json_response( + { + "error": "A plugin with the same ID as the uploaded plugin already exists", + "errcode": "plugin_exists", + }, + status=HTTPStatus.CONFLICT, + ) + + @property + def plugin_in_use(self) -> web.Response: + return web.json_response( + { + "error": "Plugin instances of this type still exist", + "errcode": "plugin_in_use", + }, + status=HTTPStatus.PRECONDITION_FAILED, + ) + + @property + def client_in_use(self) -> web.Response: + return web.json_response( + { + "error": "Plugin instances with this client as their primary user still exist", + "errcode": "client_in_use", + }, + status=HTTPStatus.PRECONDITION_FAILED, + ) + + @staticmethod + def plugin_import_error(error: str, stacktrace: str) -> web.Response: + return web.json_response( + { + "error": error, + "stacktrace": stacktrace, + "errcode": "plugin_invalid", + }, + status=HTTPStatus.BAD_REQUEST, + ) + + @staticmethod + def plugin_reload_error(error: str, stacktrace: str) -> web.Response: + return web.json_response( + { + "error": error, + "stacktrace": stacktrace, + "errcode": "plugin_reload_fail", + }, + status=HTTPStatus.INTERNAL_SERVER_ERROR, + ) + + @property + def internal_server_error(self) -> web.Response: + return web.json_response( + { + "error": "Internal server error", + "errcode": "internal_server_error", + }, + status=HTTPStatus.INTERNAL_SERVER_ERROR, + ) + + @property + def invalid_server(self) -> web.Response: + return web.json_response( + { + "error": "Invalid registration server object in maubot configuration", + "errcode": "invalid_server", + }, + status=HTTPStatus.INTERNAL_SERVER_ERROR, + ) + + @property + def unsupported_plugin_loader(self) -> web.Response: + return web.json_response( + { + "error": "Existing plugin with same ID uses unsupported plugin loader", + "errcode": "unsupported_plugin_loader", + }, + status=HTTPStatus.BAD_REQUEST, + ) + + @property + def client_has_keys(self) -> web.Response: + return web.json_response( + { + "error": "Client already has cross-signing keys", + "errcode": "client_has_keys", + }, + status=HTTPStatus.CONFLICT, + ) + + def internal_crypto_error(self, message: str) -> web.Response: + return web.json_response( + { + "error": f"Internal crypto error: {message}", + "errcode": "internal_crypto_error", + }, + status=HTTPStatus.INTERNAL_SERVER_ERROR, + ) + + @property + def not_implemented(self) -> web.Response: + return web.json_response( + { + "error": "Not implemented", + "errcode": "not_implemented", + }, + status=HTTPStatus.NOT_IMPLEMENTED, + ) + + @property + def ok(self) -> web.Response: + return web.json_response( + {"success": True}, + status=HTTPStatus.OK, + ) + + @property + def deleted(self) -> web.Response: + return web.Response(status=HTTPStatus.NO_CONTENT) + + @staticmethod + def found(data: dict) -> web.Response: + return web.json_response(data, status=HTTPStatus.OK) + + @staticmethod + def updated(data: dict, is_login: bool = False) -> web.Response: + return web.json_response(data, status=HTTPStatus.ACCEPTED if is_login else HTTPStatus.OK) + + def logged_in(self, token: str) -> web.Response: + return self.found({"token": token}) + + def pong(self, user: str, features: dict) -> web.Response: + return self.found({"username": user, "features": features}) + + @staticmethod + def created(data: dict) -> web.Response: + return web.json_response(data, status=HTTPStatus.CREATED) + + +resp = _Response() diff --git a/maubot-src/maubot/management/api/spec.md b/maubot-src/maubot/management/api/spec.md new file mode 100644 index 0000000..88978ab --- /dev/null +++ b/maubot-src/maubot/management/api/spec.md @@ -0,0 +1,2 @@ +# Maubot Management API +This document has been moved to docs.mau.fi: diff --git a/maubot-src/maubot/management/api/spec.yaml b/maubot-src/maubot/management/api/spec.yaml new file mode 100644 index 0000000..8529599 --- /dev/null +++ b/maubot-src/maubot/management/api/spec.yaml @@ -0,0 +1,728 @@ +openapi: 3.0.0 +info: + title: Maubot Management + version: 0.1.0 + description: The API to manage a [maubot](https://github.com/maubot/maubot) instance + license: + name: GNU Affero General Public License version 3 + url: 'https://github.com/maubot/maubot/blob/master/LICENSE' +security: +- bearer: [] +servers: +- url: /_matrix/maubot/v1 + +paths: + /auth/login: + post: + operationId: login + summary: Log in with the unshared secret or username+password + tags: [Authentication] + requestBody: + content: + application/json: + schema: + type: object + description: Set either username+password or secret. + properties: + secret: + type: string + description: The unshared server secret for root login + username: + type: string + description: The username for normal login + password: + type: string + description: The password for normal login + responses: + 200: + description: Logged in successfully + content: + application/json: + schema: + type: object + properties: + token: + type: string + 401: + description: Invalid credentials + /auth/ping: + post: + operationId: ping + summary: Check if the given token is valid + tags: [Authentication] + responses: + 200: + description: Token is OK + content: + application/json: + schema: + type: object + properties: + username: + type: string + 401: + description: Token is not OK + + /plugins: + get: + operationId: get_plugins + summary: Get the list of installed plugins + tags: [Plugins] + responses: + 200: + description: The list of plugins + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Plugin' + 401: + $ref: '#/components/responses/Unauthorized' + /plugins/upload: + post: + operationId: upload_plugin + summary: Upload a new plugin + description: Upload a new plugin. If the plugin already exists, enabled instances will be restarted. + tags: [Plugins] + parameters: + - name: allow_override + in: query + description: Set to allow overriding existing plugins + required: false + schema: + type: boolean + default: false + requestBody: + content: + application/zip: + schema: + type: string + format: binary + example: The plugin maubot archive (.mbp) + responses: + 200: + description: Plugin uploaded and replaced current version successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Plugin' + 201: + description: New plugin uploaded successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Plugin' + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' + 409: + description: Plugin already exists and allow_override was not specified. + '/plugin/{id}': + parameters: + - name: id + in: path + description: The ID of the plugin to get + required: true + schema: + type: string + get: + operationId: get_plugin + summary: Get information about a specific plugin + tags: [Plugins] + responses: + 200: + description: Plugin found + content: + application/json: + schema: + $ref: '#/components/schemas/Plugin' + 401: + $ref: '#/components/responses/Unauthorized' + 404: + $ref: '#/components/responses/PluginNotFound' + delete: + operationId: delete_plugin + summary: Delete a plugin + description: Delete a plugin. All instances of the plugin must be deleted before deleting the plugin. + tags: [Plugins] + responses: + 204: + description: Plugin deleted + 401: + $ref: '#/components/responses/Unauthorized' + 404: + $ref: '#/components/responses/PluginNotFound' + 412: + description: One or more plugin instances of this type exist + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + put: + operationId: put_plugin + summary: Upload a new or replacement plugin + description: | + Upload a new or replacement plugin with the specified ID. + A HTTP 400 will be returned if the ID of the uploaded plugin + doesn't match the ID in the path. If the plugin already + exists, enabled instances will be restarted. + tags: [Plugins] + requestBody: + content: + application/zip: + schema: + type: string + format: binary + example: The plugin maubot archive (.mbp) + responses: + 200: + description: Plugin uploaded and replaced current version successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Plugin' + 201: + description: New plugin uploaded successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Plugin' + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' + /plugin/{id}/reload: + parameters: + - name: id + in: path + description: The ID of the plugin to get + required: true + schema: + type: string + post: + operationId: reload_plugin + summary: Reload a plugin from disk + tags: [Plugins] + responses: + 200: + description: Plugin reloaded + 401: + $ref: '#/components/responses/Unauthorized' + 404: + $ref: '#/components/responses/PluginNotFound' + + /instances: + get: + operationId: get_instances + summary: Get all plugin instances + tags: [Plugin instances] + responses: + 200: + description: The list of instances + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PluginInstance' + 401: + $ref: '#/components/responses/Unauthorized' + '/instance/{id}': + parameters: + - name: id + in: path + description: The ID of the instance to get + required: true + schema: + type: string + get: + operationId: get_instance + summary: Get information about a specific plugin instance + tags: [Plugin instances] + responses: + 200: + description: Plugin instance found + content: + application/json: + schema: + $ref: '#/components/schemas/PluginInstance' + 401: + $ref: '#/components/responses/Unauthorized' + 404: + $ref: '#/components/responses/InstanceNotFound' + delete: + operationId: delete_instance + summary: Delete a specific plugin instance + tags: [Plugin instances] + responses: + 204: + description: Plugin instance deleted + 401: + $ref: '#/components/responses/Unauthorized' + 404: + $ref: '#/components/responses/InstanceNotFound' + put: + operationId: update_instance + summary: Create a plugin instance or edit the details of an existing plugin instance + tags: [Plugin instances] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PluginInstance' + responses: + 200: + description: Plugin instance edited + 201: + description: Plugin instance created + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' + 404: + description: The referenced client or plugin type could not be found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + '/clients': + get: + operationId: get_clients + summary: Get the list of Matrix clients + tags: [Clients] + responses: + 200: + description: The list of plugins + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/MatrixClient' + 401: + $ref: '#/components/responses/Unauthorized' + /client/new: + post: + operationId: create_client + summary: Create a Matrix client + tags: [Clients] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MatrixClient' + responses: + 201: + description: Client created + content: + application/json: + schema: + $ref: '#/components/schemas/MatrixClient' + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' + 404: + $ref: '#/components/responses/ClientNotFound' + 409: + description: There is already a client with the user ID of that token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '/client/{id}': + parameters: + - name: id + in: path + description: The Matrix user ID of the client to get + required: true + schema: + type: string + get: + operationId: get_client + summary: Get information about a specific Matrix client + tags: [Clients] + responses: + 200: + description: Client found + content: + application/json: + schema: + $ref: '#/components/schemas/MatrixClient' + 401: + $ref: '#/components/responses/Unauthorized' + 404: + $ref: '#/components/responses/ClientNotFound' + put: + operationId: update_client + summary: Create or update a Matrix client + tags: [Clients] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MatrixClient' + responses: + 202: + description: Client updated + content: + application/json: + schema: + $ref: '#/components/schemas/MatrixClient' + 201: + description: Client created + content: + application/json: + schema: + $ref: '#/components/schemas/MatrixClient' + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' + delete: + operationId: delete_client + summary: Delete a Matrix client + tags: [Clients] + responses: + 204: + description: Client deleted + 401: + $ref: '#/components/responses/Unauthorized' + 404: + $ref: '#/components/responses/ClientNotFound' + 412: + description: One or more plugin instances with this as their primary client exist + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '/client/{id}/clearcache': + parameters: + - name: id + in: path + description: The Matrix user ID of the client to change + required: true + schema: + type: string + put: + operationId: clear_client_cache + summary: Clear the sync/state cache of a Matrix client + tags: [Clients] + responses: + 200: + description: Cache cleared + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + 401: + $ref: '#/components/responses/Unauthorized' + 404: + $ref: '#/components/responses/ClientNotFound' + /client/auth/servers: + get: + operationId: get_client_auth_servers + summary: Get the list of servers you can register or log in on via the maubot server + tags: [Clients] + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + description: Key-value map from server name to homeserver URL + additionalProperties: + type: string + description: The homeserver URL + example: + maunium.net: https://maunium.net + example.com: https://matrix.example.org + 401: + $ref: '#/components/responses/Unauthorized' + '/client/auth/{server}/register': + parameters: + - name: server + in: path + description: The server name to register the account on. + required: true + schema: + type: string + - name: update_client + in: query + description: Should maubot store the access details in a Client instead of returning them? + required: false + schema: + type: boolean + post: + operationId: client_auth_register + summary: | + Register a new account on the given Matrix server using the shared registration + secret configured into the maubot server. + tags: [Clients] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MatrixAuthentication' + responses: + 200: + description: Registration successful + content: + application/json: + schema: + type: object + properties: + access_token: + type: string + example: syt_123_456_789 + user_id: + type: string + example: '@putkiteippi:maunium.net' + device_id: + type: string + example: maubot_F00BAR12 + 201: + description: Client created (when update_client is true) + content: + application/json: + schema: + $ref: '#/components/schemas/MatrixClient' + 401: + $ref: '#/components/responses/Unauthorized' + 409: + description: | + There is already a client with the user ID of that token. + This should usually not happen, because the user ID was just created. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + 500: + $ref: '#/components/responses/MatrixServerError' + '/client/auth/{server}/login': + parameters: + - name: server + in: path + description: The server name to log in to. + required: true + schema: + type: string + - name: update_client + in: query + description: Should maubot store the access details in a Client instead of returning them? + required: false + schema: + type: boolean + post: + operationId: client_auth_login + summary: Log in to the given Matrix server via the maubot server + tags: [Clients] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MatrixAuthentication' + responses: + 200: + description: Login successful + content: + application/json: + schema: + type: object + properties: + user_id: + type: string + example: '@putkiteippi:maunium.net' + access_token: + type: string + example: syt_123_456_789 + device_id: + type: string + example: maubot_F00BAR12 + 201: + description: Client created (when update_client is true) + content: + application/json: + schema: + $ref: '#/components/schemas/MatrixClient' + 202: + description: Client updated (when update_client is true) + content: + application/json: + schema: + $ref: '#/components/schemas/MatrixClient' + 401: + $ref: '#/components/responses/Unauthorized' + 500: + $ref: '#/components/responses/MatrixServerError' + +components: + responses: + Unauthorized: + description: Invalid or missing access token + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + PluginNotFound: + description: Plugin not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + ClientNotFound: + description: Client not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + InstanceNotFound: + description: Plugin instance not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + BadRequest: + description: Bad request (e.g. bad request body) + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + MatrixServerError: + description: The Matrix server returned an error + content: + application/json: + schema: + type: object + properties: + errcode: + type: string + description: The `errcode` returned by the server. + error: + type: string + description: The human-readable error returned by the server. + http_status: + type: integer + description: The HTTP status returned by the server. + + securitySchemes: + bearer: + type: http + scheme: bearer + description: Required authentication for all endpoints + schemas: + Error: + type: object + properties: + error: + type: string + description: A human-readable error message + errcode: + type: string + description: A simple error code + Plugin: + type: object + properties: + id: + type: string + example: xyz.maubot.jesaribot + version: + type: string + example: 2.0.0 + instances: + type: array + items: + $ref: '#/components/schemas/PluginInstance' + PluginInstance: + type: object + properties: + id: + type: string + example: jesaribot + type: + type: string + example: xyz.maubot.jesaribot + enabled: + type: boolean + example: true + started: + type: boolean + example: true + primary_user: + type: string + example: '@putkiteippi:maunium.net' + config: + type: string + example: "YAML" + MatrixClient: + type: object + properties: + id: + type: string + example: '@putkiteippi:maunium.net' + readOnly: true + description: The Matrix user ID of this client. + homeserver: + type: string + example: 'https://maunium.net' + description: The homeserver URL for this client. + access_token: + type: string + description: The Matrix access token for this client. + device_id: + type: string + description: The Matrix device ID corresponding to the access token. + fingerprint: + type: string + description: The encryption device fingerprint for verification. + enabled: + type: boolean + example: true + description: Whether or not this client is enabled. + started: + type: boolean + example: true + description: Whether or not this client and its instances have been started. + sync: + type: boolean + example: true + description: Whether or not syncing is enabled on this client. + sync_ok: + type: boolean + example: true + description: Whether or not the previous sync was successful on this client. + autojoin: + type: boolean + example: true + description: Whether or not this client should automatically join rooms when invited. + displayname: + type: string + example: J. E. Saarinen + description: The display name for this client. + avatar_url: + type: string + example: 'mxc://maunium.net/FsPQQTntCCqhJMFtwArmJdaU' + description: The content URI of the avatar for this client. + instances: + type: array + readOnly: true + items: + $ref: '#/components/schemas/PluginInstance' + MatrixAuthentication: + type: object + properties: + username: + type: string + example: putkiteippi + description: The user ID localpart to register/log in as. + password: + type: string + example: p455w0rd + description: The password for/of the user. diff --git a/maubot-src/maubot/management/frontend/.eslintrc.json b/maubot-src/maubot/management/frontend/.eslintrc.json new file mode 100644 index 0000000..8451d44 --- /dev/null +++ b/maubot-src/maubot/management/frontend/.eslintrc.json @@ -0,0 +1,67 @@ +{ + "extends": "react-app", + "plugins": [ + "import" + ], + "rules": { + "indent": ["error", 4, { + "ignoredNodes": [ + "JSXAttribute", + "JSXSpreadAttribute" + ], + "FunctionDeclaration": {"parameters": "first"}, + "FunctionExpression": {"parameters": "first"}, + "CallExpression": {"arguments": "first"}, + "ArrayExpression": "first", + "ObjectExpression": "first", + "ImportDeclaration": "first" + }], + "react/jsx-indent-props": ["error", "first"], + "object-curly-newline": ["error", { + "consistent": true + }], + "object-curly-spacing": ["error", "always", { + "arraysInObjects": false, + "objectsInObjects": false + }], + "array-bracket-spacing": ["error", "never"], + "one-var": ["error", { + "initialized": "never", + "uninitialized": "always" + }], + "one-var-declaration-per-line": ["error", "initializations"], + "quotes": ["error", "double"], + "semi": ["error", "never"], + "comma-dangle": ["error", "always-multiline"], + "max-len": ["warn", 100], + "space-before-function-paren": ["error", { + "anonymous": "never", + "named": "never", + "asyncArrow": "always" + }], + "func-style": ["warn", "declaration", {"allowArrowFunctions": true}], + "id-length": ["warn", {"max": 40, "exceptions": ["i", "j", "x", "y", "_"]}], + "arrow-body-style": ["error", "as-needed"], + "new-cap": ["warn", { + "newIsCap": true, + "capIsNew": true + }], + "no-empty": ["error", { + "allowEmptyCatch": true + }], + "eol-last": ["error", "always"], + "no-console": "off", + "import/no-nodejs-modules": "error", + "import/order": ["warn", { + "groups": [ + "builtin", + "external", + "internal", + "parent", + "sibling", + "index" + ], + "newlines-between": "never" + }] + } +} diff --git a/maubot-src/maubot/management/frontend/.gitignore b/maubot-src/maubot/management/frontend/.gitignore new file mode 100644 index 0000000..97c5f10 --- /dev/null +++ b/maubot-src/maubot/management/frontend/.gitignore @@ -0,0 +1,5 @@ +/node_modules +/build +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/maubot-src/maubot/management/frontend/.sass-lint.yml b/maubot-src/maubot/management/frontend/.sass-lint.yml new file mode 100644 index 0000000..61fdcfc --- /dev/null +++ b/maubot-src/maubot/management/frontend/.sass-lint.yml @@ -0,0 +1,27 @@ +options: + merge-default-rules: false + formatter: html + max-warnings: 50 + +files: + include: 'src/style/**/*.sass' + +rules: + extends-before-mixins: 2 + extends-before-declarations: 2 + placeholder-in-extend: 2 + mixins-before-declarations: + - 2 + - exclude: + - breakpoint + - mq + no-warn: 1 + no-debug: 1 + hex-notation: + - 2 + - style: uppercase + indentation: + - 2 + - size: 4 + property-sort-order: + - 0 diff --git a/maubot-src/maubot/management/frontend/package.json b/maubot-src/maubot/management/frontend/package.json new file mode 100644 index 0000000..294c1f7 --- /dev/null +++ b/maubot-src/maubot/management/frontend/package.json @@ -0,0 +1,40 @@ +{ + "name": "maubot-manager", + "version": "0.1.1", + "private": true, + "author": "Tulir Asokan", + "license": "AGPL-3.0-or-later", + "repository": { + "type": "git", + "url": "git+https://github.com/maubot/maubot.git" + }, + "bugs": { + "url": "https://github.com/maubot/maubot/issues" + }, + "homepage": ".", + "dependencies": { + "react": "^17.0.2", + "react-ace": "^9.4.1", + "react-contextmenu": "^2.14.0", + "react-dom": "^17.0.2", + "react-json-tree": "^0.16.1", + "react-router-dom": "^5.3.0", + "react-scripts": "5.0.0", + "react-select": "^5.2.1", + "sass": "^1.34.1" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "browserslist": [ + "last 2 firefox versions", + "last 2 and_ff versions", + "last 2 chrome versions", + "last 2 and_chr versions", + "last 1 safari versions", + "last 1 ios_saf versions" + ] +} diff --git a/maubot-src/maubot/management/frontend/public/favicon.png b/maubot-src/maubot/management/frontend/public/favicon.png new file mode 100644 index 0000000..43fab1b Binary files /dev/null and b/maubot-src/maubot/management/frontend/public/favicon.png differ diff --git a/maubot-src/maubot/management/frontend/public/index.html b/maubot-src/maubot/management/frontend/public/index.html new file mode 100644 index 0000000..d3679bf --- /dev/null +++ b/maubot-src/maubot/management/frontend/public/index.html @@ -0,0 +1,37 @@ + + + + + + + + + + + + + Maubot Manager + + + +
+ + diff --git a/maubot-src/maubot/management/frontend/public/manifest.json b/maubot-src/maubot/management/frontend/public/manifest.json new file mode 100644 index 0000000..a76f730 --- /dev/null +++ b/maubot-src/maubot/management/frontend/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "Maubot", + "name": "Maubot Manager", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 48x48 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#50D367", + "background_color": "#FAFAFA" +} diff --git a/maubot-src/maubot/management/frontend/src/api.js b/maubot-src/maubot/management/frontend/src/api.js new file mode 100644 index 0000000..27b3ff6 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/api.js @@ -0,0 +1,276 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +let BASE_PATH = "/_matrix/maubot/v1" + +export function setBasePath(basePath) { + BASE_PATH = basePath +} + +function getHeaders(contentType = "application/json") { + return { + "Content-Type": contentType, + "Authorization": `Bearer ${localStorage.accessToken}`, + } +} + +async function defaultDelete(type, id) { + const resp = await fetch(`${BASE_PATH}/${type}/${id}`, { + headers: getHeaders(), + method: "DELETE", + }) + if (resp.status === 204) { + return { + "success": true, + } + } + return await resp.json() +} + +async function defaultPut(type, entry, id = undefined, suffix = undefined) { + const resp = await fetch(`${BASE_PATH}/${type}/${id || entry.id}${suffix || ""}`, { + headers: getHeaders(), + body: JSON.stringify(entry), + method: "PUT", + }) + return await resp.json() +} + +async function defaultGet(path) { + const resp = await fetch(`${BASE_PATH}${path}`, { headers: getHeaders() }) + return await resp.json() +} + +export async function login(username, password) { + const resp = await fetch(`${BASE_PATH}/auth/login`, { + method: "POST", + body: JSON.stringify({ + username, + password, + }), + }) + return await resp.json() +} + +let features = null + +export async function ping() { + const response = await fetch(`${BASE_PATH}/auth/ping`, { + method: "POST", + headers: getHeaders(), + }) + const json = await response.json() + if (json.username) { + features = json.features + return json.username + } else if (json.errcode === "auth_token_missing" || json.errcode === "auth_token_invalid") { + if (!features) { + await remoteGetFeatures() + } + return null + } + throw json +} + +export const remoteGetFeatures = async () => { + features = await defaultGet("/features") +} + +export const getFeatures = () => features + +export async function openLogSocket() { + let protocol = window.location.protocol === "https:" ? "wss:" : "ws:" + const url = `${protocol}//${window.location.host}${BASE_PATH}/logs` + const wrapper = { + socket: null, + connected: false, + authenticated: false, + onLog: data => undefined, + onHistory: history => undefined, + fails: -1, + } + const openHandler = () => { + wrapper.socket.send(localStorage.accessToken) + wrapper.connected = true + } + const messageHandler = evt => { + // TODO use logs + const data = JSON.parse(evt.data) + if (data.auth_success !== undefined) { + if (data.auth_success) { + console.info("Websocket connection authentication successful") + wrapper.authenticated = true + wrapper.fails = -1 + } else { + console.info("Websocket connection authentication failed") + } + } else if (data.history) { + wrapper.onHistory(data.history) + } else { + wrapper.onLog(data) + } + } + const closeHandler = evt => { + if (evt) { + if (evt.code === 4000) { + console.error("Websocket connection failed: access token invalid or not provided") + } else if (evt.code === 1012) { + console.info("Websocket connection closed: server is restarting") + } + } + wrapper.connected = false + wrapper.socket = null + wrapper.fails++ + const SECOND = 1000 + setTimeout(() => { + wrapper.socket = new WebSocket(url) + wrapper.socket.onopen = openHandler + wrapper.socket.onmessage = messageHandler + wrapper.socket.onclose = closeHandler + }, Math.min(wrapper.fails * 5 * SECOND, 30 * SECOND)) + } + + closeHandler() + + return wrapper +} + +let _debugOpenFileEnabled = undefined +export const debugOpenFileEnabled = () => _debugOpenFileEnabled +export const updateDebugOpenFileEnabled = async () => { + const resp = await defaultGet("/debug/open") + _debugOpenFileEnabled = resp["enabled"] || false +} + +export async function debugOpenFile(path, line) { + const resp = await fetch(`${BASE_PATH}/debug/open`, { + headers: getHeaders(), + body: JSON.stringify({ path, line }), + method: "POST", + }) + return await resp.json() +} + +export const getInstances = () => defaultGet("/instances") +export const getInstance = id => defaultGet(`/instance/${id}`) +export const putInstance = (instance, id) => defaultPut("instance", instance, id) +export const deleteInstance = id => defaultDelete("instance", id) + +export const getInstanceDatabase = id => defaultGet(`/instance/${id}/database`) +export const queryInstanceDatabase = async (id, query) => { + const resp = await fetch(`${BASE_PATH}/instance/${id}/database/query`, { + headers: getHeaders(), + body: JSON.stringify({ query }), + method: "POST", + }) + return await resp.json() +} + +export const getPlugins = () => defaultGet("/plugins") +export const getPlugin = id => defaultGet(`/plugin/${id}`) +export const deletePlugin = id => defaultDelete("plugin", id) + +export async function uploadPlugin(data, id) { + let resp + if (id) { + resp = await fetch(`${BASE_PATH}/plugin/${id}`, { + headers: getHeaders("application/zip"), + body: data, + method: "PUT", + }) + } else { + resp = await fetch(`${BASE_PATH}/plugins/upload`, { + headers: getHeaders("application/zip"), + body: data, + method: "POST", + }) + } + return await resp.json() +} + +export const getClients = () => defaultGet("/clients") +export const getClient = id => defaultGet(`/clients/${id}`) + +export async function uploadAvatar(id, data, mime) { + const resp = await fetch(`${BASE_PATH}/proxy/${id}/_matrix/media/v3/upload`, { + headers: getHeaders(mime), + body: data, + method: "POST", + }) + return await resp.json() +} + +export function getAvatarURL({ id, avatar_url }) { + if (!avatar_url?.startsWith("mxc://")) { + return null + } + avatar_url = avatar_url.substring("mxc://".length) + // Note: the maubot backend will replace the query param with an authorization header + return `${BASE_PATH}/proxy/${id}/_matrix/client/v1/media/download/${avatar_url}?access_token=${ + localStorage.accessToken}` +} + +export const putClient = client => defaultPut("client", client) +export const deleteClient = id => defaultDelete("client", id) + +export async function clearClientCache(id) { + const resp = await fetch(`${BASE_PATH}/client/${id}/clearcache`, { + headers: getHeaders(), + method: "POST", + }) + return await resp.json() +} + +export async function verifyClient(id, recovery_key) { + const resp = await fetch(`${BASE_PATH}/client/${id}/verify`, { + headers: getHeaders(), + method: "POST", + body: JSON.stringify({ recovery_key }), + }) + return await resp.json() +} + +export async function generateRecoveryKey(id) { + const resp = await fetch(`${BASE_PATH}/client/${id}/generate_recovery_key`, { + headers: getHeaders(), + method: "POST", + }) + return await resp.json() +} + +export const getClientAuthServers = () => defaultGet("/client/auth/servers") + +export async function doClientAuth(server, type, username, password) { + const resp = await fetch(`${BASE_PATH}/client/auth/${server}/${type}`, { + headers: getHeaders(), + body: JSON.stringify({ username, password }), + method: "POST", + }) + return await resp.json() +} + +// eslint-disable-next-line import/no-anonymous-default-export +export default { + login, ping, setBasePath, getFeatures, remoteGetFeatures, + openLogSocket, + debugOpenFile, debugOpenFileEnabled, updateDebugOpenFileEnabled, + getInstances, getInstance, putInstance, deleteInstance, + getInstanceDatabase, queryInstanceDatabase, + getPlugins, getPlugin, uploadPlugin, deletePlugin, + getClients, getClient, uploadAvatar, getAvatarURL, putClient, deleteClient, clearClientCache, + verifyClient, generateRecoveryKey, + getClientAuthServers, doClientAuth, +} diff --git a/maubot-src/maubot/management/frontend/src/components/PreferenceTable.js b/maubot-src/maubot/management/frontend/src/components/PreferenceTable.js new file mode 100644 index 0000000..ca965a0 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/components/PreferenceTable.js @@ -0,0 +1,71 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React from "react" +import Select from "react-select" +import CreatableSelect from "react-select/creatable" +import Switch from "./Switch" + +export const PrefTable = ({ children, wrapperClass }) => { + if (wrapperClass) { + return ( +
+
+ {children} +
+
+ ) + } + return ( +
+ {children} +
+ ) +} + +export const PrefRow = + ({ name, fullWidth = false, labelFor = undefined, changed = false, children }) => ( +
+ +
{children}
+
+ ) + +export const PrefInput = ({ rowName, value, origValue, fullWidth = false, ...args }) => ( + + + +) + +export const PrefSwitch = ({ rowName, active, origActive, fullWidth = false, ...args }) => ( + + + +) + +export const PrefSelect = ({ + rowName, value, origValue, fullWidth = false, creatable = false, ...args +}) => ( + + {creatable + ? + : + + + {this.state.error &&
{this.state.error}
} + + + } +} + +export default Login diff --git a/maubot-src/maubot/management/frontend/src/pages/Main.js b/maubot-src/maubot/management/frontend/src/pages/Main.js new file mode 100644 index 0000000..5ee374e --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/pages/Main.js @@ -0,0 +1,91 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React, { Component } from "react" +import { HashRouter as Router, Switch } from "react-router-dom" +import PrivateRoute from "../components/PrivateRoute" +import Spinner from "../components/Spinner" +import api from "../api" +import Dashboard from "./dashboard" +import Login from "./Login" + +class Main extends Component { + constructor(props) { + super(props) + this.state = { + pinged: false, + authed: false, + } + } + + async componentDidMount() { + await this.getBasePath() + if (localStorage.accessToken) { + await this.ping() + } else { + await api.remoteGetFeatures() + } + this.setState({ pinged: true }) + } + + async getBasePath() { + try { + const resp = await fetch(process.env.PUBLIC_URL + "/paths.json", { + headers: { "Content-Type": "application/json" }, + }) + const apiPaths = await resp.json() + api.setBasePath(apiPaths.api_path) + } catch (err) { + console.error("Failed to get API path:", err) + } + } + + + async ping() { + try { + const username = await api.ping() + if (username) { + localStorage.username = username + this.setState({ authed: true }) + } else { + delete localStorage.accessToken + } + } catch (err) { + console.error(err) + } + } + + login = async (token) => { + localStorage.accessToken = token + await this.ping() + } + + render() { + if (!this.state.pinged) { + return + } + return +
+ + } + authed={!this.state.authed} to="/"/> + + +
+
+ } +} + +export default Main diff --git a/maubot-src/maubot/management/frontend/src/pages/dashboard/BaseMainView.js b/maubot-src/maubot/management/frontend/src/pages/dashboard/BaseMainView.js new file mode 100644 index 0000000..dd77081 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/pages/dashboard/BaseMainView.js @@ -0,0 +1,98 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React, { Component } from "react" +import { Link } from "react-router-dom" +import api from "../../api" + +class BaseMainView extends Component { + constructor(props) { + super(props) + this.state = Object.assign(this.initialState, props.entry) + } + + UNSAFE_componentWillReceiveProps(nextProps) { + const newState = Object.assign(this.initialState, nextProps.entry) + for (const key of this.entryKeys) { + if (this.props.entry[key] === nextProps.entry[key]) { + newState[key] = this.state[key] + } + } + this.setState(newState) + } + + delete = async () => { + if (!window.confirm(`Are you sure you want to delete ${this.state.id}?`)) { + return + } + this.setState({ deleting: true }) + const resp = await this.deleteFunc(this.state.id) + if (resp.success) { + this.props.history.push("/") + this.props.onDelete() + } else { + this.setState({ deleting: false, error: resp.error }) + } + } + + get entryKeys() { + return [] + } + + get initialState() { + return {} + } + + get hasInstances() { + return this.state.instances && this.state.instances.length > 0 + } + + get isNew() { + return !this.props.entry.id + } + + inputChange = event => { + if (!event.target.name) { + return + } + this.setState({ [event.target.name]: event.target.value }) + } + + async readFile(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.readAsArrayBuffer(file) + reader.onload = evt => resolve(evt.target.result) + reader.onerror = err => reject(err) + }) + } + + renderInstances = () => !this.isNew && ( +
+

{this.hasInstances ? "Instances" : "No instances :("}

+ {this.state.instances.map(instance => ( + + {instance.id} + + ))} +
+ ) + + renderLogButton = (filter) => !this.isNew && api.getFeatures().log &&
+ +
+} + +export default BaseMainView diff --git a/maubot-src/maubot/management/frontend/src/pages/dashboard/Client.js b/maubot-src/maubot/management/frontend/src/pages/dashboard/Client.js new file mode 100644 index 0000000..1504337 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/pages/dashboard/Client.js @@ -0,0 +1,392 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React from "react" +import { NavLink, withRouter } from "react-router-dom" +import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg" +import { ReactComponent as UploadButton } from "../../res/upload.svg" +import { ReactComponent as NoAvatarIcon } from "../../res/bot.svg" +import { PrefTable, PrefSwitch, PrefInput, PrefSelect } from "../../components/PreferenceTable" +import Spinner from "../../components/Spinner" +import api from "../../api" +import BaseMainView from "./BaseMainView" + +const ClientListEntry = ({ entry }) => { + const classes = ["client", "entry"] + if (!entry.enabled) { + classes.push("disabled") + } else if (!entry.started) { + classes.push("stopped") + } + const avatarMXC = entry.avatar_url === "disable" + ? entry.remote_avatar_url + : entry.avatar_url + const avatarURL = avatarMXC && api.getAvatarURL({ + id: entry.id, + avatar_url: avatarMXC, + }) + const displayname = ( + entry.displayname === "disable" + ? entry.remote_displayname + : entry.displayname + ) || entry.id + return ( + + {avatarURL + ? + : } + {displayname} + + + ) +} + +class Client extends BaseMainView { + static ListEntry = ClientListEntry + + constructor(props) { + super(props) + this.deleteFunc = api.deleteClient + this.recovery_key = null + this.homeserverOptions = [] + } + + get entryKeys() { + return ["id", "displayname", "homeserver", "avatar_url", "access_token", "device_id", + "sync", "autojoin", "online", "enabled", "started", "trust_state"] + } + + get initialState() { + return { + id: "", + displayname: "disable", + homeserver: "", + avatar_url: "disable", + access_token: "", + device_id: "", + fingerprint: null, + sync: true, + autojoin: true, + enabled: true, + online: true, + started: false, + trust_state: null, + + instances: [], + + uploadingAvatar: false, + saving: false, + deleting: false, + verifying: false, + startingOrStopping: false, + clearingCache: false, + error: "", + } + } + + get clientInState() { + const client = Object.assign({}, this.state) + delete client.uploadingAvatar + delete client.saving + delete client.deleting + delete client.startingOrStopping + delete client.clearingCache + delete client.error + delete client.instances + return client + } + + get selectedHomeserver() { + return this.state.homeserver + ? this.homeserverEntry([this.props.ctx.homeserversByURL[this.state.homeserver], + this.state.homeserver]) + : {} + } + + homeserverEntry = ([serverName, serverURL]) => serverURL && { + id: serverURL, + value: serverURL, + label: serverName || serverURL, + } + + componentDidUpdate(prevProps) { + this.updateHomeserverOptions() + } + + updateHomeserverOptions() { + this.homeserverOptions = Object + .entries(this.props.ctx.homeserversByName) + .map(this.homeserverEntry) + } + + isValidHomeserver(value) { + try { + return Boolean(new URL(value)) + } catch (err) { + return false + } + } + + avatarUpload = async event => { + const file = event.target.files[0] + this.setState({ + uploadingAvatar: true, + }) + const data = await this.readFile(file) + const resp = await api.uploadAvatar(this.state.id, data, file.type) + this.setState({ + uploadingAvatar: false, + avatar_url: resp.content_uri, + }) + } + + save = async () => { + this.setState({ saving: true }) + const resp = await api.putClient(this.clientInState) + if (resp.id) { + if (this.isNew) { + this.props.history.push(`/client/${resp.id}`) + } else { + this.setState({ saving: false, error: "" }) + } + this.props.onChange(resp) + } else { + this.setState({ saving: false, error: resp.error }) + } + } + + startOrStop = async () => { + this.setState({ startingOrStopping: true }) + const resp = await api.putClient({ + id: this.props.entry.id, + started: !this.props.entry.started, + }) + if (resp.id) { + this.props.onChange(resp) + this.setState({ startingOrStopping: false, error: "" }) + } else { + this.setState({ startingOrStopping: false, error: resp.error }) + } + } + + clearCache = async () => { + this.setState({ clearingCache: true }) + const resp = await api.clearClientCache(this.props.entry.id) + if (resp.success) { + this.setState({ clearingCache: false, error: "" }) + } else { + this.setState({ clearingCache: false, error: resp.error }) + } + } + + verifyRecoveryKey = async () => { + const recoveryKey = window.prompt("Enter recovery key") + if (!recoveryKey) { + return + } + this.setState({ verifying: true }) + const resp = await api.verifyClient(this.props.entry.id, recoveryKey) + if (resp.success) { + this.setState({ verifying: false, error: "" }) + } else { + this.setState({ verifying: false, error: resp.error }) + } + } + + generateRecoveryKey = async () => { + this.setState({ verifying: true }) + const resp = await api.generateRecoveryKey(this.props.entry.id) + if (resp.success) { + this.recovery_key = resp.recovery_key + this.setState({ verifying: false, error: "" }) + } else { + this.setState({ verifying: false, error: resp.error }) + } + } + + get loading() { + return this.state.saving || this.state.startingOrStopping + || this.clearingCache || this.state.deleting || this.state.verifying + } + + renderStartedContainer = () => { + let text + if (this.props.entry.started) { + if (this.props.entry.sync_ok) { + text = "Started" + } else { + text = "Erroring" + } + } else if (this.props.entry.enabled) { + text = "Stopped" + } else { + text = "Disabled" + } + return
+ + {text} +
+ } + + get avatarMXC() { + if (this.state.avatar_url === "disable") { + return this.props.entry.remote_avatar_url + } else if (!this.state.avatar_url?.startsWith("mxc://")) { + return null + } + return this.state.avatar_url + } + + get avatarURL() { + return api.getAvatarURL({ + id: this.state.id, + avatar_url: this.avatarMXC, + }) + } + + renderSidebar = () => !this.isNew && ( +
+
+ {this.avatarMXC && Avatar} + + evt.target.parentElement.classList.add("drag")} + onDragLeave={evt => evt.target.parentElement.classList.remove("drag")}/> + {this.state.uploadingAvatar && } +
+ {this.renderStartedContainer()} + {(this.props.entry.started || this.props.entry.enabled) && <> + + + } +
+ ) + + renderPreferences = () => ( + + + {api.getFeatures().client_auth ? ( + this.setState({ homeserver: value })} + creatable={true} isValidNewOption={this.isValidHomeserver}/> + ) : ( + + )} + + + {this.props.entry.fingerprint && <> + + + } + + + this.setState({ sync })}/> + this.setState({ autojoin })}/> + this.setState({ + enabled, + started: enabled && this.state.started, + })}/> + this.setState({ online })} /> + + ) + + renderPrefButtons = () => <> +
+ {!this.isNew && ( + + )} + +
+ {this.renderLogButton(this.state.id)} +
{this.state.error}
+ + + renderVerifyButtons = () => <> +
+ + +
+ {this.recovery_key &&
+ Recovery key: {this.recovery_key} +
} + + + render() { + return <> +
+ {this.renderSidebar()} +
+ {this.renderPreferences()} + {this.renderPrefButtons()} + {this.props.entry.fingerprint && this.renderVerifyButtons()} + {this.renderInstances()} +
+
+ + } +} + +export default withRouter(Client) diff --git a/maubot-src/maubot/management/frontend/src/pages/dashboard/Home.js b/maubot-src/maubot/management/frontend/src/pages/dashboard/Home.js new file mode 100644 index 0000000..e78354f --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/pages/dashboard/Home.js @@ -0,0 +1,30 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React, { Component } from "react" + +class Home extends Component { + render() { + return <> +
+ See sidebar to get started +
+
+
+ + } +} + +export default Home diff --git a/maubot-src/maubot/management/frontend/src/pages/dashboard/Instance.js b/maubot-src/maubot/management/frontend/src/pages/dashboard/Instance.js new file mode 100644 index 0000000..049c5e5 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/pages/dashboard/Instance.js @@ -0,0 +1,233 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React from "react" +import { Link, NavLink, Route, Switch, withRouter } from "react-router-dom" +import AceEditor from "react-ace" +import "ace-builds/src-noconflict/mode-yaml" +import "ace-builds/src-noconflict/theme-github" +import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg" +import { ReactComponent as NoAvatarIcon } from "../../res/bot.svg" +import PrefTable, { PrefInput, PrefSelect, PrefSwitch } from "../../components/PreferenceTable" +import api from "../../api" +import Spinner from "../../components/Spinner" +import BaseMainView from "./BaseMainView" +import InstanceDatabase from "./InstanceDatabase" + +const InstanceListEntry = ({ entry }) => ( + + {entry.id} + + +) + +class Instance extends BaseMainView { + static ListEntry = InstanceListEntry + + constructor(props) { + super(props) + this.deleteFunc = api.deleteInstance + this.updateClientOptions() + } + + get entryKeys() { + return ["id", "primary_user", "enabled", "started", "type", "config", "database_engine"] + } + + get initialState() { + return { + id: "", + primary_user: "", + enabled: true, + started: true, + type: "", + config: "", + database_engine: "", + + saving: false, + deleting: false, + error: "", + } + } + + get instanceInState() { + const instance = Object.assign({}, this.state) + delete instance.saving + delete instance.deleting + delete instance.error + return instance + } + + componentDidUpdate(prevProps) { + this.updateClientOptions() + } + + getAvatarMXC(client) { + return client.avatar_url === "disable" ? client.remote_avatar_url : client.avatar_url + } + + getAvatarURL(client) { + return api.getAvatarURL({ + id: client.id, + avatar_url: this.getAvatarMXC(client), + }) + } + + clientSelectEntry = client => client && { + id: client.id, + value: client.id, + label: ( +
+ {this.getAvatarMXC(client) + ? + : } + { + (client.displayname === "disable" + ? client.remote_displayname + : client.displayname + ) || client.id + } +
+ ), + } + + updateClientOptions() { + this.clientOptions = Object.values(this.props.ctx.clients).map(this.clientSelectEntry) + } + + save = async () => { + this.setState({ saving: true }) + const resp = await api.putInstance(this.instanceInState, this.props.entry + ? this.props.entry.id : undefined) + if (resp.id) { + if (this.isNew) { + this.props.history.push(`/instance/${resp.id}`) + } else { + if (resp.id !== this.props.entry.id) { + this.props.history.replace(`/instance/${resp.id}`) + } + this.setState({ saving: false, error: "" }) + } + this.props.onChange(resp) + } else { + this.setState({ saving: false, error: resp.error }) + } + } + + get selectedClientEntry() { + return this.state.primary_user + ? this.clientSelectEntry(this.props.ctx.clients[this.state.primary_user]) + : {} + } + + get selectedPluginEntry() { + return { + id: this.state.type, + value: this.state.type, + label: this.state.type, + } + } + + get typeOptions() { + return Object.values(this.props.ctx.plugins).map(plugin => plugin && { + id: plugin.id, + value: plugin.id, + label: plugin.id, + }) + } + + get loading() { + return this.state.deleting || this.state.saving + } + + get isValid() { + return this.state.id && this.state.primary_user && this.state.type + } + + render() { + return + + + + } + + renderDatabase = () => + + renderMain = () =>
+ + + this.setState({ enabled })}/> + this.setState({ started })}/> + {api.getFeatures().client ? ( + this.setState({ primary_user: id })}/> + ) : ( + + )} + {api.getFeatures().plugin ? ( + this.setState({ type: id })}/> + ) : ( + + )} + + {!this.isNew && Boolean(this.props.entry.base_config) && + this.setState({ config })} + name="config" value={this.state.config} + editorProps={{ + fontSize: "10pt", + $blockScrolling: true, + }}/>} +
+ {!this.isNew && ( + + )} + +
+ {!this.isNew &&
+ {this.props.entry.database && ( + + View database + + )} + {api.getFeatures().log && + } +
} +
{this.state.error}
+
+} + +export default withRouter(Instance) diff --git a/maubot-src/maubot/management/frontend/src/pages/dashboard/InstanceDatabase.js b/maubot-src/maubot/management/frontend/src/pages/dashboard/InstanceDatabase.js new file mode 100644 index 0000000..2f3f525 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/pages/dashboard/InstanceDatabase.js @@ -0,0 +1,332 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React, { Component } from "react" +import { NavLink, Link, withRouter } from "react-router-dom" +import { ContextMenu, ContextMenuTrigger, MenuItem } from "react-contextmenu" +import { ReactComponent as ChevronLeft } from "../../res/chevron-left.svg" +import { ReactComponent as OrderDesc } from "../../res/sort-down.svg" +import { ReactComponent as OrderAsc } from "../../res/sort-up.svg" +import api from "../../api" +import Spinner from "../../components/Spinner" + +// eslint-disable-next-line no-extend-native +Map.prototype.map = function(func) { + const res = [] + for (const [key, value] of this) { + res.push(func(value, key, this)) + } + return res +} + +class InstanceDatabase extends Component { + constructor(props) { + super(props) + this.state = { + tables: null, + header: null, + content: null, + query: "", + selectedTable: null, + + error: null, + + prevQuery: null, + statusMsg: null, + rowCount: null, + insertedPrimaryKey: null, + } + this.order = new Map() + } + + async componentDidMount() { + const tables = new Map(Object.entries(await api.getInstanceDatabase(this.props.instanceID))) + for (const [name, table] of tables) { + table.name = name + table.columns = new Map(Object.entries(table.columns)) + for (const [columnName, column] of table.columns) { + column.name = columnName + column.sort = null + } + } + this.setState({ tables }) + this.checkLocationTable() + } + + componentDidUpdate(prevProps) { + if (this.props.location !== prevProps.location) { + this.order = new Map() + this.setState({ header: null, content: null }) + this.checkLocationTable() + } + } + + checkLocationTable() { + const prefix = `/instance/${this.props.instanceID}/database/` + if (this.props.location.pathname.startsWith(prefix)) { + const table = this.props.location.pathname.substr(prefix.length) + this.setState({ selectedTable: table }) + this.buildSQLQuery(table) + } + } + + getSortQueryParams(table) { + const order = [] + for (const [column, sort] of Array.from(this.order.entries()).reverse()) { + order.push(`order=${column}:${sort}`) + } + return order + } + + buildSQLQuery(table = this.state.selectedTable, resetContent = true) { + let query = `SELECT * FROM "${table}"` + + if (this.order.size > 0) { + const order = Array.from(this.order.entries()).reverse() + .map(([column, sort]) => `${column} ${sort}`) + query += ` ORDER BY ${order.join(", ")}` + } + + query += " LIMIT 100" + this.setState({ query }, () => this.reloadContent(resetContent)) + } + + reloadContent = async (resetContent = true) => { + this.setState({ loading: true }) + const res = await api.queryInstanceDatabase(this.props.instanceID, this.state.query) + this.setState({ loading: false }) + if (resetContent) { + this.setState({ + prevQuery: null, + rowCount: null, + insertedPrimaryKey: null, + statusMsg: null, + error: null, + }) + } + if (!res.ok) { + this.setState({ + error: res.error, + }) + } else if (res.rows) { + this.setState({ + header: res.columns, + content: res.rows, + }) + } else { + this.setState({ + prevQuery: res.query, + rowCount: res.rowcount, + insertedPrimaryKey: res.inserted_primary_key, + statusMsg: res.status_msg, + }) + this.buildSQLQuery(this.state.selectedTable, false) + } + } + + toggleSort(column) { + const oldSort = this.order.get(column) || "auto" + this.order.delete(column) + switch (oldSort) { + case "auto": + this.order.set(column, "DESC") + break + case "DESC": + this.order.set(column, "ASC") + break + case "ASC": + default: + break + } + this.buildSQLQuery() + } + + getSortIcon(column) { + switch (this.order.get(column)) { + case "DESC": + return + case "ASC": + return + default: + return null + } + } + + getColumnInfo(columnName) { + const table = this.state.tables.get(this.state.selectedTable) + if (!table) { + return null + } + const column = table.columns.get(columnName) + if (!column) { + return null + } + if (column.primary) { + return  (pk) + } else if (column.unique) { + return  (u) + } + return null + } + + getColumnType(columnName) { + const table = this.state.tables.get(this.state.selectedTable) + if (!table) { + return null + } + const column = table.columns.get(columnName) + if (!column) { + return null + } + return column.type + } + + deleteRow = async (_, data) => { + const values = this.state.content[data.row] + const keys = this.state.header + const condition = [] + for (const [index, key] of Object.entries(keys)) { + const val = values[index] + condition.push(`${key}='${this.sqlEscape(val.toString())}'`) + } + const query = `DELETE FROM "${this.state.selectedTable}" WHERE ${condition.join(" AND ")}` + const res = await api.queryInstanceDatabase(this.props.instanceID, query) + this.setState({ + prevQuery: `DELETE FROM "${this.state.selectedTable}" ...`, + rowCount: res.rowcount, + }) + await this.reloadContent(false) + } + + editCell = async (evt, data) => { + console.log("Edit", data) + } + + collectContextMeta = props => ({ + row: props.row, + col: props.col, + }) + + // eslint-disable-next-line no-control-regex + sqlEscape = str => str.replace(/[\0\x08\x09\x1a\n\r"'\\%]/g, char => { + switch (char) { + case "\0": + return "\\0" + case "\x08": + return "\\b" + case "\x09": + return "\\t" + case "\x1a": + return "\\z" + case "\n": + return "\\n" + case "\r": + return "\\r" + case "\"": + case "'": + case "\\": + case "%": + return "\\" + char + default: + return char + } + }) + + renderTable = () =>
+ {this.state.header ? <> + + + + {this.state.header.map(column => ( + + ))} + + + + {this.state.content.map((row, rowIndex) => ( + + {row.map((cell, colIndex) => ( + + {cell} + + ))} + + ))} + +
+ this.toggleSort(column)} + title={this.getColumnType(column)}> + {column} + {this.getColumnInfo(column)} + {this.getSortIcon(column)} + +
+ + Delete row + Edit cell + + : this.state.loading ? : null} +
+ + renderContent() { + return <> +
+ {this.state.tables.map((_, tbl) => ( + + {tbl} + + ))} +
+
+ this.setState({ query: evt.target.value })}/> + +
+ {this.state.error &&
+ {this.state.error} +
} + {this.state.prevQuery &&
+

+ Executed {this.state.prevQuery} - { + this.state.statusMsg + || <>affected {this.state.rowCount} rows. + } +

+ {this.state.insertedPrimaryKey &&

+ Inserted primary key: {this.state.insertedPrimaryKey} +

} +
} + {this.renderTable()} + + } + + render() { + return
+
+ + + Back + +
+ {this.state.tables + ? this.renderContent() + : } +
+ } +} + +export default withRouter(InstanceDatabase) diff --git a/maubot-src/maubot/management/frontend/src/pages/dashboard/Log.js b/maubot-src/maubot/management/frontend/src/pages/dashboard/Log.js new file mode 100644 index 0000000..b80e6ad --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/pages/dashboard/Log.js @@ -0,0 +1,191 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React, { PureComponent } from "react" +import { Link } from "react-router-dom" +import { JSONTree } from "react-json-tree" +import api from "../../api" +import Modal from "./Modal" + +class LogEntry extends PureComponent { + static contextType = Modal.Context + + renderName() { + const line = this.props.line + if (line.nameLink) { + const modal = this.context + return <> + + {line.name} + + {line.nameSuffix} + + } + return line.name + } + + renderContent() { + if (this.props.line.matrix_http_request) { + const req = this.props.line.matrix_http_request + + return <> + {req.method} {req.url || req.path} +
+ {Object.entries(req.content || {}).length > 0 + && } +
+ + } + return this.props.line.msg + } + + onClickOpen(path, line) { + return () => { + if (api.debugOpenFileEnabled()) { + api.debugOpenFile(path, line) + } + return false + } + } + + renderTimeTitle() { + return this.props.line.time.toDateString() + } + + renderTime() { + return + {this.props.line.time.toLocaleTimeString("en-GB")} + + } + + renderLevelName() { + return + {this.props.line.levelname} + + } + + get unfocused() { + return this.props.focus && this.props.line.name !== this.props.focus + ? "unfocused" + : "" + } + + renderRow(content) { + return ( +
+ {this.renderTime()} + {this.renderLevelName()} + {this.renderName()} + {content} +
+ ) + } + + renderExceptionInfo() { + if (!api.debugOpenFileEnabled()) { + return this.props.line.exc_info + } + const fileLinks = [] + let str = this.props.line.exc_info.replace( + /File "(.+)", line ([0-9]+), in (.+)/g, + (_, file, line, method) => { + fileLinks.push( + + File "{file}", line {line}, in {method} + , + ) + return "||EDGE||" + }) + fileLinks.reverse() + + const result = [] + let key = 0 + for (const part of str.split("||EDGE||")) { + result.push( + {part} + {fileLinks.pop()} + ) + } + return result + } + + render() { + return <> + {this.renderRow(this.renderContent())} + {this.props.line.exc_info && this.renderRow(this.renderExceptionInfo())} + + } +} + +class Log extends PureComponent { + constructor(props) { + super(props) + + this.linesRef = React.createRef() + this.linesBottomRef = React.createRef() + } + + getSnapshotBeforeUpdate() { + if (this.linesRef.current && this.linesBottomRef.current) { + return Log.isVisible(this.linesRef.current, this.linesBottomRef.current) + } + return false + } + + + componentDidUpdate(_1, _2, wasVisible) { + if (wasVisible) { + Log.scrollParentToChild(this.linesRef.current, this.linesBottomRef.current) + } + } + + componentDidMount() { + if (this.linesRef.current && this.linesBottomRef.current) { + Log.scrollParentToChild(this.linesRef.current, this.linesBottomRef.current) + } + } + + static scrollParentToChild(parent, child) { + const parentRect = parent.getBoundingClientRect() + const childRect = child.getBoundingClientRect() + + if (!Log.isVisible(parent, child)) { + parent.scrollBy({ top: (childRect.top + parent.scrollTop) - parentRect.top }) + } + } + + static isVisible(parent, child) { + const parentRect = parent.getBoundingClientRect() + const childRect = child.getBoundingClientRect() + return (childRect.top >= parentRect.top) + && (childRect.top <= parentRect.top + parent.clientHeight) + } + + render() { + return ( +
+
+ {this.props.lines.map(data => )} +
+
+
+ ) + } +} + +export default Log diff --git a/maubot-src/maubot/management/frontend/src/pages/dashboard/Modal.js b/maubot-src/maubot/management/frontend/src/pages/dashboard/Modal.js new file mode 100644 index 0000000..924079f --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/pages/dashboard/Modal.js @@ -0,0 +1,52 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React, { Component, createContext } from "react" + +const rem = 16 + +class Modal extends Component { + static Context = createContext(null) + + constructor(props) { + super(props) + this.state = { + open: false, + } + this.wrapper = { clientWidth: 9001 } + } + + open = () => this.setState({ open: true }) + close = () => this.setState({ open: false }) + isOpen = () => this.state.open + + render() { + return this.state.open && ( +
this.wrapper = ref} + onClick={() => this.wrapper.clientWidth > 45 * rem && this.close()}> +
evt.stopPropagation()}> + +
+ + {this.props.children} + +
+
+
+ ) + } +} + +export default Modal diff --git a/maubot-src/maubot/management/frontend/src/pages/dashboard/Plugin.js b/maubot-src/maubot/management/frontend/src/pages/dashboard/Plugin.js new file mode 100644 index 0000000..923a397 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/pages/dashboard/Plugin.js @@ -0,0 +1,104 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React from "react" +import { NavLink, withRouter } from "react-router-dom" +import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg" +import { ReactComponent as UploadButton } from "../../res/upload.svg" +import PrefTable, { PrefInput } from "../../components/PreferenceTable" +import Spinner from "../../components/Spinner" +import api from "../../api" +import BaseMainView from "./BaseMainView" + +const PluginListEntry = ({ entry }) => ( + + {entry.id} + + +) + + +class Plugin extends BaseMainView { + static ListEntry = PluginListEntry + + constructor(props) { + super(props) + this.deleteFunc = api.deletePlugin + } + + get initialState() { + return { + id: "", + version: "", + + instances: [], + + uploading: false, + deleting: false, + error: "", + } + } + + upload = async event => { + const file = event.target.files[0] + this.setState({ + uploadingAvatar: true, + }) + const data = await this.readFile(file) + const resp = await api.uploadPlugin(data, this.state.id) + if (resp.id) { + if (this.isNew) { + this.props.history.push(`/plugin/${resp.id}`) + } else { + this.setState({ saving: false, error: "" }) + } + this.props.onChange(resp) + } else { + this.setState({ saving: false, error: resp.error }) + } + } + + render() { + return
+ {!this.isNew && + + + } + {api.getFeatures().plugin_upload && +
+ + evt.target.parentElement.classList.add("drag")} + onDragLeave={evt => evt.target.parentElement.classList.remove("drag")}/> + {this.state.uploading && } +
} + {!this.isNew &&
+ +
} + {this.renderLogButton("loader.zip")} +
{this.state.error}
+ {this.renderInstances()} +
+ } +} + +export default withRouter(Plugin) diff --git a/maubot-src/maubot/management/frontend/src/pages/dashboard/index.js b/maubot-src/maubot/management/frontend/src/pages/dashboard/index.js new file mode 100644 index 0000000..9655a00 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/pages/dashboard/index.js @@ -0,0 +1,272 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React, { Component } from "react" +import { Route, Switch, Link, withRouter } from "react-router-dom" +import api from "../../api" +import { ReactComponent as Plus } from "../../res/plus.svg" +import Instance from "./Instance" +import Client from "./Client" +import Plugin from "./Plugin" +import Home from "./Home" +import Log from "./Log" +import Modal from "./Modal" + +class Dashboard extends Component { + constructor(props) { + super(props) + this.state = { + instances: {}, + clients: {}, + plugins: {}, + homeserversByName: {}, + homeserversByURL: {}, + sidebarOpen: false, + modalOpen: false, + logFocus: null, + logLines: [], + } + this.logModal = { + open: () => undefined, + isOpen: () => false, + } + window.maubot = this + } + + componentDidUpdate(prevProps) { + if (this.props.location !== prevProps.location) { + this.setState({ sidebarOpen: false }) + } + } + + async componentDidMount() { + const [instanceList, clientList, pluginList, homeservers] = await Promise.all([ + api.getInstances(), api.getClients(), api.getPlugins(), api.getClientAuthServers(), + api.updateDebugOpenFileEnabled()]) + const instances = {} + if (api.getFeatures().instance) { + for (const instance of instanceList) { + instances[instance.id] = instance + } + } + const clients = {} + if (api.getFeatures().client) { + for (const client of clientList) { + clients[client.id] = client + } + } + const plugins = {} + if (api.getFeatures().plugin) { + for (const plugin of pluginList) { + plugins[plugin.id] = plugin + } + } + const homeserversByName = homeservers + const homeserversByURL = {} + if (api.getFeatures().client_auth) { + for (const [key, value] of Object.entries(homeservers)) { + homeserversByURL[value] = key + } + } + this.setState({ instances, clients, plugins, homeserversByName, homeserversByURL }) + + await this.enableLogs() + } + + async enableLogs() { + if (!api.getFeatures().log) { + return + } + + const logs = await api.openLogSocket() + + const processEntry = (entry) => { + entry.time = new Date(entry.time) + entry.nameSuffix = "" + if (entry.name.startsWith("maubot.client.")) { + if (entry.name.endsWith(".crypto")) { + entry.name = entry.name.slice(0, -".crypto".length) + entry.nameSuffix = "/crypto" + } + entry.name = `client/${entry.name.slice("maubot.client.".length)}` + entry.nameLink = `/${entry.name}` + } else if (entry.name.startsWith("maubot.instance.")) { + entry.name = `instance/${entry.name.slice("maubot.instance.".length)}` + entry.nameLink = `/${entry.name}` + } else if (entry.name.startsWith("maubot.instance_db.")) { + entry.nameSuffix = "/db" + entry.name = `instance/${entry.name.slice("maubot.instance_db.".length)}` + entry.nameLink = `/${entry.name}` + } + } + + logs.onHistory = history => { + for (const data of history) { + processEntry(data) + } + this.setState({ + logLines: history, + }) + } + logs.onLog = data => { + processEntry(data) + this.setState({ + logLines: this.state.logLines.concat(data), + }) + } + } + + renderList(field, type) { + return this.state[field] && Object.values(this.state[field]).map(entry => + React.createElement(type, { key: entry.id, entry })) + } + + delete(stateField, id) { + const data = Object.assign({}, this.state[stateField]) + delete data[id] + this.setState({ [stateField]: data }) + } + + add(stateField, entry, oldID = undefined) { + const data = Object.assign({}, this.state[stateField]) + if (oldID && oldID !== entry.id) { + delete data[oldID] + } + data[entry.id] = entry + this.setState({ [stateField]: data }) + } + + renderView(field, type, id) { + const typeName = field.slice(0, -1) + if (!api.getFeatures()[typeName]) { + return this.renderDisabled(typeName) + } + const entry = this.state[field][id] + if (!entry) { + return this.renderNotFound(typeName) + } + return React.createElement(type, { + entry, + onDelete: () => this.delete(field, id), + onChange: newEntry => this.add(field, newEntry, id), + openLog: this.openLog, + ctx: this.state, + }) + } + + openLog = filter => { + this.setState({ + logFocus: typeof filter === "string" ? filter : null, + }) + this.logModal.open() + } + + renderNotFound = (thing = "path") => ( +
+ Oops! I'm afraid that {thing} couldn't be found. +
+ ) + + renderDisabled = (thing = "path") => ( +
+ The {thing} API has been disabled in the maubot config. +
+ ) + + renderMain() { + return
+ + + Maubot Manager + +
+ {localStorage.username} +
+ + + +
this.setState({ sidebarOpen: !this.state.sidebarOpen })}> +
+ +
+
+ +
+ + }/> + + this.add("instances", newEntry)} + entry={{}} ctx={this.state}/>}/> + + this.add("clients", newEntry)} + entry={{}} ctx={this.state}/>}/> + + this.add("plugins", newEntry)} + entry={{}} ctx={this.state}/>}/> + + this.renderView("instances", Instance, match.params.id)}/> + + this.renderView("clients", Client, match.params.id)}/> + + this.renderView("plugins", Plugin, match.params.id)}/> + this.renderNotFound()}/> + +
+
+ } + + renderModal() { + return this.logModal = ref}> + + + } + + render() { + return <> + {this.renderMain()} + {this.renderModal()} + + } +} + +export default withRouter(Dashboard) diff --git a/maubot-src/maubot/management/frontend/src/res/bot.svg b/maubot-src/maubot/management/frontend/src/res/bot.svg new file mode 100644 index 0000000..3c690fb --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/res/bot.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/maubot-src/maubot/management/frontend/src/res/chevron-left.svg b/maubot-src/maubot/management/frontend/src/res/chevron-left.svg new file mode 100644 index 0000000..714c0b1 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/res/chevron-left.svg @@ -0,0 +1 @@ + diff --git a/maubot-src/maubot/management/frontend/src/res/chevron-right.svg b/maubot-src/maubot/management/frontend/src/res/chevron-right.svg new file mode 100644 index 0000000..265effd --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/res/chevron-right.svg @@ -0,0 +1 @@ + diff --git a/maubot-src/maubot/management/frontend/src/res/plus.svg b/maubot-src/maubot/management/frontend/src/res/plus.svg new file mode 100644 index 0000000..8030204 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/res/plus.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/maubot-src/maubot/management/frontend/src/res/sort-down.svg b/maubot-src/maubot/management/frontend/src/res/sort-down.svg new file mode 100644 index 0000000..bb5413b --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/res/sort-down.svg @@ -0,0 +1 @@ + diff --git a/maubot-src/maubot/management/frontend/src/res/sort-up.svg b/maubot-src/maubot/management/frontend/src/res/sort-up.svg new file mode 100644 index 0000000..84ad364 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/res/sort-up.svg @@ -0,0 +1 @@ + diff --git a/maubot-src/maubot/management/frontend/src/res/upload.svg b/maubot-src/maubot/management/frontend/src/res/upload.svg new file mode 100644 index 0000000..f1deea6 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/res/upload.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/maubot-src/maubot/management/frontend/src/setupProxy.js b/maubot-src/maubot/management/frontend/src/setupProxy.js new file mode 100644 index 0000000..fedb271 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/setupProxy.js @@ -0,0 +1,6 @@ +const proxy = require("http-proxy-middleware") + +module.exports = function(app) { + app.use(proxy("/_matrix/maubot/v1", { target: process.env.PROXY || "http://localhost:29316" })) + app.use(proxy("/_matrix/maubot/v1/logs", { target: process.env.PROXY || "http://localhost:29316", ws: true })) +} diff --git a/maubot-src/maubot/management/frontend/src/style/base/body.sass b/maubot-src/maubot/management/frontend/src/style/base/body.sass new file mode 100644 index 0000000..a3c6fb3 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/style/base/body.sass @@ -0,0 +1,39 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +body + font-family: $font-stack + margin: 0 + padding: 0 + font-size: 16px + +#root + position: fixed + top: 0 + bottom: 0 + right: 0 + left: 0 + +.maubot-wrapper + position: absolute + top: 0 + bottom: 0 + left: 0 + right: 0 + background-color: $background-dark + +.maubot-loading + margin-top: 10rem + width: 10rem diff --git a/maubot-src/maubot/management/frontend/src/style/base/elements.sass b/maubot-src/maubot/management/frontend/src/style/base/elements.sass new file mode 100644 index 0000000..5de0577 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/style/base/elements.sass @@ -0,0 +1,120 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +=button($width: null, $height: null, $padding: .375rem 1rem) + font-family: $font-stack + padding: $padding + width: $width + height: $height + background-color: $background + border: none + border-radius: .25rem + color: $text-color + box-sizing: border-box + font-size: 1rem + + &.disabled-bg + background-color: $background-dark + + &:not(:disabled) + cursor: pointer + + &:hover + background-color: darken($background, 10%) + +=link-button() + display: inline-block + text-align: center + text-decoration: none + +=main-color-button() + background-color: $primary + color: $inverted-text-color + &:hover:not(:disabled) + background-color: $primary-dark + + &:disabled.disabled-bg + background-color: $background-dark !important + color: $text-color + +.button + +button + + &.main-color + +main-color-button + +=button-group() + width: 100% + display: flex + > button, > .button + flex: 1 + + &:first-child + margin-right: .5rem + + &:last-child + margin-left: .5rem + + &:first-child:last-child + margin: 0 + +=vertical-button-group() + display: flex + flex-direction: column + > button, > .button + flex: 1 + border-radius: 0 + + &:first-of-type + border-radius: .25rem .25rem 0 0 + + &:last-of-type + border-radius: 0 0 .25rem .25rem + + &:first-of-type:last-of-type + border-radius: .25rem + +=input($width: null, $height: null, $vertical-padding: .375rem, $horizontal-padding: 1rem, $font-size: 1rem) + font-family: $font-stack + border: 1px solid $border-color + background-color: $background + color: $text-color + width: $width + height: $height + box-sizing: border-box + border-radius: .25rem + padding: $vertical-padding $horizontal-padding + font-size: $font-size + resize: vertical + + &:hover, &:focus + border-color: $primary + + &:focus + border-width: 2px + padding: calc(#{$vertical-padding} - 1px) calc(#{$horizontal-padding} - 1px) + +.input, .textarea + +input + +input + font-family: $font-stack + +=notification($border: $error-dark, $background: transparentize($error-light, 0.5)) + padding: 1rem + border-radius: .25rem + border: 2px solid $border + background-color: $background diff --git a/maubot-src/maubot/management/frontend/src/style/base/fonts.sass b/maubot-src/maubot/management/frontend/src/style/base/fonts.sass new file mode 100644 index 0000000..89a09a9 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/style/base/fonts.sass @@ -0,0 +1,30 @@ +@font-face + font-family: 'Raleway' + font-style: normal + font-weight: 300 + src: local('Raleway Light'), local('Raleway-Light'), url('../../fonts/raleway-light.woff2') format('woff2') + +@font-face + font-family: 'Raleway' + font-style: normal + font-weight: 400 + src: local('Raleway'), local('Raleway-Regular'), url('../../fonts/raleway-regular.woff2') format('woff2') + +@font-face + font-family: 'Raleway' + font-style: normal + font-weight: 700 + src: local('Raleway Bold'), local('Raleway-Bold'), url('../../fonts/raleway-bold.woff2') format('woff2') + + +@font-face + font-family: 'Fira Code' + src: url('../../fonts/firacode-regular.woff2') format('woff2') + font-weight: 400 + font-style: normal + +@font-face + font-family: 'Fira Code' + src: url('../../fonts/firacode-bold.woff2') format('woff2') + font-weight: 700 + font-style: normal diff --git a/maubot-src/maubot/management/frontend/src/style/base/vars.sass b/maubot-src/maubot/management/frontend/src/style/base/vars.sass new file mode 100644 index 0000000..36c8028 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/style/base/vars.sass @@ -0,0 +1,33 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +$primary: #00C853 +$primary-dark: #009624 +$primary-light: #5EFC82 +$secondary: #00B8D4 +$secondary-dark: #0088A3 +$secondary-light: #62EBFF +$error: #B71C1C +$error-dark: #7F0000 +$error-light: #F05545 +$warning: orange + +$border-color: #DDD +$text-color: #212121 +$background: #FAFAFA +$background-dark: #E7E7E7 +$inverted-text-color: $background +$font-stack: Raleway, sans-serif diff --git a/maubot-src/maubot/management/frontend/src/style/index.sass b/maubot-src/maubot/management/frontend/src/style/index.sass new file mode 100644 index 0000000..05dd369 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/style/index.sass @@ -0,0 +1,33 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +@import lib/spinner +@import lib/contextmenu + +@import base/fonts +@import base/vars +@import base/body +@import base/elements + +@import lib/preferencetable +@import lib/switch + +@import pages/mixins/upload-container +@import pages/mixins/instancelist + +@import pages/login +@import pages/dashboard +@import pages/modal +@import pages/log diff --git a/maubot-src/maubot/management/frontend/src/style/lib/contextmenu.scss b/maubot-src/maubot/management/frontend/src/style/lib/contextmenu.scss new file mode 100644 index 0000000..5575fb4 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/style/lib/contextmenu.scss @@ -0,0 +1,80 @@ +.react-contextmenu { + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, .15); + border-radius: .25rem; + color: #373a3c; + font-size: 16px; + margin: 2px 0 0; + min-width: 160px; + outline: none; + opacity: 0; + padding: 5px 0; + pointer-events: none; + text-align: left; + transition: opacity 250ms ease !important; +} + +.react-contextmenu.react-contextmenu--visible { + opacity: 1; + pointer-events: auto; + z-index: 9999; +} + +.react-contextmenu-item { + background: 0 0; + border: 0; + color: #373a3c; + cursor: pointer; + font-weight: 400; + line-height: 1.5; + padding: 3px 20px; + text-align: inherit; + white-space: nowrap; +} + +.react-contextmenu-item.react-contextmenu-item--active, +.react-contextmenu-item.react-contextmenu-item--selected { + color: #fff; + background-color: #20a0ff; + border-color: #20a0ff; + text-decoration: none; +} + +.react-contextmenu-item.react-contextmenu-item--disabled, +.react-contextmenu-item.react-contextmenu-item--disabled:hover { + background-color: transparent; + border-color: rgba(0, 0, 0, .15); + color: #878a8c; +} + +.react-contextmenu-item--divider { + border-bottom: 1px solid rgba(0, 0, 0, .15); + cursor: inherit; + margin-bottom: 3px; + padding: 2px 0; +} + +.react-contextmenu-item--divider:hover { + background-color: transparent; + border-color: rgba(0, 0, 0, .15); +} + +.react-contextmenu-item.react-contextmenu-submenu { + padding: 0; +} + +.react-contextmenu-item.react-contextmenu-submenu > .react-contextmenu-item { +} + +.react-contextmenu-item.react-contextmenu-submenu > .react-contextmenu-item:after { + content: "▶"; + display: inline-block; + position: absolute; + right: 7px; +} + +.example-multiple-targets::after { + content: attr(data-count); + display: block; +} diff --git a/maubot-src/maubot/management/frontend/src/style/lib/preferencetable.sass b/maubot-src/maubot/management/frontend/src/style/lib/preferencetable.sass new file mode 100644 index 0000000..5c671c9 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/style/lib/preferencetable.sass @@ -0,0 +1,85 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +.preference-table + display: flex + + width: 100% + + flex-wrap: wrap + + > .entry + display: block + + @media screen and (max-width: 55rem) + width: calc(100% - 1rem) + width: calc(50% - 1rem) + margin: .5rem + + &.full-width + width: 100% + + &.changed > label + font-weight: bold + &:after + content: "*" + + > label, > .value + display: block + width: 100% + + > label + font-size: 0.875rem + padding-bottom: .25rem + font-weight: lighter + + > .value + > .switch + width: auto + height: 2rem + + > .select + height: 2.5rem + box-sizing: border-box + + > input + border: none + height: 2rem + width: 100% + color: $text-color + + box-sizing: border-box + + padding: .375rem 0 calc(.375rem + 1px) + background-color: $background + + font-size: 1rem + + border-bottom: 1px solid $background + + &.id:disabled + font-family: "Fira Code", monospace + font-weight: bold + + &:not(:disabled) + border-bottom: 1px dotted $primary + + &:hover + border-bottom: 1px solid $primary + + &:focus + border-bottom: 2px solid $primary + padding-bottom: .375rem diff --git a/maubot-src/maubot/management/frontend/src/style/lib/spinner.sass b/maubot-src/maubot/management/frontend/src/style/lib/spinner.sass new file mode 100644 index 0000000..d164586 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/style/lib/spinner.sass @@ -0,0 +1,65 @@ +$green: #008744 +$blue: #0057e7 +$red: #d62d20 +$yellow: #ffa700 + +.spinner + position: relative + margin: 0 auto + width: 5rem + + &:before + content: "" + display: block + padding-top: 100% + + svg + animation: rotate 2s linear infinite + height: 100% + transform-origin: center center + width: 100% + position: absolute + top: 0 + bottom: 0 + left: 0 + right: 0 + margin: auto + + circle + stroke-dasharray: 1, 200 + stroke-dashoffset: 0 + animation: dash 1.5s ease-in-out infinite, color 6s ease-in-out infinite + stroke-linecap: round + +=white-spinner() + circle + stroke: white !important + +=thick-spinner($thickness: 5) + svg > circle + stroke-width: $thickness + +@keyframes rotate + 100% + transform: rotate(360deg) + +@keyframes dash + 0% + stroke-dasharray: 1, 200 + stroke-dashoffset: 0 + 50% + stroke-dasharray: 89, 200 + stroke-dashoffset: -35px + 100% + stroke-dasharray: 89, 200 + stroke-dashoffset: -124px + +@keyframes color + 100%, 0% + stroke: $red + 40% + stroke: $blue + 66% + stroke: $green + 80%, 90% + stroke: $yellow diff --git a/maubot-src/maubot/management/frontend/src/style/lib/switch.sass b/maubot-src/maubot/management/frontend/src/style/lib/switch.sass new file mode 100644 index 0000000..9a47ce9 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/style/lib/switch.sass @@ -0,0 +1,77 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +.switch + display: flex + + width: 100% + height: 2rem + + cursor: pointer + + border: 1px solid $error-light + border-radius: .25rem + background-color: $background + + box-sizing: border-box + + > .box + display: flex + box-sizing: border-box + width: 50% + height: 100% + + transition: .5s + text-align: center + + border-radius: .15rem 0 0 .15rem + background-color: $error-light + color: $inverted-text-color + + align-items: center + + > .text + box-sizing: border-box + width: 100% + + text-align: center + vertical-align: middle + + color: $inverted-text-color + font-size: 1rem + + user-select: none + + .on + display: none + + .off + display: inline + + + &[data-active=true] + border: 1px solid $primary + > .box + background-color: $primary + transform: translateX(100%) + + border-radius: 0 .15rem .15rem 0 + + .on + display: inline + + .off + display: none diff --git a/maubot-src/maubot/management/frontend/src/style/pages/client/avatar.sass b/maubot-src/maubot/management/frontend/src/style/pages/client/avatar.sass new file mode 100644 index 0000000..36856c3 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/style/pages/client/avatar.sass @@ -0,0 +1,62 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +> div.avatar-container + +upload-box + + width: 8rem + height: 8rem + border-radius: 50% + + @media screen and (max-width: 40rem) + margin: 0 auto 1rem + + > img.avatar + position: absolute + display: block + max-width: 8rem + max-height: 8rem + user-select: none + + > svg.upload + visibility: hidden + + width: 6rem + height: 6rem + + > input.file-selector + width: 8rem + height: 8rem + + &:not(.uploading) + &:hover, &.drag + > img.avatar + opacity: .25 + + > svg.upload + visibility: visible + + &.no-avatar + > img.avatar + visibility: hidden + + > svg.upload + visibility: visible + opacity: .5 + + &.uploading + > img.avatar + opacity: .25 diff --git a/maubot-src/maubot/management/frontend/src/style/pages/client/index.sass b/maubot-src/maubot/management/frontend/src/style/pages/client/index.sass new file mode 100644 index 0000000..18db567 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/style/pages/client/index.sass @@ -0,0 +1,48 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +> div.client + display: flex + + > div.sidebar + vertical-align: top + text-align: center + width: 8rem + margin-right: 1rem + + > * + margin-bottom: 1rem + + @import avatar + @import started + + > div.info + vertical-align: top + flex: 1 + + > div.instances + +instancelist + + input.fingerprint + font-family: "Fira Code", monospace + font-size: 0.8em + + @media screen and (max-width: 40rem) + flex-wrap: wrap + + > div.sidebar, > div.info + width: 100% + margin-right: 0 diff --git a/maubot-src/maubot/management/frontend/src/style/pages/client/started.sass b/maubot-src/maubot/management/frontend/src/style/pages/client/started.sass new file mode 100644 index 0000000..1292e86 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/style/pages/client/started.sass @@ -0,0 +1,46 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +> div.started-container + display: inline-flex + + > span.started + display: inline-block + height: 0 + width: 0 + border-radius: 50% + margin: .5rem + + &.true + &.sync_ok + background-color: $primary + box-shadow: 0 0 .75rem .75rem $primary + + &.sync_error + background-color: $warning + box-shadow: 0 0 .75rem .75rem $warning + + &.false + background-color: $error-light + box-shadow: 0 0 .75rem .75rem $error-light + + &.disabled + background-color: $border-color + box-shadow: 0 0 .75rem .75rem $border-color + + > span.text + display: inline-block + margin-left: 1rem diff --git a/maubot-src/maubot/management/frontend/src/style/pages/dashboard-grid.scss b/maubot-src/maubot/management/frontend/src/style/pages/dashboard-grid.scss new file mode 100644 index 0000000..5f12093 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/style/pages/dashboard-grid.scss @@ -0,0 +1,19 @@ +.dashboard { + grid-template: + [row1-start] "title main" 3.5rem [row1-end] + [row2-start] "user main" 2.5rem [row2-end] + [row3-start] "sidebar main" auto [row3-end] + / 15rem auto; +} + + +@media screen and (max-width: 35rem) { + .dashboard { + grid-template: + [row1-start] "title topbar" 3.5rem [row1-end] + [row2-start] "user main" 2.5rem [row2-end] + [row3-start] "sidebar main" auto [row3-end] + / 15rem 100%; + overflow-x: hidden; + } +} diff --git a/maubot-src/maubot/management/frontend/src/style/pages/dashboard.sass b/maubot-src/maubot/management/frontend/src/style/pages/dashboard.sass new file mode 100644 index 0000000..9887fa1 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/style/pages/dashboard.sass @@ -0,0 +1,129 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +@import "dashboard-grid" + +.dashboard + display: grid + height: 100% + max-width: 60rem + margin: auto + box-shadow: 0 .5rem .5rem rgba(0, 0, 0, 0.5) + background-color: $background + + > a.title + grid-area: title + background-color: white + display: flex + align-items: center + justify-content: center + + font-size: 1.35rem + font-weight: bold + + color: $text-color + text-decoration: none + + > img + max-width: 2rem + margin-right: .5rem + + > div.user + grid-area: user + background-color: white + border-bottom: 1px solid $border-color + display: flex + align-items: center + justify-content: center + span + display: flex + align-items: center + justify-content: center + background-color: $primary + color: $inverted-text-color + margin: .375rem .5rem + width: 100% + height: calc(100% - .375rem) + box-sizing: border-box + border-radius: .25rem + + @import sidebar + @import topbar + + @media screen and (max-width: 35rem) + &:not(.sidebar-open) > * + transform: translateX(-15rem) + > * + transition: transform 0.4s + + > main.view + grid-area: main + border-left: 1px solid $border-color + + overflow-y: auto + + @import client/index + @import instance + @import instance-database + @import plugin + + > div + margin: 2rem 4rem + + @media screen and (max-width: 50rem) + margin: 1rem + + > div.not-found, > div.home + text-align: center + margin-top: 5rem + font-size: 1.5rem + + div.buttons + +button-group + display: flex + margin: 1rem .5rem + width: calc(100% - 1rem) + + button.open-log, a.open-database + +button + +main-color-button + + a.open-database + +link-button + + div.error + +notification($error) + margin: 1rem .5rem + + &:empty + display: none + + button.delete + background-color: $error-light !important + + &:hover + background-color: $error !important + + button.save, button.clearcache, button.delete, button.verify + +button + +main-color-button + width: 100% + height: 2.5rem + padding: 0 + + > .spinner + +thick-spinner + +white-spinner + width: 2rem diff --git a/maubot-src/maubot/management/frontend/src/style/pages/instance-database.sass b/maubot-src/maubot/management/frontend/src/style/pages/instance-database.sass new file mode 100644 index 0000000..6477aa4 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/style/pages/instance-database.sass @@ -0,0 +1,101 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +> div.instance-database + margin: 0 + + > div.topbar + background-color: $primary-light + + display: flex + justify-items: center + align-items: center + + > a + display: flex + justify-items: center + align-items: center + text-decoration: none + user-select: none + + height: 2.5rem + width: 100% + + > *:not(.topbar) + margin: 2rem 4rem + + @media screen and (max-width: 50rem) + margin: 1rem + + > div.tables + display: flex + flex-wrap: wrap + + > a + +link-button + color: black + flex: 1 + + border-bottom: 2px solid $primary + + padding: .25rem + margin: .25rem + + &:hover + background-color: $primary-light + border-bottom: 2px solid $primary-dark + + &.active + background-color: $primary + + > div.query + display: flex + + > input + +input + font-family: "Fira Code", monospace + flex: 1 + margin-right: .5rem + + > button + +button + +main-color-button + + > div.prev-query + +notification($primary, $primary-light) + + span.query + font-family: "Fira Code", monospace + + p + margin: 0 + + > div.table + overflow-x: auto + overflow-y: hidden + + table + font-family: "Fira Code", monospace + width: 100% + box-sizing: border-box + + > thead + > tr > td > span + align-items: center + justify-items: center + display: flex + cursor: pointer + user-select: none diff --git a/maubot-src/maubot/management/frontend/src/style/pages/instance.sass b/maubot-src/maubot/management/frontend/src/style/pages/instance.sass new file mode 100644 index 0000000..e1da481 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/style/pages/instance.sass @@ -0,0 +1,35 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +> div.instance + > div.preference-table + .select-client + display: flex + align-items: center + + img.avatar, svg.avatar + max-height: 1.375rem + border-radius: 50% + margin-right: .5rem + + > div.ace_editor + z-index: 0 + height: 15rem !important + width: calc(100% - 1rem) !important + font-size: 12px + font-family: "Fira Code", monospace + + margin: .75rem .5rem 1.5rem diff --git a/maubot-src/maubot/management/frontend/src/style/pages/log.sass b/maubot-src/maubot/management/frontend/src/style/pages/log.sass new file mode 100644 index 0000000..db7d91d --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/style/pages/log.sass @@ -0,0 +1,89 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +div.log + height: 100% + width: 100% + overflow: auto + +div.log > div.lines + text-align: left + font-size: 12px + max-height: 100% + min-width: 100% + font-family: "Fira Code", monospace + display: table + +div.log > div.lines > div.row + display: table-row + white-space: pre + + &.trace + opacity: .75 + + &.debug, &.trace + background-color: $background + + &:nth-child(odd) + background-color: $background-dark + + &.info + background-color: #AAFAFA + + &:nth-child(odd) + background-color: #66FAFA + + &.warning, &.warn + background-color: #FABB77 + + &:nth-child(odd) + background-color: #FAAA55 + + &.error + background-color: #FAAAAA + + &:nth-child(odd) + background-color: #FA9999 + + &.fatal + background-color: #CC44CC + + &:nth-child(odd) + background-color: #AA44AA + + &.unfocused + opacity: .25 + + > span + padding: .125rem .25rem + display: table-cell + + &:first-child + padding-left: 0 + + &:last-child + padding-right: 0 + + a + color: inherit + text-decoration: none + + &:hover + text-decoration: underline + + > span.text + > div.content > * + background-color: inherit !important + margin: 0 !important diff --git a/maubot-src/maubot/management/frontend/src/style/pages/login.sass b/maubot-src/maubot/management/frontend/src/style/pages/login.sass new file mode 100644 index 0000000..5a9e139 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/style/pages/login.sass @@ -0,0 +1,62 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +.maubot-wrapper:not(.authenticated) + background-color: $primary + + text-align: center + +.login + width: 25rem + height: 23rem + display: inline-block + box-sizing: border-box + background-color: white + border-radius: .25rem + margin-top: 3rem + + @media screen and (max-width: 27rem) + margin: 3rem 1rem 0 + width: calc(100% - 2rem) + + h1 + color: $primary + margin: 3rem 0 + + input, button + margin: .5rem 2.5rem + height: 3rem + width: calc(100% - 5rem) + box-sizing: border-box + + input + +input + + button + +button($width: calc(100% - 5rem), $height: 3rem, $padding: 0) + +main-color-button + + .spinner + +white-spinner + +thick-spinner + width: 2rem + + &.errored + height: 26.5rem + + .error + +notification($error) + margin: .5rem 2.5rem diff --git a/maubot-src/maubot/management/frontend/src/style/pages/mixins/instancelist.sass b/maubot-src/maubot/management/frontend/src/style/pages/mixins/instancelist.sass new file mode 100644 index 0000000..886d542 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/style/pages/mixins/instancelist.sass @@ -0,0 +1,40 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +=instancelist() + margin: 1rem 0 + + display: flex + flex-wrap: wrap + + > h3 + margin: .5rem + width: 100% + + > a.instance + display: block + width: calc(50% - 1rem) + padding: .375rem .5rem + margin: .5rem + background-color: white + border-radius: .25rem + color: $text-color + text-decoration: none + box-sizing: border-box + border: 1px solid $primary + + &:hover + background-color: $primary diff --git a/maubot-src/maubot/management/frontend/src/style/pages/mixins/upload-container.sass b/maubot-src/maubot/management/frontend/src/style/pages/mixins/upload-container.sass new file mode 100644 index 0000000..05af405 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/style/pages/mixins/upload-container.sass @@ -0,0 +1,43 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +=upload-box() + position: relative + overflow: hidden + + display: flex + align-items: center + justify-content: center + + > svg.upload + position: absolute + display: block + + padding: 1rem + user-select: none + + + > input.file-selector + position: absolute + user-select: none + opacity: 0 + + > div.spinner + +thick-spinner + + &:not(.uploading) + > input.file-selector + cursor: pointer diff --git a/maubot-src/maubot/management/frontend/src/style/pages/modal.sass b/maubot-src/maubot/management/frontend/src/style/pages/modal.sass new file mode 100644 index 0000000..76056f0 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/style/pages/modal.sass @@ -0,0 +1,71 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +div.modal-wrapper-wrapper + z-index: 9001 + position: fixed + top: 0 + bottom: 0 + left: 0 + right: 0 + background-color: rgba(0, 0, 0, 0.5) + + --modal-margin: 2.5rem + --button-height: 0rem + + @media screen and (max-width: 45rem) + --modal-margin: 1rem + --button-height: 2.5rem + + @media screen and (max-width: 35rem) + --modal-margin: 0rem + --button-height: 3rem + + button.close + +button + + display: none + + width: 100% + height: var(--button-height) + border-radius: .25rem .25rem 0 0 + + @media screen and (max-width: 45rem) + display: block + @media screen and (max-width: 35rem) + border-radius: 0 + + div.modal-wrapper + width: calc(100% - 2 * var(--modal-margin)) + height: calc(100% - 2 * var(--modal-margin) - var(--button-height)) + margin: var(--modal-margin) + border-radius: .25rem + + @media screen and (max-width: 35rem) + border-radius: 0 + + div.modal + padding: 1rem + height: 100% + width: 100% + background-color: $background + box-sizing: border-box + border-radius: .25rem + + @media screen and (max-width: 45rem) + border-radius: 0 0 .25rem .25rem + @media screen and (max-width: 35rem) + border-radius: 0 + padding: .5rem diff --git a/maubot-src/maubot/management/frontend/src/style/pages/plugin.sass b/maubot-src/maubot/management/frontend/src/style/pages/plugin.sass new file mode 100644 index 0000000..3e358d7 --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/style/pages/plugin.sass @@ -0,0 +1,53 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +> .plugin + > .upload-box + +upload-box + + width: calc(100% - 1rem) + height: 10rem + margin: .5rem + border-radius: .5rem + box-sizing: border-box + + border: .25rem dotted $primary + + > svg.upload + width: 8rem + height: 8rem + + opacity: .5 + + > input.file-selector + width: 100% + height: 100% + + &:not(.uploading):hover, &:not(.uploading).drag + border: .25rem solid $primary + background-color: $primary-light + + > svg.upload + opacity: 1 + + &.uploading + > svg.upload + visibility: hidden + > input.file-selector + cursor: default + + > div.instances + +instancelist diff --git a/maubot-src/maubot/management/frontend/src/style/pages/sidebar.sass b/maubot-src/maubot/management/frontend/src/style/pages/sidebar.sass new file mode 100644 index 0000000..7d68cab --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/style/pages/sidebar.sass @@ -0,0 +1,79 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +> nav.sidebar + grid-area: sidebar + background-color: white + + padding: .5rem + + overflow-y: auto + + div.buttons + margin-bottom: 1.5rem + + button + +button + background-color: white + width: 100% + + div.list + &:not(:last-of-type) + margin-bottom: 1.5rem + + div.title + h2 + display: inline-block + margin: 0 0 .25rem 0 + font-size: 1.25rem + + a + display: inline-block + float: right + + a.entry + display: block + color: $text-color + text-decoration: none + padding: .25rem + border-radius: .25rem + height: 2rem + box-sizing: border-box + white-space: nowrap + overflow-x: hidden + + &:not(:hover) > svg.chevron + display: none + + > svg.chevron + float: right + + &:hover + background-color: $primary-light + + &.active + background-color: $primary + color: white + + &.client + img.avatar, svg.avatar + max-height: 1.5rem + border-radius: 100% + vertical-align: middle + + span.displayname, span.id + margin-left: .25rem + vertical-align: middle diff --git a/maubot-src/maubot/management/frontend/src/style/pages/topbar.sass b/maubot-src/maubot/management/frontend/src/style/pages/topbar.sass new file mode 100644 index 0000000..51a8c3d --- /dev/null +++ b/maubot-src/maubot/management/frontend/src/style/pages/topbar.sass @@ -0,0 +1,79 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2022 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +> .topbar + background-color: $primary + + display: flex + justify-items: center + align-items: center + padding: 0 .75rem + + @media screen and (min-width: calc(35rem + 1px)) + display: none + +// Hamburger menu based on "Pure CSS Hamburger fold-out menu" codepen by Erik Terwan (MIT license) +// https://codepen.io/erikterwan/pen/EVzeRP + +> .topbar + user-select: none + +> .topbar > .hamburger + display: block + user-select: none + cursor: pointer + + > span + display: block + width: 29px + height: 4px + margin-bottom: 5px + position: relative + + background: white + border-radius: 3px + user-select: none + + z-index: 1 + + transform-origin: 4px 0 + + transition: transform 0.4s cubic-bezier(0.77, 0.2, 0.05, 1.0), background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1.0), opacity 0.55s ease + + &:nth-of-type(1) + transform-origin: 0 0 + + &:nth-of-type(3) + transform-origin: 0 100% + + transform: translateY(2px) + transition: transform 0.4s cubic-bezier(0.77, 0.2, 0.05, 1.0) + + &.active + transform: translateX(1px) translateY(4px) + + &.active > span + opacity: 1 + + &:nth-of-type(1) + transform: rotate(45deg) translate(-2px, -1px) + + &:nth-of-type(2) + opacity: 0 + transform: rotate(0deg) scale(0.2, 0.2) + + &:nth-of-type(3) + transform: rotate(-45deg) translate(0, -1px) diff --git a/maubot-src/maubot/management/frontend/yarn.lock b/maubot-src/maubot/management/frontend/yarn.lock new file mode 100644 index 0000000..31b86b2 --- /dev/null +++ b/maubot-src/maubot/management/frontend/yarn.lock @@ -0,0 +1,8963 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ampproject/remapping@^2.1.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.1.2.tgz#4edca94973ded9630d20101cd8559cedb8d8bd34" + integrity sha512-hoyByceqwKirw7w3Z7gnIIZC3Wx3J484Y3L/cMpXFbr7d9ZQj2mODrirNzcJa+SM3UlpWXYvKV4RlRpFXlWgXg== + dependencies: + "@jridgewell/trace-mapping" "^0.3.0" + +"@apideck/better-ajv-errors@^0.3.1": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.3.tgz#ab0b1e981e1749bf59736cf7ebe25cfc9f949c15" + integrity sha512-9o+HO2MbJhJHjDYZaDxJmSDckvDpiuItEsrIShV0DXeCshXWRHhqYyU/PKHMkuClOmFnZhRd6wzv4vpDu/dRKg== + dependencies: + json-schema "^0.4.0" + jsonpointer "^5.0.0" + leven "^3.1.0" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.8.3": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" + integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg== + dependencies: + "@babel/highlight" "^7.16.7" + +"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.16.8", "@babel/compat-data@^7.17.0", "@babel/compat-data@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.7.tgz#078d8b833fbbcc95286613be8c716cef2b519fa2" + integrity sha512-p8pdE6j0a29TNGebNm7NzYZWB3xVZJBZ7XGs42uAKzQo8VQ3F0By/cQCtUEABwIqw5zo6WA4NbmxsfzADzMKnQ== + +"@babel/core@^7.1.0", "@babel/core@^7.11.1", "@babel/core@^7.12.3", "@babel/core@^7.16.0", "@babel/core@^7.7.2", "@babel/core@^7.8.0": + version "7.17.8" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.8.tgz#3dac27c190ebc3a4381110d46c80e77efe172e1a" + integrity sha512-OdQDV/7cRBtJHLSOBqqbYNkOcydOgnX59TZx4puf41fzcVtN3e/4yqY8lMQsK+5X2lJtAdmA+6OHqsj1hBJ4IQ== + dependencies: + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.17.7" + "@babel/helper-compilation-targets" "^7.17.7" + "@babel/helper-module-transforms" "^7.17.7" + "@babel/helpers" "^7.17.8" + "@babel/parser" "^7.17.8" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.17.3" + "@babel/types" "^7.17.0" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.1.2" + semver "^6.3.0" + +"@babel/eslint-parser@^7.16.3": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.17.0.tgz#eabb24ad9f0afa80e5849f8240d0e5facc2d90d6" + integrity sha512-PUEJ7ZBXbRkbq3qqM/jZ2nIuakUBqCYc7Qf52Lj7dlZ6zERnqisdHioL0l4wwQZnmskMeasqUNzLBFKs3nylXA== + dependencies: + eslint-scope "^5.1.1" + eslint-visitor-keys "^2.1.0" + semver "^6.3.0" + +"@babel/generator@^7.17.3", "@babel/generator@^7.17.7", "@babel/generator@^7.7.2": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.7.tgz#8da2599beb4a86194a3b24df6c085931d9ee45ad" + integrity sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w== + dependencies: + "@babel/types" "^7.17.0" + jsesc "^2.5.1" + source-map "^0.5.0" + +"@babel/helper-annotate-as-pure@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz#bb2339a7534a9c128e3102024c60760a3a7f3862" + integrity sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-builder-binary-assignment-operator-visitor@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.16.7.tgz#38d138561ea207f0f69eb1626a418e4f7e6a580b" + integrity sha512-C6FdbRaxYjwVu/geKW4ZeQ0Q31AftgRcdSnZ5/jsH6BzCJbtvXvhpfkbkThYSuutZA7nCXpPR6AD9zd1dprMkA== + dependencies: + "@babel/helper-explode-assignable-expression" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.16.7", "@babel/helper-compilation-targets@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.7.tgz#a3c2924f5e5f0379b356d4cfb313d1414dc30e46" + integrity sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w== + dependencies: + "@babel/compat-data" "^7.17.7" + "@babel/helper-validator-option" "^7.16.7" + browserslist "^4.17.5" + semver "^6.3.0" + +"@babel/helper-create-class-features-plugin@^7.16.10", "@babel/helper-create-class-features-plugin@^7.16.7", "@babel/helper-create-class-features-plugin@^7.17.6": + version "7.17.6" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.6.tgz#3778c1ed09a7f3e65e6d6e0f6fbfcc53809d92c9" + integrity sha512-SogLLSxXm2OkBbSsHZMM4tUi8fUzjs63AT/d0YQIzr6GSd8Hxsbk2KYDX0k0DweAzGMj/YWeiCsorIdtdcW8Eg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.16.7" + "@babel/helper-member-expression-to-functions" "^7.16.7" + "@babel/helper-optimise-call-expression" "^7.16.7" + "@babel/helper-replace-supers" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + +"@babel/helper-create-regexp-features-plugin@^7.16.7": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.17.0.tgz#1dcc7d40ba0c6b6b25618997c5dbfd310f186fe1" + integrity sha512-awO2So99wG6KnlE+TPs6rn83gCz5WlEePJDTnLEqbchMVrBeAujURVphRdigsk094VhvZehFoNOihSlcBjwsXA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.7" + regexpu-core "^5.0.1" + +"@babel/helper-define-polyfill-provider@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.1.tgz#52411b445bdb2e676869e5a74960d2d3826d2665" + integrity sha512-J9hGMpJQmtWmj46B3kBHmL38UhJGhYX7eqkcq+2gsstyYt341HmPeWspihX43yVRA0mS+8GGk2Gckc7bY/HCmA== + dependencies: + "@babel/helper-compilation-targets" "^7.13.0" + "@babel/helper-module-imports" "^7.12.13" + "@babel/helper-plugin-utils" "^7.13.0" + "@babel/traverse" "^7.13.0" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + semver "^6.1.2" + +"@babel/helper-environment-visitor@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz#ff484094a839bde9d89cd63cba017d7aae80ecd7" + integrity sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-explode-assignable-expression@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.16.7.tgz#12a6d8522fdd834f194e868af6354e8650242b7a" + integrity sha512-KyUenhWMC8VrxzkGP0Jizjo4/Zx+1nNZhgocs+gLzyZyB8SHidhoq9KK/8Ato4anhwsivfkBLftky7gvzbZMtQ== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-function-name@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz#f1ec51551fb1c8956bc8dd95f38523b6cf375f8f" + integrity sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA== + dependencies: + "@babel/helper-get-function-arity" "^7.16.7" + "@babel/template" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/helper-get-function-arity@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz#ea08ac753117a669f1508ba06ebcc49156387419" + integrity sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-hoist-variables@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz#86bcb19a77a509c7b77d0e22323ef588fa58c246" + integrity sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-member-expression-to-functions@^7.16.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.17.7.tgz#a34013b57d8542a8c4ff8ba3f747c02452a4d8c4" + integrity sha512-thxXgnQ8qQ11W2wVUObIqDL4p148VMxkt5T/qpN5k2fboRyzFGFmKsTGViquyM5QHKUy48OZoca8kw4ajaDPyw== + dependencies: + "@babel/types" "^7.17.0" + +"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437" + integrity sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-module-transforms@^7.16.7", "@babel/helper-module-transforms@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz#3943c7f777139e7954a5355c815263741a9c1cbd" + integrity sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw== + dependencies: + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-simple-access" "^7.17.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/helper-validator-identifier" "^7.16.7" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.17.3" + "@babel/types" "^7.17.0" + +"@babel/helper-optimise-call-expression@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.7.tgz#a34e3560605abbd31a18546bd2aad3e6d9a174f2" + integrity sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz#aa3a8ab4c3cceff8e65eb9e73d87dc4ff320b2f5" + integrity sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA== + +"@babel/helper-remap-async-to-generator@^7.16.8": + version "7.16.8" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.8.tgz#29ffaade68a367e2ed09c90901986918d25e57e3" + integrity sha512-fm0gH7Flb8H51LqJHy3HJ3wnE1+qtYR2A99K06ahwrawLdOFsCEWjZOrYricXJHoPSudNKxrMBUPEIPxiIIvBw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-wrap-function" "^7.16.8" + "@babel/types" "^7.16.8" + +"@babel/helper-replace-supers@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.16.7.tgz#e9f5f5f32ac90429c1a4bdec0f231ef0c2838ab1" + integrity sha512-y9vsWilTNaVnVh6xiJfABzsNpgDPKev9HnAgz6Gb1p6UUwf9NepdlsV7VXGCftJM+jqD5f7JIEubcpLjZj5dBw== + dependencies: + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-member-expression-to-functions" "^7.16.7" + "@babel/helper-optimise-call-expression" "^7.16.7" + "@babel/traverse" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/helper-simple-access@^7.17.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz#aaa473de92b7987c6dfa7ce9a7d9674724823367" + integrity sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA== + dependencies: + "@babel/types" "^7.17.0" + +"@babel/helper-skip-transparent-expression-wrappers@^7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.16.0.tgz#0ee3388070147c3ae051e487eca3ebb0e2e8bb09" + integrity sha512-+il1gTy0oHwUsBQZyJvukbB4vPMdcYBrFHa0Uc4AizLxbq6BOYC51Rv4tWocX9BLBDLZ4kc6qUFpQ6HRgL+3zw== + dependencies: + "@babel/types" "^7.16.0" + +"@babel/helper-split-export-declaration@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz#0b648c0c42da9d3920d85ad585f2778620b8726b" + integrity sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-validator-identifier@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" + integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== + +"@babel/helper-validator-option@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz#b203ce62ce5fe153899b617c08957de860de4d23" + integrity sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ== + +"@babel/helper-wrap-function@^7.16.8": + version "7.16.8" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.16.8.tgz#58afda087c4cd235de92f7ceedebca2c41274200" + integrity sha512-8RpyRVIAW1RcDDGTA+GpPAwV22wXCfKOoM9bet6TLkGIFTkRQSkH1nMQ5Yet4MpoXe1ZwHPVtNasc2w0uZMqnw== + dependencies: + "@babel/helper-function-name" "^7.16.7" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.16.8" + "@babel/types" "^7.16.8" + +"@babel/helpers@^7.17.8": + version "7.17.8" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.17.8.tgz#288450be8c6ac7e4e44df37bcc53d345e07bc106" + integrity sha512-QcL86FGxpfSJwGtAvv4iG93UL6bmqBdmoVY0CMCU2g+oD2ezQse3PT5Pa+jiD6LJndBQi0EDlpzOWNlLuhz5gw== + dependencies: + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.17.3" + "@babel/types" "^7.17.0" + +"@babel/highlight@^7.16.7": + version "7.16.10" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.10.tgz#744f2eb81579d6eea753c227b0f570ad785aba88" + integrity sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.16.7", "@babel/parser@^7.17.3", "@babel/parser@^7.17.8": + version "7.17.8" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.8.tgz#2817fb9d885dd8132ea0f8eb615a6388cca1c240" + integrity sha512-BoHhDJrJXqcg+ZL16Xv39H9n+AqJ4pcDrQBGZN+wHxIysrLZ3/ECwCBUch/1zUNhnsXULcONU3Ei5Hmkfk6kiQ== + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.7.tgz#4eda6d6c2a0aa79c70fa7b6da67763dfe2141050" + integrity sha512-anv/DObl7waiGEnC24O9zqL0pSuI9hljihqiDuFHC8d7/bjr/4RLGPWuc8rYOff/QPzbEPSkzG8wGG9aDuhHRg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.16.7.tgz#cc001234dfc139ac45f6bcf801866198c8c72ff9" + integrity sha512-di8vUHRdf+4aJ7ltXhaDbPoszdkh59AQtJM5soLsuHpQJdFQZOA4uGj0V2u/CZ8bJ/u8ULDL5yq6FO/bCXnKHw== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" + "@babel/plugin-proposal-optional-chaining" "^7.16.7" + +"@babel/plugin-proposal-async-generator-functions@^7.16.8": + version "7.16.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.16.8.tgz#3bdd1ebbe620804ea9416706cd67d60787504bc8" + integrity sha512-71YHIvMuiuqWJQkebWJtdhQTfd4Q4mF76q2IX37uZPkG9+olBxsX+rH1vkhFto4UeJZ9dPY2s+mDvhDm1u2BGQ== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-remap-async-to-generator" "^7.16.8" + "@babel/plugin-syntax-async-generators" "^7.8.4" + +"@babel/plugin-proposal-class-properties@^7.16.0", "@babel/plugin-proposal-class-properties@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.16.7.tgz#925cad7b3b1a2fcea7e59ecc8eb5954f961f91b0" + integrity sha512-IobU0Xme31ewjYOShSIqd/ZGM/r/cuOz2z0MDbNrhF5FW+ZVgi0f2lyeoj9KFPDOAqsYxmLWZte1WOwlvY9aww== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-proposal-class-static-block@^7.16.7": + version "7.17.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.17.6.tgz#164e8fd25f0d80fa48c5a4d1438a6629325ad83c" + integrity sha512-X/tididvL2zbs7jZCeeRJ8167U/+Ac135AM6jCAx6gYXDUviZV5Ku9UDvWS2NCuWlFjIRXklYhwo6HhAC7ETnA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.17.6" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + +"@babel/plugin-proposal-decorators@^7.16.4": + version "7.17.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.17.8.tgz#4f0444e896bee85d35cf714a006fc5418f87ff00" + integrity sha512-U69odN4Umyyx1xO1rTII0IDkAEC+RNlcKXtqOblfpzqy1C+aOplb76BQNq0+XdpVkOaPlpEDwd++joY8FNFJKA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.17.6" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-replace-supers" "^7.16.7" + "@babel/plugin-syntax-decorators" "^7.17.0" + charcodes "^0.2.0" + +"@babel/plugin-proposal-dynamic-import@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.16.7.tgz#c19c897eaa46b27634a00fee9fb7d829158704b2" + integrity sha512-I8SW9Ho3/8DRSdmDdH3gORdyUuYnk1m4cMxUAdu5oy4n3OfN8flDEH+d60iG7dUfi0KkYwSvoalHzzdRzpWHTg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + +"@babel/plugin-proposal-export-namespace-from@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.16.7.tgz#09de09df18445a5786a305681423ae63507a6163" + integrity sha512-ZxdtqDXLRGBL64ocZcs7ovt71L3jhC1RGSyR996svrCi3PYqHNkb3SwPJCs8RIzD86s+WPpt2S73+EHCGO+NUA== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + +"@babel/plugin-proposal-json-strings@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.16.7.tgz#9732cb1d17d9a2626a08c5be25186c195b6fa6e8" + integrity sha512-lNZ3EEggsGY78JavgbHsK9u5P3pQaW7k4axlgFLYkMd7UBsiNahCITShLjNQschPyjtO6dADrL24757IdhBrsQ== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-json-strings" "^7.8.3" + +"@babel/plugin-proposal-logical-assignment-operators@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.16.7.tgz#be23c0ba74deec1922e639832904be0bea73cdea" + integrity sha512-K3XzyZJGQCr00+EtYtrDjmwX7o7PLK6U9bi1nCwkQioRFVUv6dJoxbQjtWVtP+bCPy82bONBKG8NPyQ4+i6yjg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + +"@babel/plugin-proposal-nullish-coalescing-operator@^7.16.0", "@babel/plugin-proposal-nullish-coalescing-operator@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.16.7.tgz#141fc20b6857e59459d430c850a0011e36561d99" + integrity sha512-aUOrYU3EVtjf62jQrCj63pYZ7k6vns2h/DQvHPWGmsJRYzWXZ6/AsfgpiRy6XiuIDADhJzP2Q9MwSMKauBQ+UQ== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + +"@babel/plugin-proposal-numeric-separator@^7.16.0", "@babel/plugin-proposal-numeric-separator@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.16.7.tgz#d6b69f4af63fb38b6ca2558442a7fb191236eba9" + integrity sha512-vQgPMknOIgiuVqbokToyXbkY/OmmjAzr/0lhSIbG/KmnzXPGwW/AdhdKpi+O4X/VkWiWjnkKOBiqJrTaC98VKw== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + +"@babel/plugin-proposal-object-rest-spread@^7.16.7": + version "7.17.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.17.3.tgz#d9eb649a54628a51701aef7e0ea3d17e2b9dd390" + integrity sha512-yuL5iQA/TbZn+RGAfxQXfi7CNLmKi1f8zInn4IgobuCWcAb7i+zj4TYzQ9l8cEzVyJ89PDGuqxK1xZpUDISesw== + dependencies: + "@babel/compat-data" "^7.17.0" + "@babel/helper-compilation-targets" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.16.7" + +"@babel/plugin-proposal-optional-catch-binding@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.16.7.tgz#c623a430674ffc4ab732fd0a0ae7722b67cb74cf" + integrity sha512-eMOH/L4OvWSZAE1VkHbr1vckLG1WUcHGJSLqqQwl2GaUqG6QjddvrOaTUMNYiv77H5IKPMZ9U9P7EaHwvAShfA== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + +"@babel/plugin-proposal-optional-chaining@^7.16.0", "@babel/plugin-proposal-optional-chaining@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.16.7.tgz#7cd629564724816c0e8a969535551f943c64c39a" + integrity sha512-eC3xy+ZrUcBtP7x+sq62Q/HYd674pPTb/77XZMb5wbDPGWIdUbSr4Agr052+zaUPSb+gGRnjxXfKFvx5iMJ+DA== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + +"@babel/plugin-proposal-private-methods@^7.16.0", "@babel/plugin-proposal-private-methods@^7.16.11": + version "7.16.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.16.11.tgz#e8df108288555ff259f4527dbe84813aac3a1c50" + integrity sha512-F/2uAkPlXDr8+BHpZvo19w3hLFKge+k75XUprE6jaqKxjGkSYcK+4c+bup5PdW/7W/Rpjwql7FTVEDW+fRAQsw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.16.10" + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-proposal-private-property-in-object@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.16.7.tgz#b0b8cef543c2c3d57e59e2c611994861d46a3fce" + integrity sha512-rMQkjcOFbm+ufe3bTZLyOfsOUOxyvLXZJCTARhJr+8UMSoZmqTe1K1BgkFcrW37rAchWg57yI69ORxiWvUINuQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-create-class-features-plugin" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + +"@babel/plugin-proposal-unicode-property-regex@^7.16.7", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.16.7.tgz#635d18eb10c6214210ffc5ff4932552de08188a2" + integrity sha512-QRK0YI/40VLhNVGIjRNAAQkEHws0cswSdFFjpFyt943YmJIU1da9uW63Iu6NFV6CxTZW5eTDCrwZUstBWgp/Rg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-bigint@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" + integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.12.13", "@babel/plugin-syntax-class-properties@^7.8.3": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-class-static-block@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" + integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-decorators@^7.17.0": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.17.0.tgz#a2be3b2c9fe7d78bd4994e790896bc411e2f166d" + integrity sha512-qWe85yCXsvDEluNP0OyeQjH63DlhAR3W7K9BxxU1MvbDb48tgBG+Ao6IJJ6smPDrrVzSQZrbF6donpkFBMcs3A== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-syntax-dynamic-import@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" + integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-export-namespace-from@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" + integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-syntax-flow@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.16.7.tgz#202b147e5892b8452bbb0bb269c7ed2539ab8832" + integrity sha512-UDo3YGQO0jH6ytzVwgSLv9i/CzMcUjbKenL67dTrAZPPv6GFAtDhe6jqnvmoKzC/7htNTohhos+onPtDMqJwaQ== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-syntax-import-meta@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-jsx@^7.12.13", "@babel/plugin-syntax-jsx@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.16.7.tgz#50b6571d13f764266a113d77c82b4a6508bbe665" + integrity sha512-Esxmk7YjA8QysKeT3VhTXvF6y77f/a91SIs4pWb4H2eWGQkCKFgQaG6hdoEVZtGsrAcb2K5BW66XsOErD4WU3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4", "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.10.4", "@babel/plugin-syntax-numeric-separator@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-private-property-in-object@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" + integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-top-level-await@^7.14.5", "@babel/plugin-syntax-top-level-await@^7.8.3": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-typescript@^7.16.7", "@babel/plugin-syntax-typescript@^7.7.2": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.16.7.tgz#39c9b55ee153151990fb038651d58d3fd03f98f8" + integrity sha512-YhUIJHHGkqPgEcMYkPCKTyGUdoGKWtopIycQyjJH8OjvRgOYsXsaKehLVPScKJWAULPxMa4N1vCe6szREFlZ7A== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-arrow-functions@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.16.7.tgz#44125e653d94b98db76369de9c396dc14bef4154" + integrity sha512-9ffkFFMbvzTvv+7dTp/66xvZAWASuPD5Tl9LK3Z9vhOmANo6j94rik+5YMBt4CwHVMWLWpMsriIc2zsa3WW3xQ== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-async-to-generator@^7.16.8": + version "7.16.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.16.8.tgz#b83dff4b970cf41f1b819f8b49cc0cfbaa53a808" + integrity sha512-MtmUmTJQHCnyJVrScNzNlofQJ3dLFuobYn3mwOTKHnSCMtbNsqvF71GQmJfFjdrXSsAA7iysFmYWw4bXZ20hOg== + dependencies: + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-remap-async-to-generator" "^7.16.8" + +"@babel/plugin-transform-block-scoped-functions@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.16.7.tgz#4d0d57d9632ef6062cdf354bb717102ee042a620" + integrity sha512-JUuzlzmF40Z9cXyytcbZEZKckgrQzChbQJw/5PuEHYeqzCsvebDx0K0jWnIIVcmmDOAVctCgnYs0pMcrYj2zJg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-block-scoping@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.16.7.tgz#f50664ab99ddeaee5bc681b8f3a6ea9d72ab4f87" + integrity sha512-ObZev2nxVAYA4bhyusELdo9hb3H+A56bxH3FZMbEImZFiEDYVHXQSJ1hQKFlDnlt8G9bBrCZ5ZpURZUrV4G5qQ== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-classes@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.16.7.tgz#8f4b9562850cd973de3b498f1218796eb181ce00" + integrity sha512-WY7og38SFAGYRe64BrjKf8OrE6ulEHtr5jEYaZMwox9KebgqPi67Zqz8K53EKk1fFEJgm96r32rkKZ3qA2nCWQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.16.7" + "@babel/helper-optimise-call-expression" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-replace-supers" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + globals "^11.1.0" + +"@babel/plugin-transform-computed-properties@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.16.7.tgz#66dee12e46f61d2aae7a73710f591eb3df616470" + integrity sha512-gN72G9bcmenVILj//sv1zLNaPyYcOzUho2lIJBMh/iakJ9ygCo/hEF9cpGb61SCMEDxbbyBoVQxrt+bWKu5KGw== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-destructuring@^7.16.7": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.17.7.tgz#49dc2675a7afa9a5e4c6bdee636061136c3408d1" + integrity sha512-XVh0r5yq9sLR4vZ6eVZe8FKfIcSgaTBxVBRSYokRj2qksf6QerYnTxz9/GTuKTH/n/HwLP7t6gtlybHetJ/6hQ== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-dotall-regex@^7.16.7", "@babel/plugin-transform-dotall-regex@^7.4.4": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.16.7.tgz#6b2d67686fab15fb6a7fd4bd895d5982cfc81241" + integrity sha512-Lyttaao2SjZF6Pf4vk1dVKv8YypMpomAbygW+mU5cYP3S5cWTfCJjG8xV6CFdzGFlfWK81IjL9viiTvpb6G7gQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-duplicate-keys@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.16.7.tgz#2207e9ca8f82a0d36a5a67b6536e7ef8b08823c9" + integrity sha512-03DvpbRfvWIXyK0/6QiR1KMTWeT6OcQ7tbhjrXyFS02kjuX/mu5Bvnh5SDSWHxyawit2g5aWhKwI86EE7GUnTw== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-exponentiation-operator@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.16.7.tgz#efa9862ef97e9e9e5f653f6ddc7b665e8536fe9b" + integrity sha512-8UYLSlyLgRixQvlYH3J2ekXFHDFLQutdy7FfFAMm3CPZ6q9wHCwnUyiXpQCe3gVVnQlHc5nsuiEVziteRNTXEA== + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-flow-strip-types@^7.16.0": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.16.7.tgz#291fb140c78dabbf87f2427e7c7c332b126964b8" + integrity sha512-mzmCq3cNsDpZZu9FADYYyfZJIOrSONmHcop2XEKPdBNMa4PDC4eEvcOvzZaCNcjKu72v0XQlA5y1g58aLRXdYg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-flow" "^7.16.7" + +"@babel/plugin-transform-for-of@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.16.7.tgz#649d639d4617dff502a9a158c479b3b556728d8c" + integrity sha512-/QZm9W92Ptpw7sjI9Nx1mbcsWz33+l8kuMIQnDwgQBG5s3fAfQvkRjQ7NqXhtNcKOnPkdICmUHyCaWW06HCsqg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-function-name@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.16.7.tgz#5ab34375c64d61d083d7d2f05c38d90b97ec65cf" + integrity sha512-SU/C68YVwTRxqWj5kgsbKINakGag0KTgq9f2iZEXdStoAbOzLHEBRYzImmA6yFo8YZhJVflvXmIHUO7GWHmxxA== + dependencies: + "@babel/helper-compilation-targets" "^7.16.7" + "@babel/helper-function-name" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-literals@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.16.7.tgz#254c9618c5ff749e87cb0c0cef1a0a050c0bdab1" + integrity sha512-6tH8RTpTWI0s2sV6uq3e/C9wPo4PTqqZps4uF0kzQ9/xPLFQtipynvmT1g/dOfEJ+0EQsHhkQ/zyRId8J2b8zQ== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-member-expression-literals@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.16.7.tgz#6e5dcf906ef8a098e630149d14c867dd28f92384" + integrity sha512-mBruRMbktKQwbxaJof32LT9KLy2f3gH+27a5XSuXo6h7R3vqltl0PgZ80C8ZMKw98Bf8bqt6BEVi3svOh2PzMw== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-modules-amd@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.16.7.tgz#b28d323016a7daaae8609781d1f8c9da42b13186" + integrity sha512-KaaEtgBL7FKYwjJ/teH63oAmE3lP34N3kshz8mm4VMAw7U3PxjVwwUmxEFksbgsNUaO3wId9R2AVQYSEGRa2+g== + dependencies: + "@babel/helper-module-transforms" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + babel-plugin-dynamic-import-node "^2.3.3" + +"@babel/plugin-transform-modules-commonjs@^7.16.8": + version "7.17.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.17.7.tgz#d86b217c8e45bb5f2dbc11eefc8eab62cf980d19" + integrity sha512-ITPmR2V7MqioMJyrxUo2onHNC3e+MvfFiFIR0RP21d3PtlVb6sfzoxNKiphSZUOM9hEIdzCcZe83ieX3yoqjUA== + dependencies: + "@babel/helper-module-transforms" "^7.17.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-simple-access" "^7.17.7" + babel-plugin-dynamic-import-node "^2.3.3" + +"@babel/plugin-transform-modules-systemjs@^7.16.7": + version "7.17.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.17.8.tgz#81fd834024fae14ea78fbe34168b042f38703859" + integrity sha512-39reIkMTUVagzgA5x88zDYXPCMT6lcaRKs1+S9K6NKBPErbgO/w/kP8GlNQTC87b412ZTlmNgr3k2JrWgHH+Bw== + dependencies: + "@babel/helper-hoist-variables" "^7.16.7" + "@babel/helper-module-transforms" "^7.17.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-validator-identifier" "^7.16.7" + babel-plugin-dynamic-import-node "^2.3.3" + +"@babel/plugin-transform-modules-umd@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.16.7.tgz#23dad479fa585283dbd22215bff12719171e7618" + integrity sha512-EMh7uolsC8O4xhudF2F6wedbSHm1HHZ0C6aJ7K67zcDNidMzVcxWdGr+htW9n21klm+bOn+Rx4CBsAntZd3rEQ== + dependencies: + "@babel/helper-module-transforms" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.16.8": + version "7.16.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.16.8.tgz#7f860e0e40d844a02c9dcf9d84965e7dfd666252" + integrity sha512-j3Jw+n5PvpmhRR+mrgIh04puSANCk/T/UA3m3P1MjJkhlK906+ApHhDIqBQDdOgL/r1UYpz4GNclTXxyZrYGSw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.16.7" + +"@babel/plugin-transform-new-target@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.16.7.tgz#9967d89a5c243818e0800fdad89db22c5f514244" + integrity sha512-xiLDzWNMfKoGOpc6t3U+etCE2yRnn3SM09BXqWPIZOBpL2gvVrBWUKnsJx0K/ADi5F5YC5f8APFfWrz25TdlGg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-object-super@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.16.7.tgz#ac359cf8d32cf4354d27a46867999490b6c32a94" + integrity sha512-14J1feiQVWaGvRxj2WjyMuXS2jsBkgB3MdSN5HuC2G5nRspa5RK9COcs82Pwy5BuGcjb+fYaUj94mYcOj7rCvw== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-replace-supers" "^7.16.7" + +"@babel/plugin-transform-parameters@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.16.7.tgz#a1721f55b99b736511cb7e0152f61f17688f331f" + integrity sha512-AT3MufQ7zZEhU2hwOA11axBnExW0Lszu4RL/tAlUJBuNoRak+wehQW8h6KcXOcgjY42fHtDxswuMhMjFEuv/aw== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-property-literals@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.16.7.tgz#2dadac85155436f22c696c4827730e0fe1057a55" + integrity sha512-z4FGr9NMGdoIl1RqavCqGG+ZuYjfZ/hkCIeuH6Do7tXmSm0ls11nYVSJqFEUOSJbDab5wC6lRE/w6YjVcr6Hqw== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-react-constant-elements@^7.12.1": + version "7.17.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.17.6.tgz#6cc273c2f612a6a50cb657e63ee1303e5e68d10a" + integrity sha512-OBv9VkyyKtsHZiHLoSfCn+h6yU7YKX8nrs32xUmOa1SRSk+t03FosB6fBZ0Yz4BpD1WV7l73Nsad+2Tz7APpqw== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-react-display-name@^7.16.0", "@babel/plugin-transform-react-display-name@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.16.7.tgz#7b6d40d232f4c0f550ea348593db3b21e2404340" + integrity sha512-qgIg8BcZgd0G/Cz916D5+9kqX0c7nPZyXaP8R2tLNN5tkyIZdG5fEwBrxwplzSnjC1jvQmyMNVwUCZPcbGY7Pg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-react-jsx-development@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.16.7.tgz#43a00724a3ed2557ed3f276a01a929e6686ac7b8" + integrity sha512-RMvQWvpla+xy6MlBpPlrKZCMRs2AGiHOGHY3xRwl0pEeim348dDyxeH4xBsMPbIMhujeq7ihE702eM2Ew0Wo+A== + dependencies: + "@babel/plugin-transform-react-jsx" "^7.16.7" + +"@babel/plugin-transform-react-jsx@^7.16.7": + version "7.17.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.17.3.tgz#eac1565da176ccb1a715dae0b4609858808008c1" + integrity sha512-9tjBm4O07f7mzKSIlEmPdiE6ub7kfIe6Cd+w+oQebpATfTQMAgW+YOuWxogbKVTulA+MEO7byMeIUtQ1z+z+ZQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-jsx" "^7.16.7" + "@babel/types" "^7.17.0" + +"@babel/plugin-transform-react-pure-annotations@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.16.7.tgz#232bfd2f12eb551d6d7d01d13fe3f86b45eb9c67" + integrity sha512-hs71ToC97k3QWxswh2ElzMFABXHvGiJ01IB1TbYQDGeWRKWz/MPUTh5jGExdHvosYKpnJW5Pm3S4+TA3FyX+GA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-regenerator@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.16.7.tgz#9e7576dc476cb89ccc5096fff7af659243b4adeb" + integrity sha512-mF7jOgGYCkSJagJ6XCujSQg+6xC1M77/03K2oBmVJWoFGNUtnVJO4WHKJk3dnPC8HCcj4xBQP1Egm8DWh3Pb3Q== + dependencies: + regenerator-transform "^0.14.2" + +"@babel/plugin-transform-reserved-words@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.16.7.tgz#1d798e078f7c5958eec952059c460b220a63f586" + integrity sha512-KQzzDnZ9hWQBjwi5lpY5v9shmm6IVG0U9pB18zvMu2i4H90xpT4gmqwPYsn8rObiadYe2M0gmgsiOIF5A/2rtg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-runtime@^7.16.4": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.17.0.tgz#0a2e08b5e2b2d95c4b1d3b3371a2180617455b70" + integrity sha512-fr7zPWnKXNc1xoHfrIU9mN/4XKX4VLZ45Q+oMhfsYIaHvg7mHgmhfOy/ckRWqDK7XF3QDigRpkh5DKq6+clE8A== + dependencies: + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + babel-plugin-polyfill-corejs2 "^0.3.0" + babel-plugin-polyfill-corejs3 "^0.5.0" + babel-plugin-polyfill-regenerator "^0.3.0" + semver "^6.3.0" + +"@babel/plugin-transform-shorthand-properties@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.16.7.tgz#e8549ae4afcf8382f711794c0c7b6b934c5fbd2a" + integrity sha512-hah2+FEnoRoATdIb05IOXf+4GzXYTq75TVhIn1PewihbpyrNWUt2JbudKQOETWw6QpLe+AIUpJ5MVLYTQbeeUg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-spread@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.16.7.tgz#a303e2122f9f12e0105daeedd0f30fb197d8ff44" + integrity sha512-+pjJpgAngb53L0iaA5gU/1MLXJIfXcYepLgXB3esVRf4fqmj8f2cxM3/FKaHsZms08hFQJkFccEWuIpm429TXg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" + +"@babel/plugin-transform-sticky-regex@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.16.7.tgz#c84741d4f4a38072b9a1e2e3fd56d359552e8660" + integrity sha512-NJa0Bd/87QV5NZZzTuZG5BPJjLYadeSZ9fO6oOUoL4iQx+9EEuw/eEM92SrsT19Yc2jgB1u1hsjqDtH02c3Drw== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-template-literals@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.16.7.tgz#f3d1c45d28967c8e80f53666fc9c3e50618217ab" + integrity sha512-VwbkDDUeenlIjmfNeDX/V0aWrQH2QiVyJtwymVQSzItFDTpxfyJh3EVaQiS0rIN/CqbLGr0VcGmuwyTdZtdIsA== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-typeof-symbol@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.16.7.tgz#9cdbe622582c21368bd482b660ba87d5545d4f7e" + integrity sha512-p2rOixCKRJzpg9JB4gjnG4gjWkWa89ZoYUnl9snJ1cWIcTH/hvxZqfO+WjG6T8DRBpctEol5jw1O5rA8gkCokQ== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-typescript@^7.16.7": + version "7.16.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.16.8.tgz#591ce9b6b83504903fa9dd3652c357c2ba7a1ee0" + integrity sha512-bHdQ9k7YpBDO2d0NVfkj51DpQcvwIzIusJ7mEUaMlbZq3Kt/U47j24inXZHQ5MDiYpCs+oZiwnXyKedE8+q7AQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-typescript" "^7.16.7" + +"@babel/plugin-transform-unicode-escapes@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.16.7.tgz#da8717de7b3287a2c6d659750c964f302b31ece3" + integrity sha512-TAV5IGahIz3yZ9/Hfv35TV2xEm+kaBDaZQCn2S/hG9/CZ0DktxJv9eKfPc7yYCvOYR4JGx1h8C+jcSOvgaaI/Q== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-unicode-regex@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.16.7.tgz#0f7aa4a501198976e25e82702574c34cfebe9ef2" + integrity sha512-oC5tYYKw56HO75KZVLQ+R/Nl3Hro9kf8iG0hXoaHP7tjAyCpvqBiSNe6vGrZni1Z6MggmUOC6A7VP7AVmw225Q== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/preset-env@^7.11.0", "@babel/preset-env@^7.12.1", "@babel/preset-env@^7.16.4": + version "7.16.11" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.16.11.tgz#5dd88fd885fae36f88fd7c8342475c9f0abe2982" + integrity sha512-qcmWG8R7ZW6WBRPZK//y+E3Cli151B20W1Rv7ln27vuPaXU/8TKms6jFdiJtF7UDTxcrb7mZd88tAeK9LjdT8g== + dependencies: + "@babel/compat-data" "^7.16.8" + "@babel/helper-compilation-targets" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-validator-option" "^7.16.7" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.16.7" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.16.7" + "@babel/plugin-proposal-async-generator-functions" "^7.16.8" + "@babel/plugin-proposal-class-properties" "^7.16.7" + "@babel/plugin-proposal-class-static-block" "^7.16.7" + "@babel/plugin-proposal-dynamic-import" "^7.16.7" + "@babel/plugin-proposal-export-namespace-from" "^7.16.7" + "@babel/plugin-proposal-json-strings" "^7.16.7" + "@babel/plugin-proposal-logical-assignment-operators" "^7.16.7" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.16.7" + "@babel/plugin-proposal-numeric-separator" "^7.16.7" + "@babel/plugin-proposal-object-rest-spread" "^7.16.7" + "@babel/plugin-proposal-optional-catch-binding" "^7.16.7" + "@babel/plugin-proposal-optional-chaining" "^7.16.7" + "@babel/plugin-proposal-private-methods" "^7.16.11" + "@babel/plugin-proposal-private-property-in-object" "^7.16.7" + "@babel/plugin-proposal-unicode-property-regex" "^7.16.7" + "@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-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@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-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + "@babel/plugin-transform-arrow-functions" "^7.16.7" + "@babel/plugin-transform-async-to-generator" "^7.16.8" + "@babel/plugin-transform-block-scoped-functions" "^7.16.7" + "@babel/plugin-transform-block-scoping" "^7.16.7" + "@babel/plugin-transform-classes" "^7.16.7" + "@babel/plugin-transform-computed-properties" "^7.16.7" + "@babel/plugin-transform-destructuring" "^7.16.7" + "@babel/plugin-transform-dotall-regex" "^7.16.7" + "@babel/plugin-transform-duplicate-keys" "^7.16.7" + "@babel/plugin-transform-exponentiation-operator" "^7.16.7" + "@babel/plugin-transform-for-of" "^7.16.7" + "@babel/plugin-transform-function-name" "^7.16.7" + "@babel/plugin-transform-literals" "^7.16.7" + "@babel/plugin-transform-member-expression-literals" "^7.16.7" + "@babel/plugin-transform-modules-amd" "^7.16.7" + "@babel/plugin-transform-modules-commonjs" "^7.16.8" + "@babel/plugin-transform-modules-systemjs" "^7.16.7" + "@babel/plugin-transform-modules-umd" "^7.16.7" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.16.8" + "@babel/plugin-transform-new-target" "^7.16.7" + "@babel/plugin-transform-object-super" "^7.16.7" + "@babel/plugin-transform-parameters" "^7.16.7" + "@babel/plugin-transform-property-literals" "^7.16.7" + "@babel/plugin-transform-regenerator" "^7.16.7" + "@babel/plugin-transform-reserved-words" "^7.16.7" + "@babel/plugin-transform-shorthand-properties" "^7.16.7" + "@babel/plugin-transform-spread" "^7.16.7" + "@babel/plugin-transform-sticky-regex" "^7.16.7" + "@babel/plugin-transform-template-literals" "^7.16.7" + "@babel/plugin-transform-typeof-symbol" "^7.16.7" + "@babel/plugin-transform-unicode-escapes" "^7.16.7" + "@babel/plugin-transform-unicode-regex" "^7.16.7" + "@babel/preset-modules" "^0.1.5" + "@babel/types" "^7.16.8" + babel-plugin-polyfill-corejs2 "^0.3.0" + babel-plugin-polyfill-corejs3 "^0.5.0" + babel-plugin-polyfill-regenerator "^0.3.0" + core-js-compat "^3.20.2" + semver "^6.3.0" + +"@babel/preset-modules@^0.1.5": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.5.tgz#ef939d6e7f268827e1841638dc6ff95515e115d9" + integrity sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA== + 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" + +"@babel/preset-react@^7.12.5", "@babel/preset-react@^7.16.0": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.16.7.tgz#4c18150491edc69c183ff818f9f2aecbe5d93852" + integrity sha512-fWpyI8UM/HE6DfPBzD8LnhQ/OcH8AgTaqcqP2nGOXEUV+VKBR5JRN9hCk9ai+zQQ57vtm9oWeXguBCPNUjytgA== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-validator-option" "^7.16.7" + "@babel/plugin-transform-react-display-name" "^7.16.7" + "@babel/plugin-transform-react-jsx" "^7.16.7" + "@babel/plugin-transform-react-jsx-development" "^7.16.7" + "@babel/plugin-transform-react-pure-annotations" "^7.16.7" + +"@babel/preset-typescript@^7.16.0": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.16.7.tgz#ab114d68bb2020afc069cd51b37ff98a046a70b9" + integrity sha512-WbVEmgXdIyvzB77AQjGBEyYPZx+8tTsO50XtfozQrkW8QB2rLJpH2lgx0TRw5EJrBxOZQ+wCcyPVQvS8tjEHpQ== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-validator-option" "^7.16.7" + "@babel/plugin-transform-typescript" "^7.16.7" + +"@babel/runtime-corejs3@^7.10.2": + version "7.17.8" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.17.8.tgz#d7dd49fb812f29c61c59126da3792d8740d4e284" + integrity sha512-ZbYSUvoSF6dXZmMl/CYTMOvzIFnbGfv4W3SEHYgMvNsFTeLaF2gkGAF4K2ddmtSK4Emej+0aYcnSC6N5dPCXUQ== + dependencies: + core-js-pure "^3.20.2" + regenerator-runtime "^0.13.4" + +"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": + version "7.17.8" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.8.tgz#3e56e4aff81befa55ac3ac6a0967349fd1c5bca2" + integrity sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA== + dependencies: + regenerator-runtime "^0.13.4" + +"@babel/template@^7.16.7", "@babel/template@^7.3.3": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" + integrity sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/parser" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/traverse@^7.13.0", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.3", "@babel/traverse@^7.7.2": + version "7.17.3" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.17.3.tgz#0ae0f15b27d9a92ba1f2263358ea7c4e7db47b57" + integrity sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.17.3" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.16.7" + "@babel/helper-hoist-variables" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/parser" "^7.17.3" + "@babel/types" "^7.17.0" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.12.6", "@babel/types@^7.16.0", "@babel/types@^7.16.7", "@babel/types@^7.16.8", "@babel/types@^7.17.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": + version "7.17.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.0.tgz#a826e368bccb6b3d84acd76acad5c0d87342390b" + integrity sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + to-fast-properties "^2.0.0" + +"@bcoe/v8-coverage@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== + +"@csstools/normalize.css@*": + version "12.0.0" + resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-12.0.0.tgz#a9583a75c3f150667771f30b60d9f059473e62c4" + integrity sha512-M0qqxAcwCsIVfpFQSlGN5XjXWu8l5JDZN+fPt1LeW5SZexQTgnaEvgXAY+CeygRw0EeppWHi12JxESWiWrB0Sg== + +"@csstools/postcss-color-function@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@csstools/postcss-color-function/-/postcss-color-function-1.0.3.tgz#251c961a852c99e9aabdbbdbefd50e9a96e8a9ff" + integrity sha512-J26I69pT2B3MYiLY/uzCGKVJyMYVg9TCpXkWsRlt+Yfq+nELUEm72QXIMYXs4xA9cJA4Oqs2EylrfokKl3mJEQ== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^1.1.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-font-format-keywords@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.0.tgz#7e7df948a83a0dfb7eb150a96e2390ac642356a1" + integrity sha512-oO0cZt8do8FdVBX8INftvIA4lUrKUSCcWUf9IwH9IPWOgKT22oAZFXeHLoDK7nhB2SmkNycp5brxfNMRLIhd6Q== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-hwb-function@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.0.tgz#d6785c1c5ba8152d1d392c66f3a6a446c6034f6d" + integrity sha512-VSTd7hGjmde4rTj1rR30sokY3ONJph1reCBTUXqeW1fKwETPy1x4t/XIeaaqbMbC5Xg4SM/lyXZ2S8NELT2TaA== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-ic-unit@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.0.tgz#f484db59fc94f35a21b6d680d23b0ec69b286b7f" + integrity sha512-i4yps1mBp2ijrx7E96RXrQXQQHm6F4ym1TOD0D69/sjDjZvQ22tqiEvaNw7pFZTUO5b9vWRHzbHzP9+UKuw+bA== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^1.1.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-is-pseudo-class@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.1.tgz#472fff2cf434bdf832f7145b2a5491587e790c9e" + integrity sha512-Og5RrTzwFhrKoA79c3MLkfrIBYmwuf/X83s+JQtz/Dkk/MpsaKtqHV1OOzYkogQ+tj3oYp5Mq39XotBXNqVc3Q== + dependencies: + postcss-selector-parser "^6.0.9" + +"@csstools/postcss-normalize-display-values@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.0.tgz#ce698f688c28517447aedf15a9037987e3d2dc97" + integrity sha512-bX+nx5V8XTJEmGtpWTO6kywdS725t71YSLlxWt78XoHUbELWgoCXeOFymRJmL3SU1TLlKSIi7v52EWqe60vJTQ== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-oklab-function@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.0.2.tgz#87cd646e9450347a5721e405b4f7cc35157b7866" + integrity sha512-QwhWesEkMlp4narAwUi6pgc6kcooh8cC7zfxa9LSQNYXqzcdNUtNBzbGc5nuyAVreb7uf5Ox4qH1vYT3GA1wOg== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^1.1.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-progressive-custom-properties@^1.1.0", "@csstools/postcss-progressive-custom-properties@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz#542292558384361776b45c85226b9a3a34f276fa" + integrity sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA== + dependencies: + postcss-value-parser "^4.2.0" + +"@emotion/babel-plugin@^11.7.1": + version "11.7.2" + resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.7.2.tgz#fec75f38a6ab5b304b0601c74e2a5e77c95e5fa0" + integrity sha512-6mGSCWi9UzXut/ZAN6lGFu33wGR3SJisNl3c0tvlmb8XChH1b2SUvxvnOh7hvLpqyRdHHU9AiazV3Cwbk5SXKQ== + dependencies: + "@babel/helper-module-imports" "^7.12.13" + "@babel/plugin-syntax-jsx" "^7.12.13" + "@babel/runtime" "^7.13.10" + "@emotion/hash" "^0.8.0" + "@emotion/memoize" "^0.7.5" + "@emotion/serialize" "^1.0.2" + babel-plugin-macros "^2.6.1" + convert-source-map "^1.5.0" + escape-string-regexp "^4.0.0" + find-root "^1.1.0" + source-map "^0.5.7" + stylis "4.0.13" + +"@emotion/cache@^11.4.0", "@emotion/cache@^11.7.1": + version "11.7.1" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.7.1.tgz#08d080e396a42e0037848214e8aa7bf879065539" + integrity sha512-r65Zy4Iljb8oyjtLeCuBH8Qjiy107dOYC6SJq7g7GV5UCQWMObY4SJDPGFjiiVpPrOJ2hmJOoBiYTC7hwx9E2A== + dependencies: + "@emotion/memoize" "^0.7.4" + "@emotion/sheet" "^1.1.0" + "@emotion/utils" "^1.0.0" + "@emotion/weak-memoize" "^0.2.5" + stylis "4.0.13" + +"@emotion/hash@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" + integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== + +"@emotion/memoize@^0.7.4", "@emotion/memoize@^0.7.5": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.5.tgz#2c40f81449a4e554e9fc6396910ed4843ec2be50" + integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ== + +"@emotion/react@^11.1.1": + version "11.8.2" + resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.8.2.tgz#e51f5e6372e22e82780836c9288da19af4b51e70" + integrity sha512-+1bcHBaNJv5nkIIgnGKVsie3otS0wF9f1T1hteF3WeVvMNQEtfZ4YyFpnphGoot3ilU/wWMgP2SgIDuHLE/wAA== + dependencies: + "@babel/runtime" "^7.13.10" + "@emotion/babel-plugin" "^11.7.1" + "@emotion/cache" "^11.7.1" + "@emotion/serialize" "^1.0.2" + "@emotion/utils" "^1.1.0" + "@emotion/weak-memoize" "^0.2.5" + hoist-non-react-statics "^3.3.1" + +"@emotion/serialize@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.0.2.tgz#77cb21a0571c9f68eb66087754a65fa97bfcd965" + integrity sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A== + dependencies: + "@emotion/hash" "^0.8.0" + "@emotion/memoize" "^0.7.4" + "@emotion/unitless" "^0.7.5" + "@emotion/utils" "^1.0.0" + csstype "^3.0.2" + +"@emotion/sheet@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.1.0.tgz#56d99c41f0a1cda2726a05aa6a20afd4c63e58d2" + integrity sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g== + +"@emotion/unitless@^0.7.5": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" + integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== + +"@emotion/utils@^1.0.0", "@emotion/utils@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.1.0.tgz#86b0b297f3f1a0f2bdb08eeac9a2f49afd40d0cf" + integrity sha512-iRLa/Y4Rs5H/f2nimczYmS5kFJEbpiVvgN3XVfZ022IYhuNA1IRSHEizcof88LtCTXtl9S2Cxt32KgaXEu72JQ== + +"@emotion/weak-memoize@^0.2.5": + version "0.2.5" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" + integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== + +"@eslint/eslintrc@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.1.tgz#8b5e1c49f4077235516bc9ec7d41378c0f69b8c6" + integrity sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.3.1" + globals "^13.9.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.0.4" + strip-json-comments "^3.1.1" + +"@humanwhocodes/config-array@^0.9.2": + version "0.9.5" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.5.tgz#2cbaf9a89460da24b5ca6531b8bbfc23e1df50c7" + integrity sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw== + dependencies: + "@humanwhocodes/object-schema" "^1.2.1" + debug "^4.1.1" + minimatch "^3.0.4" + +"@humanwhocodes/object-schema@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== + +"@istanbuljs/load-nyc-config@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" + 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" + +"@istanbuljs/schema@^0.1.2": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + +"@jest/console@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-27.5.1.tgz#260fe7239602fe5130a94f1aa386eff54b014bba" + 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" + +"@jest/core@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-27.5.1.tgz#267ac5f704e09dc52de2922cbf3af9edcd64b626" + 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" + +"@jest/environment@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-27.5.1.tgz#d7425820511fe7158abbecc010140c3fd3be9c74" + 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" + +"@jest/fake-timers@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-27.5.1.tgz#76979745ce0579c8a94a4678af7a748eda8ada74" + 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" + +"@jest/globals@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-27.5.1.tgz#7ac06ce57ab966566c7963431cef458434601b2b" + integrity sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q== + dependencies: + "@jest/environment" "^27.5.1" + "@jest/types" "^27.5.1" + expect "^27.5.1" + +"@jest/reporters@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-27.5.1.tgz#ceda7be96170b03c923c37987b64015812ffec04" + 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" + +"@jest/source-map@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-27.5.1.tgz#6608391e465add4205eae073b55e7f279e04e8cf" + integrity sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg== + dependencies: + callsites "^3.0.0" + graceful-fs "^4.2.9" + source-map "^0.6.0" + +"@jest/test-result@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-27.5.1.tgz#56a6585fa80f7cdab72b8c5fc2e871d03832f5bb" + 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" + +"@jest/test-sequencer@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz#4057e0e9cea4439e544c6353c6affe58d095745b" + 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" + +"@jest/transform@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.5.1.tgz#6c3501dcc00c4c08915f292a600ece5ecfe1f409" + 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" + +"@jest/types@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.1.tgz#3c79ec4a8ba61c170bf937bcf9e98a9df175ec80" + 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" + +"@jridgewell/resolve-uri@^3.0.3": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz#68eb521368db76d040a6315cdb24bf2483037b9c" + integrity sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.11" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz#771a1d8d744eeb71b6adb35808e1a6c7b9b8c8ec" + integrity sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg== + +"@jridgewell/trace-mapping@^0.3.0": + version "0.3.4" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz#f6a0832dffd5b8a6aaa633b7d9f8e8e94c83a0c3" + integrity sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@pmmmwh/react-refresh-webpack-plugin@^0.5.3": + version "0.5.4" + resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.4.tgz#df0d0d855fc527db48aac93c218a0bf4ada41f99" + integrity sha512-zZbZeHQDnoTlt2AF+diQT0wsSXpvWiaIOZwBRdltNFhG1+I3ozyaw7U/nBiUwyJ0D+zwdXp0E3bWOl38Ag2BMw== + dependencies: + ansi-html-community "^0.0.8" + common-path-prefix "^3.0.0" + core-js-pure "^3.8.1" + error-stack-parser "^2.0.6" + find-up "^5.0.0" + html-entities "^2.1.0" + loader-utils "^2.0.0" + schema-utils "^3.0.0" + source-map "^0.7.3" + +"@rollup/plugin-babel@^5.2.0": + version "5.3.1" + resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283" + integrity sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q== + dependencies: + "@babel/helper-module-imports" "^7.10.4" + "@rollup/pluginutils" "^3.1.0" + +"@rollup/plugin-node-resolve@^11.2.1": + version "11.2.1" + resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz#82aa59397a29cd4e13248b106e6a4a1880362a60" + 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" + +"@rollup/plugin-replace@^2.4.1": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz#a2d539314fbc77c244858faa523012825068510a" + integrity sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg== + dependencies: + "@rollup/pluginutils" "^3.1.0" + magic-string "^0.25.7" + +"@rollup/pluginutils@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b" + integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== + dependencies: + "@types/estree" "0.0.39" + estree-walker "^1.0.1" + picomatch "^2.2.2" + +"@rushstack/eslint-patch@^1.1.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.1.tgz#782fa5da44c4f38ae9fd38e9184b54e451936118" + integrity sha512-BUyKJGdDWqvWC5GEhyOiUrGNi9iJUr4CU0O2WxJL6QJhHeeA/NVBalH+FeK0r/x/W0rPymXt5s78TDS7d6lCwg== + +"@sinonjs/commons@^1.7.0": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" + integrity sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^8.0.1": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz#3fdc2b6cb58935b21bfb8d1625eb1300484316e7" + integrity sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg== + dependencies: + "@sinonjs/commons" "^1.7.0" + +"@surma/rollup-plugin-off-main-thread@^2.2.3": + version "2.2.3" + resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053" + integrity sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ== + dependencies: + ejs "^3.1.6" + json5 "^2.2.0" + magic-string "^0.25.0" + string.prototype.matchall "^4.0.6" + +"@svgr/babel-plugin-add-jsx-attribute@^5.4.0": + version "5.4.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz#81ef61947bb268eb9d50523446f9c638fb355906" + integrity sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg== + +"@svgr/babel-plugin-remove-jsx-attribute@^5.4.0": + version "5.4.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-5.4.0.tgz#6b2c770c95c874654fd5e1d5ef475b78a0a962ef" + integrity sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg== + +"@svgr/babel-plugin-remove-jsx-empty-expression@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-5.0.1.tgz#25621a8915ed7ad70da6cea3d0a6dbc2ea933efd" + integrity sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA== + +"@svgr/babel-plugin-replace-jsx-attribute-value@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-5.0.1.tgz#0b221fc57f9fcd10e91fe219e2cd0dd03145a897" + integrity sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ== + +"@svgr/babel-plugin-svg-dynamic-title@^5.4.0": + version "5.4.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-5.4.0.tgz#139b546dd0c3186b6e5db4fefc26cb0baea729d7" + integrity sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg== + +"@svgr/babel-plugin-svg-em-dimensions@^5.4.0": + version "5.4.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-5.4.0.tgz#6543f69526632a133ce5cabab965deeaea2234a0" + integrity sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw== + +"@svgr/babel-plugin-transform-react-native-svg@^5.4.0": + version "5.4.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz#00bf9a7a73f1cad3948cdab1f8dfb774750f8c80" + integrity sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q== + +"@svgr/babel-plugin-transform-svg-component@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz#583a5e2a193e214da2f3afeb0b9e8d3250126b4a" + integrity sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ== + +"@svgr/babel-preset@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-preset/-/babel-preset-5.5.0.tgz#8af54f3e0a8add7b1e2b0fcd5a882c55393df327" + 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" + +"@svgr/core@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@svgr/core/-/core-5.5.0.tgz#82e826b8715d71083120fe8f2492ec7d7874a579" + integrity sha512-q52VOcsJPvV3jO1wkPtzTuKlvX7Y3xIcWRpCMtBF3MrteZJtBfQw/+u0B1BHy5ColpQc1/YVTrPEtSYIMNZlrQ== + dependencies: + "@svgr/plugin-jsx" "^5.5.0" + camelcase "^6.2.0" + cosmiconfig "^7.0.0" + +"@svgr/hast-util-to-babel-ast@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz#5ee52a9c2533f73e63f8f22b779f93cd432a5461" + integrity sha512-cAaR/CAiZRB8GP32N+1jocovUtvlj0+e65TB50/6Lcime+EA49m/8l+P2ko+XPJ4dw3xaPS3jOL4F2X4KWxoeQ== + dependencies: + "@babel/types" "^7.12.6" + +"@svgr/plugin-jsx@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz#1aa8cd798a1db7173ac043466d7b52236b369000" + 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" + +"@svgr/plugin-svgo@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@svgr/plugin-svgo/-/plugin-svgo-5.5.0.tgz#02da55d85320549324e201c7b2e53bf431fcc246" + integrity sha512-r5swKk46GuQl4RrVejVwpeeJaydoxkdwkM1mBKOgJLBUJPGaLci6ylg/IjhrRsREKDkr4kbMWdgOtbXEh0fyLQ== + dependencies: + cosmiconfig "^7.0.0" + deepmerge "^4.2.2" + svgo "^1.2.2" + +"@svgr/webpack@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@svgr/webpack/-/webpack-5.5.0.tgz#aae858ee579f5fa8ce6c3166ef56c6a1b381b640" + 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" + +"@tootallnate/once@1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" + integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== + +"@trysound/sax@0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" + integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== + +"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14": + version "7.1.19" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.19.tgz#7b497495b7d1b4812bdb9d02804d0576f43ee460" + integrity sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.6.4" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.4.tgz#1f20ce4c5b1990b37900b63f050182d28c2439b7" + integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.1.tgz#3d1a48fd9d6c0edfd56f2ff578daed48f36c8969" + integrity sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6": + version "7.14.2" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.14.2.tgz#ffcd470bbb3f8bf30481678fb5502278ca833a43" + integrity sha512-K2waXdXBi2302XUdcHcR1jCeU0LL4TD9HRs/gk0N2Xvrht+G/BfJa4QObBQZfhMdxiCpV3COl5Nfq4uKTeTnJA== + dependencies: + "@babel/types" "^7.3.0" + +"@types/base16@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@types/base16/-/base16-1.0.2.tgz#eb3a07db52309bfefb9ba010dfdb3c0784971f65" + integrity sha512-oYO/U4VD1DavwrKuCSQWdLG+5K22SLPem2OQaHmFcQuwHoVeGC+JGVRji2MUqZUAIQZHEonOeVfAX09hYiLsdg== + +"@types/body-parser@*": + version "1.19.2" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" + integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/bonjour@^3.5.9": + version "3.5.10" + resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.10.tgz#0f6aadfe00ea414edc86f5d106357cda9701e275" + integrity sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw== + dependencies: + "@types/node" "*" + +"@types/connect-history-api-fallback@^1.3.5": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz#d1f7a8a09d0ed5a57aee5ae9c18ab9b803205dae" + integrity sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw== + dependencies: + "@types/express-serve-static-core" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.35" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" + integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== + dependencies: + "@types/node" "*" + +"@types/eslint-scope@^3.7.3": + version "3.7.3" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.3.tgz#125b88504b61e3c8bc6f870882003253005c3224" + integrity sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "8.4.1" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.4.1.tgz#c48251553e8759db9e656de3efc846954ac32304" + integrity sha512-GE44+DNEyxxh2Kc6ro/VkIj+9ma0pO0bwv9+uHSyBrikYOHr8zYcdPvnBOp1aw8s+CjRvuSx7CyWqRrNFQ59mA== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/eslint@^7.28.2": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.29.0.tgz#e56ddc8e542815272720bb0b4ccc2aff9c3e1c78" + integrity sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*", "@types/estree@^0.0.51": + version "0.0.51" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" + integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== + +"@types/estree@0.0.39": + version "0.0.39" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" + integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== + +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.18": + version "4.17.28" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz#c47def9f34ec81dc6328d0b1b5303d1ec98d86b8" + integrity sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/express@*", "@types/express@^4.17.13": + version "4.17.13" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034" + integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.18" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/graceful-fs@^4.1.2": + version "4.1.5" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" + integrity sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw== + dependencies: + "@types/node" "*" + +"@types/html-minifier-terser@^6.0.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" + integrity sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg== + +"@types/http-proxy@^1.17.8": + version "1.17.8" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.8.tgz#968c66903e7e42b483608030ee85800f22d03f55" + integrity sha512-5kPLG5BKpWYkw/LVOGWpiq3nEVqxiN32rTgI53Sk12/xHFQ2rG3ehI9IO+O3W2QoKeyB92dJkoka8SUm6BX1pA== + dependencies: + "@types/node" "*" + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" + integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== + +"@types/istanbul-lib-report@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" + integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" + integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": + version "7.0.11" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" + integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== + +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= + +"@types/lodash@^4.14.178": + version "4.14.180" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.180.tgz#4ab7c9ddfc92ec4a887886483bc14c79fb380670" + integrity sha512-XOKXa1KIxtNXgASAnwj7cnttJxS4fksBRywK/9LzRV5YxrF80BXZIGeQSuoESQ/VkUj30Ae0+YcuHc15wJCB2g== + +"@types/mime@^1": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" + integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== + +"@types/node@*": + version "17.0.23" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.23.tgz#3b41a6e643589ac6442bdbd7a4a3ded62f33f7da" + integrity sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw== + +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + +"@types/prettier@^2.1.5": + version "2.4.4" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.4.tgz#5d9b63132df54d8909fce1c3f8ca260fdd693e17" + integrity sha512-ReVR2rLTV1kvtlWFyuot+d1pkpG2Fw/XKE3PDAdj57rbM97ttSp9JZ2UsP+2EHTylra9cUf6JA7tGwW1INzUrA== + +"@types/prop-types@*", "@types/prop-types@^15.7.4": + version "15.7.4" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" + integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== + +"@types/q@^1.5.1": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.5.tgz#75a2a8e7d8ab4b230414505d92335d1dcb53a6df" + integrity sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ== + +"@types/qs@*": + version "6.9.7" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" + integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== + +"@types/range-parser@*": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" + integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== + +"@types/react-transition-group@^4.4.0": + version "4.4.4" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.4.tgz#acd4cceaa2be6b757db61ed7b432e103242d163e" + integrity sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug== + dependencies: + "@types/react" "*" + +"@types/react@*": + version "17.0.43" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.43.tgz#4adc142887dd4a2601ce730bc56c3436fdb07a55" + integrity sha512-8Q+LNpdxf057brvPu1lMtC5Vn7J119xrP1aq4qiaefNioQUYANF/CYeK4NsKorSZyUGJ66g0IM+4bbjwx45o2A== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/resolve@1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" + integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw== + dependencies: + "@types/node" "*" + +"@types/retry@^0.12.0": + version "0.12.1" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.1.tgz#d8f1c0d0dc23afad6dc16a9e993a0865774b4065" + integrity sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g== + +"@types/scheduler@*": + version "0.16.2" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" + integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== + +"@types/serve-index@^1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.1.tgz#1b5e85370a192c01ec6cec4735cf2917337a6278" + integrity sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg== + dependencies: + "@types/express" "*" + +"@types/serve-static@*": + version "1.13.10" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9" + integrity sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/sockjs@^0.3.33": + version "0.3.33" + resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.33.tgz#570d3a0b99ac995360e3136fd6045113b1bd236f" + integrity sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw== + dependencies: + "@types/node" "*" + +"@types/stack-utils@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" + integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== + +"@types/trusted-types@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756" + integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg== + +"@types/ws@^8.2.2": + version "8.5.3" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d" + integrity sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w== + dependencies: + "@types/node" "*" + +"@types/yargs-parser@*": + version "21.0.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" + integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== + +"@types/yargs@^16.0.0": + version "16.0.4" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.4.tgz#26aad98dd2c2a38e421086ea9ad42b9e51642977" + integrity sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw== + dependencies: + "@types/yargs-parser" "*" + +"@typescript-eslint/eslint-plugin@^5.5.0": + version "5.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.16.0.tgz#78f246dd8d1b528fc5bfca99a8a64d4023a3d86d" + integrity sha512-SJoba1edXvQRMmNI505Uo4XmGbxCK9ARQpkvOd00anxzri9RNQk0DDCxD+LIl+jYhkzOJiOMMKYEHnHEODjdCw== + dependencies: + "@typescript-eslint/scope-manager" "5.16.0" + "@typescript-eslint/type-utils" "5.16.0" + "@typescript-eslint/utils" "5.16.0" + debug "^4.3.2" + functional-red-black-tree "^1.0.1" + ignore "^5.1.8" + regexpp "^3.2.0" + semver "^7.3.5" + tsutils "^3.21.0" + +"@typescript-eslint/experimental-utils@^5.0.0": + version "5.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.16.0.tgz#0b852567efa10660047f281cf004ed3db32866da" + integrity sha512-bitZtqO13XX64/UOQKoDbVg2H4VHzbHnWWlTRc7ofq7SuQyPCwEycF1Zmn5ZAMTJZ3p5uMS7xJGUdOtZK7LrNw== + dependencies: + "@typescript-eslint/utils" "5.16.0" + +"@typescript-eslint/parser@^5.5.0": + version "5.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.16.0.tgz#e4de1bde4b4dad5b6124d3da227347616ed55508" + integrity sha512-fkDq86F0zl8FicnJtdXakFs4lnuebH6ZADDw6CYQv0UZeIjHvmEw87m9/29nk2Dv5Lmdp0zQ3zDQhiMWQf/GbA== + dependencies: + "@typescript-eslint/scope-manager" "5.16.0" + "@typescript-eslint/types" "5.16.0" + "@typescript-eslint/typescript-estree" "5.16.0" + debug "^4.3.2" + +"@typescript-eslint/scope-manager@5.16.0": + version "5.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.16.0.tgz#7e7909d64bd0c4d8aef629cdc764b9d3e1d3a69a" + integrity sha512-P+Yab2Hovg8NekLIR/mOElCDPyGgFZKhGoZA901Yax6WR6HVeGLbsqJkZ+Cvk5nts/dAlFKm8PfL43UZnWdpIQ== + dependencies: + "@typescript-eslint/types" "5.16.0" + "@typescript-eslint/visitor-keys" "5.16.0" + +"@typescript-eslint/type-utils@5.16.0": + version "5.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.16.0.tgz#b482bdde1d7d7c0c7080f7f2f67ea9580b9e0692" + integrity sha512-SKygICv54CCRl1Vq5ewwQUJV/8padIWvPgCxlWPGO/OgQLCijY9G7lDu6H+mqfQtbzDNlVjzVWQmeqbLMBLEwQ== + dependencies: + "@typescript-eslint/utils" "5.16.0" + debug "^4.3.2" + tsutils "^3.21.0" + +"@typescript-eslint/types@5.16.0": + version "5.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.16.0.tgz#5827b011982950ed350f075eaecb7f47d3c643ee" + integrity sha512-oUorOwLj/3/3p/HFwrp6m/J2VfbLC8gjW5X3awpQJ/bSG+YRGFS4dpsvtQ8T2VNveV+LflQHjlLvB6v0R87z4g== + +"@typescript-eslint/typescript-estree@5.16.0": + version "5.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.16.0.tgz#32259459ec62f5feddca66adc695342f30101f61" + integrity sha512-SE4VfbLWUZl9MR+ngLSARptUv2E8brY0luCdgmUevU6arZRY/KxYoLI/3V/yxaURR8tLRN7bmZtJdgmzLHI6pQ== + dependencies: + "@typescript-eslint/types" "5.16.0" + "@typescript-eslint/visitor-keys" "5.16.0" + debug "^4.3.2" + globby "^11.0.4" + is-glob "^4.0.3" + semver "^7.3.5" + tsutils "^3.21.0" + +"@typescript-eslint/utils@5.16.0", "@typescript-eslint/utils@^5.13.0": + version "5.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.16.0.tgz#42218b459d6d66418a4eb199a382bdc261650679" + integrity sha512-iYej2ER6AwmejLWMWzJIHy3nPJeGDuCqf8Jnb+jAQVoPpmWzwQOfa9hWVB8GIQE5gsCv/rfN4T+AYb/V06WseQ== + dependencies: + "@types/json-schema" "^7.0.9" + "@typescript-eslint/scope-manager" "5.16.0" + "@typescript-eslint/types" "5.16.0" + "@typescript-eslint/typescript-estree" "5.16.0" + eslint-scope "^5.1.1" + eslint-utils "^3.0.0" + +"@typescript-eslint/visitor-keys@5.16.0": + version "5.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.16.0.tgz#f27dc3b943e6317264c7492e390c6844cd4efbbb" + integrity sha512-jqxO8msp5vZDhikTwq9ubyMHqZ67UIvawohr4qF3KhlpL7gzSjOd+8471H3nh5LyABkaI85laEKKU8SnGUK5/g== + dependencies: + "@typescript-eslint/types" "5.16.0" + eslint-visitor-keys "^3.0.0" + +"@webassemblyjs/ast@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" + integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + +"@webassemblyjs/floating-point-hex-parser@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f" + integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ== + +"@webassemblyjs/helper-api-error@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16" + integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg== + +"@webassemblyjs/helper-buffer@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5" + integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA== + +"@webassemblyjs/helper-numbers@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz#64d81da219fbbba1e3bd1bfc74f6e8c4e10a62ae" + 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" + +"@webassemblyjs/helper-wasm-bytecode@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1" + integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q== + +"@webassemblyjs/helper-wasm-section@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz#21ee065a7b635f319e738f0dd73bfbda281c097a" + 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" + +"@webassemblyjs/ieee754@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz#963929e9bbd05709e7e12243a099180812992614" + integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz#ce814b45574e93d76bae1fb2644ab9cdd9527aa5" + integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff" + integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ== + +"@webassemblyjs/wasm-edit@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz#ad206ebf4bf95a058ce9880a8c092c5dec8193d6" + 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" + +"@webassemblyjs/wasm-gen@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76" + 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" + +"@webassemblyjs/wasm-opt@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz#657b4c2202f4cf3b345f8a4c6461c8c2418985f2" + 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" + +"@webassemblyjs/wasm-parser@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz#86ca734534f417e9bd3c67c7a1c75d8be41fb199" + 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" + +"@webassemblyjs/wast-printer@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz#d0c73beda8eec5426f10ae8ef55cee5e7084c2f0" + integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@xtuc/long" "4.2.2" + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +abab@^2.0.3, abab@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" + integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== + +accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +ace-builds@^1.4.13: + version "1.4.14" + resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.4.14.tgz#2c41ccbccdd09e665d3489f161a20baeb3a3c852" + integrity sha512-NBOQlm9+7RBqRqZwimpgquaLeTJFayqb9UEPtTkpC3TkkwDnlsT/TwsCC0svjt9kEZ6G9mH5AEOHSz6Q/HrzQQ== + +acorn-globals@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45" + integrity sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg== + dependencies: + acorn "^7.1.1" + acorn-walk "^7.1.1" + +acorn-import-assertions@^1.7.6: + version "1.8.0" + resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9" + integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== + +acorn-jsx@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn-node@^1.6.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8" + integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A== + dependencies: + acorn "^7.0.0" + acorn-walk "^7.0.0" + xtend "^4.0.2" + +acorn-walk@^7.0.0, acorn-walk@^7.1.1: + version "7.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" + integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== + +acorn@^7.0.0, acorn@^7.1.1: + version "7.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== + +acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.0: + version "8.7.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" + integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== + +address@^1.0.1, address@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6" + integrity sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA== + +adjust-sourcemap-loader@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz#fc4a0fd080f7d10471f30a7320f25560ade28c99" + integrity sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A== + dependencies: + loader-utils "^2.0.0" + regex-parser "^2.2.11" + +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv-keywords@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" + integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== + dependencies: + fast-deep-equal "^3.1.3" + +ajv@^6.10.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.0.0, ajv@^8.6.0, ajv@^8.8.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" + integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +ansi-escapes@^4.2.1, ansi-escapes@^4.3.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-html-community@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" + integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + +anymatch@^3.0.3, anymatch@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +arg@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.1.tgz#eb0c9a8f77786cad2af8ff2b862899842d7b6adb" + integrity sha512-e0hDa9H2Z9AwFkk2qDlwhoMYE4eToKarchkQHovNdLTCYMHZHeRjI71crOh+dio4K6u1IcwubQqo79Ga4CyAQA== + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +aria-query@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" + integrity sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA== + dependencies: + "@babel/runtime" "^7.10.2" + "@babel/runtime-corejs3" "^7.10.2" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + +array-flatten@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" + integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== + +array-includes@^3.1.3, array-includes@^3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.4.tgz#f5b493162c760f3539631f005ba2bb46acb45ba9" + integrity sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.1" + get-intrinsic "^1.1.1" + is-string "^1.0.7" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array.prototype.flat@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz#07e0975d84bbc7c48cd1879d609e682598d33e13" + integrity sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.0" + +array.prototype.flatmap@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.5.tgz#908dc82d8a406930fdf38598d51e7411d18d4446" + integrity sha512-08u6rVyi1Lj7oqWbS9nUxliETrtIROT4XGTA4D/LWGten6E3ocm7cy9SIrmNHOL5XVbVuckUp3X6Xyg8/zpvHA== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + es-abstract "^1.19.0" + +asap@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= + +ast-types-flow@^0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" + integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0= + +async@0.9.x: + version "0.9.2" + resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" + integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0= + +async@^2.6.2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" + integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== + dependencies: + lodash "^4.17.14" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + +autoprefixer@^10.4.4: + version "10.4.4" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.4.tgz#3e85a245b32da876a893d3ac2ea19f01e7ea5a1e" + integrity sha512-Tm8JxsB286VweiZ5F0anmbyGiNI3v3wGv3mz9W+cxEDYB/6jbnj6GM9H9mK3wIL8ftgl+C07Lcwb8PG5PCCPzA== + dependencies: + browserslist "^4.20.2" + caniuse-lite "^1.0.30001317" + fraction.js "^4.2.0" + normalize-range "^0.1.2" + picocolors "^1.0.0" + postcss-value-parser "^4.2.0" + +axe-core@^4.3.5: + version "4.4.1" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.1.tgz#7dbdc25989298f9ad006645cd396782443757413" + integrity sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw== + +axobject-query@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" + integrity sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA== + +babel-jest@^27.4.2, babel-jest@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.1.tgz#a1bf8d61928edfefd21da27eb86a695bfd691444" + 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" + +babel-loader@^8.2.3: + version "8.2.4" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.4.tgz#95f5023c791b2e9e2ca6f67b0984f39c82ff384b" + integrity sha512-8dytA3gcvPPPv4Grjhnt8b5IIiTcq/zeXOPk4iTYI0SVXcsmuGg7JtBRDp8S9X+gJfhQ8ektjXZlDu1Bb33U8A== + dependencies: + find-cache-dir "^3.3.1" + loader-utils "^2.0.0" + make-dir "^3.1.0" + schema-utils "^2.6.5" + +babel-plugin-dynamic-import-node@^2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" + integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== + dependencies: + object.assign "^4.1.0" + +babel-plugin-istanbul@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" + 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" + +babel-plugin-jest-hoist@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz#9be98ecf28c331eb9f5df9c72d6f89deb8181c2e" + 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" + +babel-plugin-macros@^2.6.1: + version "2.8.0" + resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138" + integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg== + dependencies: + "@babel/runtime" "^7.7.2" + cosmiconfig "^6.0.0" + resolve "^1.12.0" + +babel-plugin-macros@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1" + integrity sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg== + dependencies: + "@babel/runtime" "^7.12.5" + cosmiconfig "^7.0.0" + resolve "^1.19.0" + +babel-plugin-named-asset-import@^0.3.8: + version "0.3.8" + resolved "https://registry.yarnpkg.com/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.8.tgz#6b7fa43c59229685368683c28bc9734f24524cc2" + integrity sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q== + +babel-plugin-polyfill-corejs2@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.1.tgz#440f1b70ccfaabc6b676d196239b138f8a2cfba5" + integrity sha512-v7/T6EQcNfVLfcN2X8Lulb7DjprieyLWJK/zOWH5DUYcAgex9sP3h25Q+DLsX9TloXe3y1O8l2q2Jv9q8UVB9w== + dependencies: + "@babel/compat-data" "^7.13.11" + "@babel/helper-define-polyfill-provider" "^0.3.1" + semver "^6.1.1" + +babel-plugin-polyfill-corejs3@^0.5.0: + version "0.5.2" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.2.tgz#aabe4b2fa04a6e038b688c5e55d44e78cd3a5f72" + integrity sha512-G3uJih0XWiID451fpeFaYGVuxHEjzKTHtc9uGFEjR6hHrvNzeS/PX+LLLcetJcytsB5m4j+K3o/EpXJNb/5IEQ== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.3.1" + core-js-compat "^3.21.0" + +babel-plugin-polyfill-regenerator@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz#2c0678ea47c75c8cc2fbb1852278d8fb68233990" + integrity sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.3.1" + +babel-plugin-transform-react-remove-prop-types@^0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz#f2edaf9b4c6a5fbe5c1d678bfb531078c1555f3a" + integrity sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA== + +babel-preset-current-node-syntax@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" + 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" + +babel-preset-jest@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz#91f10f58034cb7989cb4f962b69fa6eef6a6bc81" + integrity sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag== + dependencies: + babel-plugin-jest-hoist "^27.5.1" + babel-preset-current-node-syntax "^1.0.0" + +babel-preset-react-app@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/babel-preset-react-app/-/babel-preset-react-app-10.0.1.tgz#ed6005a20a24f2c88521809fa9aea99903751584" + 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" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base16@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/base16/-/base16-1.0.0.tgz#e297f60d7ec1014a7a971a39ebc8a98c0b681e70" + integrity sha1-4pf2DX7BAUp6lxo568ipjAtoHnA= + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY= + +bfj@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/bfj/-/bfj-7.0.2.tgz#1988ce76f3add9ac2913fd8ba47aad9e651bfbb2" + integrity sha512-+e/UqUzwmzJamNF50tBV6tZPTORow7gQ96iFow+8b562OdMpEK0BcJEq2OSPEDmAbSMBQ7PKZ87ubFkgxpYWgw== + dependencies: + bluebird "^3.5.5" + check-types "^11.1.1" + hoopy "^0.1.4" + tryer "^1.0.1" + +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +bluebird@^3.5.5: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +body-parser@1.19.2: + version "1.19.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.2.tgz#4714ccd9c157d44797b8b5607d72c0b89952f26e" + integrity sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.8.1" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.9.7" + raw-body "2.4.3" + type-is "~1.6.18" + +bonjour@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" + integrity sha1-jokKGD2O6aI5OzhExpGkK897yfU= + dependencies: + array-flatten "^2.1.0" + deep-equal "^1.0.1" + dns-equal "^1.0.0" + dns-txt "^2.0.2" + multicast-dns "^6.0.1" + multicast-dns-service-types "^1.1.0" + +boolbase@^1.0.0, boolbase@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.2, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browser-process-hrtime@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" + integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== + +browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.16.6, browserslist@^4.17.5, browserslist@^4.18.1, browserslist@^4.19.1, browserslist@^4.20.2: + version "4.20.2" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.2.tgz#567b41508757ecd904dab4d1c646c612cd3d4f88" + integrity sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA== + dependencies: + caniuse-lite "^1.0.30001317" + electron-to-chromium "^1.4.84" + escalade "^3.1.1" + node-releases "^2.0.2" + picocolors "^1.0.0" + +bser@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== + dependencies: + node-int64 "^0.4.0" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer-indexof@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" + integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== + +builtin-modules@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.2.0.tgz#45d5db99e7ee5e6bc4f362e008bf917ab5049887" + integrity sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA== + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.0, call-bind@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camel-case@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" + integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== + dependencies: + pascal-case "^3.1.2" + tslib "^2.0.3" + +camelcase-css@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" + integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== + +camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^6.2.0, camelcase@^6.2.1: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-api@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" + 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" + +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001317: + version "1.0.30001320" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001320.tgz#8397391bec389b8ccce328636499b7284ee13285" + integrity sha512-MWPzG54AGdo3nWx7zHZTefseM5Y1ccM7hlQKHRqJkPozUaw3hNbBTMmLn16GG2FUzjR13Cr3NPfhIieX5PzXDA== + +case-sensitive-paths-webpack-plugin@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz#db64066c6422eed2e08cc14b986ca43796dbc6d4" + integrity sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw== + +chalk@^2.0.0, chalk@^2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + +char-regex@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-2.0.1.tgz#6dafdb25f9d3349914079f010ba8d0e6ff9cd01e" + integrity sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw== + +charcodes@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/charcodes/-/charcodes-0.2.0.tgz#5208d327e6cc05f99eb80ffc814707572d1f14e4" + integrity sha512-Y4kiDb+AM4Ecy58YkuZrrSRJBDQdQ2L+NyS1vHHFtNtUjgutcZfx3yp1dAONI/oPaPmyGfCLx5CxL+zauIMyKQ== + +check-types@^11.1.1: + version "11.1.2" + resolved "https://registry.yarnpkg.com/check-types/-/check-types-11.1.2.tgz#86a7c12bf5539f6324eb0e70ca8896c0e38f3e2f" + integrity sha512-tzWzvgePgLORb9/3a0YenggReLKAIb2owL03H2Xdoe5pKcUyWRSEQ8xfCar8t2SIAuEDwtmx2da1YB52YuHQMQ== + +"chokidar@>=3.0.0 <4.0.0", chokidar@^3.4.2, chokidar@^3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + 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" + optionalDependencies: + fsevents "~2.3.2" + +chrome-trace-event@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" + integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== + +ci-info@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.0.tgz#b4ed1fb6818dea4803a55c623041f9165d2066b2" + integrity sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw== + +cjs-module-lexer@^1.0.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" + integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== + +classnames@^2.2.5: + version "2.3.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" + integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== + +clean-css@^5.2.2: + version "5.2.4" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.2.4.tgz#982b058f8581adb2ae062520808fb2429bd487a4" + integrity sha512-nKseG8wCzEuji/4yrgM/5cthL9oTDc5UOQyFMvW/Q53oP6gLH690o1NbuTh6Y18nujr7BxlsFuS7gXLnLzKJGg== + dependencies: + source-map "~0.6.0" + +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= + +coa@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3" + integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA== + dependencies: + "@types/q" "^1.5.1" + chalk "^2.4.1" + q "^1.1.2" + +collect-v8-coverage@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" + integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg== + +color-convert@^1.9.0, color-convert@^1.9.3: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-string@^1.6.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.0.tgz#63b6ebd1bec11999d1df3a79a7569451ac2be8aa" + integrity sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164" + integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== + dependencies: + color-convert "^1.9.3" + color-string "^1.6.0" + +colord@^2.9.1: + version "2.9.2" + resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.2.tgz#25e2bacbbaa65991422c07ea209e2089428effb1" + integrity sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ== + +colorette@^2.0.10: + version "2.0.16" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da" + integrity sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + +commander@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" + integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== + +common-path-prefix@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/common-path-prefix/-/common-path-prefix-3.0.0.tgz#7d007a7e07c58c4b4d5f433131a19141b29f11e0" + integrity sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w== + +common-tags@^1.8.0: + version "1.8.2" + resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" + integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= + +compressible@~2.0.16: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + 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" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +confusing-browser-globals@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81" + integrity sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA== + +connect-history-api-fallback@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" + integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" + integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== + dependencies: + safe-buffer "~5.1.1" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== + +core-js-compat@^3.20.2, core-js-compat@^3.21.0: + version "3.21.1" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.21.1.tgz#cac369f67c8d134ff8f9bd1623e3bc2c42068c82" + integrity sha512-gbgX5AUvMb8gwxC7FLVWYT7Kkgu/y7+h/h1X43yJkNqhlK2fuYyQimqvKGNZFAY6CKii/GFKJ2cp/1/42TN36g== + dependencies: + browserslist "^4.19.1" + semver "7.0.0" + +core-js-pure@^3.20.2, core-js-pure@^3.8.1: + version "3.21.1" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.21.1.tgz#8c4d1e78839f5f46208de7230cebfb72bc3bdb51" + integrity sha512-12VZfFIu+wyVbBebyHmRTuEE/tZrB4tJToWcwAMcsp3h4+sHR+fMJWbKpYiCRWlhFBq+KNyO8rIV9rTkeVmznQ== + +core-js@^3.19.2: + version "3.21.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.21.1.tgz#f2e0ddc1fc43da6f904706e8e955bc19d06a0d94" + integrity sha512-FRq5b/VMrWlrmCzwRrpDYNxyHP9BcAZC+xHJaqTgIE5091ZV1NTmyh0sGOg5XqpnHvR0svdy0sv1gWA1zmhxig== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cosmiconfig@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" + 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" + +cosmiconfig@^7.0.0, cosmiconfig@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d" + integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + +cross-spawn@^7.0.2, cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +crypto-random-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" + integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== + +css-blank-pseudo@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz#36523b01c12a25d812df343a32c322d2a2324561" + integrity sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ== + dependencies: + postcss-selector-parser "^6.0.9" + +css-declaration-sorter@^6.0.3: + version "6.1.4" + resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.1.4.tgz#b9bfb4ed9a41f8dcca9bf7184d849ea94a8294b4" + integrity sha512-lpfkqS0fctcmZotJGhnxkIyJWvBXgpyi2wsFd4J8VB7wzyrT6Ch/3Q+FMNJpjK4gu1+GN5khOnpU2ZVKrLbhCw== + dependencies: + timsort "^0.3.0" + +css-has-pseudo@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz#57f6be91ca242d5c9020ee3e51bbb5b89fc7af73" + integrity sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw== + dependencies: + postcss-selector-parser "^6.0.9" + +css-loader@^6.5.1: + version "6.7.1" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.7.1.tgz#e98106f154f6e1baf3fc3bc455cb9981c1d5fd2e" + integrity sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw== + dependencies: + icss-utils "^5.1.0" + postcss "^8.4.7" + 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.5" + +css-minimizer-webpack-plugin@^3.2.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.4.1.tgz#ab78f781ced9181992fe7b6e4f3422e76429878f" + 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" + +css-prefers-color-scheme@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz#ca8a22e5992c10a5b9d315155e7caee625903349" + integrity sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA== + +css-select-base-adapter@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" + integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w== + +css-select@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.1.0.tgz#6a34653356635934a81baca68d0255432105dbef" + integrity sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ== + dependencies: + boolbase "^1.0.0" + css-what "^3.2.1" + domutils "^1.7.0" + nth-check "^1.0.2" + +css-select@^4.1.3: + version "4.2.1" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.2.1.tgz#9e665d6ae4c7f9d65dbe69d0316e3221fb274cdd" + integrity sha512-/aUslKhzkTNCQUB2qTX84lVmfia9NyjP3WpDGtj/WxhwBzWBYUV3DgUpurHTme8UTPcPlAD1DJ+b0nN/t50zDQ== + dependencies: + boolbase "^1.0.0" + css-what "^5.1.0" + domhandler "^4.3.0" + domutils "^2.8.0" + nth-check "^2.0.1" + +css-tree@1.0.0-alpha.37: + version "1.0.0-alpha.37" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22" + integrity sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg== + dependencies: + mdn-data "2.0.4" + source-map "^0.6.1" + +css-tree@^1.1.2, css-tree@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" + integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== + dependencies: + mdn-data "2.0.14" + source-map "^0.6.1" + +css-what@^3.2.1: + version "3.4.2" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" + integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ== + +css-what@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.1.0.tgz#3f7b707aadf633baf62c2ceb8579b545bb40f7fe" + integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw== + +cssdb@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-6.5.0.tgz#61264b71f29c834f09b59cb3e5b43c8226590122" + integrity sha512-Rh7AAopF2ckPXe/VBcoUS9JrCZNSyc60+KpgE6X25vpVxA32TmiqvExjkfhwP4wGSb6Xe8Z/JIyGqwgx/zZYFA== + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +cssnano-preset-default@^5.2.5: + version "5.2.5" + resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.2.5.tgz#267ded811a3e1664d78707f5355fcd89feeb38ac" + integrity sha512-WopL7PzN7sos3X8B54/QGl+CZUh1f0qN4ds+y2d5EPwRSSc3jsitVw81O+Uyop0pXyOfPfZxnc+LmA8w/Ki/WQ== + dependencies: + css-declaration-sorter "^6.0.3" + cssnano-utils "^3.1.0" + postcss-calc "^8.2.3" + postcss-colormin "^5.3.0" + postcss-convert-values "^5.1.0" + postcss-discard-comments "^5.1.1" + postcss-discard-duplicates "^5.1.0" + postcss-discard-empty "^5.1.1" + postcss-discard-overridden "^5.1.0" + postcss-merge-longhand "^5.1.3" + postcss-merge-rules "^5.1.1" + postcss-minify-font-values "^5.1.0" + postcss-minify-gradients "^5.1.1" + postcss-minify-params "^5.1.2" + postcss-minify-selectors "^5.2.0" + postcss-normalize-charset "^5.1.0" + postcss-normalize-display-values "^5.1.0" + postcss-normalize-positions "^5.1.0" + postcss-normalize-repeat-style "^5.1.0" + postcss-normalize-string "^5.1.0" + postcss-normalize-timing-functions "^5.1.0" + postcss-normalize-unicode "^5.1.0" + postcss-normalize-url "^5.1.0" + postcss-normalize-whitespace "^5.1.1" + postcss-ordered-values "^5.1.1" + postcss-reduce-initial "^5.1.0" + postcss-reduce-transforms "^5.1.0" + postcss-svgo "^5.1.0" + postcss-unique-selectors "^5.1.1" + +cssnano-utils@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-3.1.0.tgz#95684d08c91511edfc70d2636338ca37ef3a6861" + integrity sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA== + +cssnano@^5.0.6: + version "5.1.5" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.1.5.tgz#5f3f519538c7f1c182c527096892243db3e17397" + integrity sha512-VZO1e+bRRVixMeia1zKagrv0lLN1B/r/u12STGNNUFxnp97LIFgZHQa0JxqlwEkvzUyA9Oz/WnCTAFkdEbONmg== + dependencies: + cssnano-preset-default "^5.2.5" + lilconfig "^2.0.3" + yaml "^1.10.2" + +csso@^4.0.2, csso@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" + integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== + dependencies: + css-tree "^1.1.2" + +cssom@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" + integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== + +cssom@~0.3.6: + version "0.3.8" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" + integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== + +cssstyle@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852" + integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== + dependencies: + cssom "~0.3.6" + +csstype@^3.0.10, csstype@^3.0.2: + version "3.0.11" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.11.tgz#d66700c5eacfac1940deb4e3ee5642792d85cd33" + integrity sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw== + +damerau-levenshtein@^1.0.7: + version "1.0.8" + resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" + integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== + +data-urls@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" + integrity sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ== + dependencies: + abab "^2.0.3" + whatwg-mimetype "^2.3.0" + whatwg-url "^8.0.0" + +debug@2.6.9, debug@^2.6.0, debug@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +debug@^3.1.1, debug@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +decimal.js@^10.2.1: + version "10.3.1" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" + integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== + +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= + +deep-equal@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" + integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g== + dependencies: + is-arguments "^1.0.4" + is-date-object "^1.0.1" + is-regex "^1.0.4" + object-is "^1.0.1" + object-keys "^1.1.1" + regexp.prototype.flags "^1.2.0" + +deep-is@^0.1.3, deep-is@~0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +deepmerge@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" + integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== + +default-gateway@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71" + integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg== + dependencies: + execa "^5.0.0" + +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + +define-properties@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + dependencies: + object-keys "^1.0.12" + +defined@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" + integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM= + +del@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/del/-/del-6.0.0.tgz#0b40d0332cea743f1614f818be4feb717714c952" + integrity sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ== + dependencies: + globby "^11.0.1" + graceful-fs "^4.2.4" + is-glob "^4.0.1" + is-path-cwd "^2.2.0" + is-path-inside "^3.0.2" + p-map "^4.0.0" + rimraf "^3.0.2" + slash "^3.0.0" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + +detect-newline@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" + integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== + +detect-node@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== + +detect-port-alt@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/detect-port-alt/-/detect-port-alt-1.1.6.tgz#24707deabe932d4a3cf621302027c2b266568275" + integrity sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q== + dependencies: + address "^1.0.1" + debug "^2.6.0" + +detective@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.0.tgz#feb2a77e85b904ecdea459ad897cc90a99bd2a7b" + integrity sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg== + dependencies: + acorn-node "^1.6.1" + defined "^1.0.0" + minimist "^1.1.1" + +didyoumean@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" + integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== + +diff-match-patch@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37" + integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw== + +diff-sequences@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" + integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +dlv@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" + integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== + +dns-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" + integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0= + +dns-packet@^1.3.1: + version "1.3.4" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.4.tgz#e3455065824a2507ba886c55a89963bb107dec6f" + integrity sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA== + dependencies: + ip "^1.1.0" + safe-buffer "^5.0.1" + +dns-txt@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" + integrity sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY= + dependencies: + buffer-indexof "^1.0.0" + +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dom-converter@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" + integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== + dependencies: + utila "~0.4" + +dom-helpers@^5.0.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + +dom-serializer@0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" + integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== + dependencies: + domelementtype "^2.0.1" + entities "^2.0.0" + +dom-serializer@^1.0.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91" + integrity sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.2.0" + entities "^2.0.0" + +domelementtype@1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" + integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== + +domelementtype@^2.0.1, domelementtype@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" + integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== + +domexception@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304" + integrity sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg== + dependencies: + webidl-conversions "^5.0.0" + +domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" + integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== + dependencies: + domelementtype "^2.2.0" + +domutils@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" + integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== + dependencies: + dom-serializer "0" + domelementtype "1" + +domutils@^2.5.2, domutils@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" + integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + +dot-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" + integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + +dotenv-expand@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0" + integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA== + +dotenv@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" + integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== + +duplexer@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" + integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +ejs@^3.1.6: + version "3.1.6" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.6.tgz#5bfd0a0689743bb5268b3550cceeebbc1702822a" + integrity sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw== + dependencies: + jake "^10.6.1" + +electron-to-chromium@^1.4.84: + version "1.4.93" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.93.tgz#2e87ac28721cb31d472ec2bd04f7daf9f2e13de2" + integrity sha512-ywq9Pc5Gwwpv7NG767CtoU8xF3aAUQJjH9//Wy3MBCg4w5JSLbJUq2L8IsCdzPMjvSgxuue9WcVaTOyyxCL0aQ== + +emittery@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860" + integrity sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +emojis-list@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" + integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + +enhanced-resolve@^5.9.2: + version "5.9.2" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz#0224dcd6a43389ebfb2d55efee517e5466772dd9" + integrity sha512-GIm3fQfwLJ8YZx2smuHpBKkXC1yOk+OBEmKckVyL0i/ea8mqDEykK3ld5dgH1QYPNyT/lIllxV2LULnxCHaHkA== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +error-stack-parser@^2.0.6: + version "2.0.7" + resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.7.tgz#b0c6e2ce27d0495cf78ad98715e0cad1219abb57" + integrity sha512-chLOW0ZGRf4s8raLrDxa5sdkvPec5YdvwbFnqJme4rk0rFajP8mPtrDL1+I+CwrQDCjswDA5sREX7jYQDQs9vA== + dependencies: + stackframe "^1.1.1" + +es-abstract@^1.17.2, es-abstract@^1.19.0, es-abstract@^1.19.1: + version "1.19.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3" + integrity sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w== + dependencies: + call-bind "^1.0.2" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + get-intrinsic "^1.1.1" + get-symbol-description "^1.0.0" + has "^1.0.3" + has-symbols "^1.0.2" + internal-slot "^1.0.3" + is-callable "^1.2.4" + is-negative-zero "^2.0.1" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.1" + is-string "^1.0.7" + is-weakref "^1.0.1" + object-inspect "^1.11.0" + object-keys "^1.1.1" + object.assign "^4.1.2" + string.prototype.trimend "^1.0.4" + string.prototype.trimstart "^1.0.4" + unbox-primitive "^1.0.1" + +es-module-lexer@^0.9.0: + version "0.9.3" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" + integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +escodegen@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" + integrity sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw== + dependencies: + esprima "^4.0.1" + estraverse "^5.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + +eslint-config-react-app@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-7.0.0.tgz#0fa96d5ec1dfb99c029b1554362ab3fa1c3757df" + integrity sha512-xyymoxtIt1EOsSaGag+/jmcywRuieQoA2JbPCjnw9HukFj9/97aGPoZVFioaotzk1K5Qt9sHO5EutZbkrAXS0g== + 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-import-resolver-node@^0.3.6: + version "0.3.6" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz#4048b958395da89668252001dbd9eca6b83bacbd" + integrity sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw== + dependencies: + debug "^3.2.7" + resolve "^1.20.0" + +eslint-module-utils@^2.7.2: + version "2.7.3" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz#ad7e3a10552fdd0642e1e55292781bd6e34876ee" + integrity sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ== + dependencies: + debug "^3.2.7" + find-up "^2.1.0" + +eslint-plugin-flowtype@^8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz#e1557e37118f24734aa3122e7536a038d34a4912" + integrity sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ== + dependencies: + lodash "^4.17.21" + string-natural-compare "^3.0.1" + +eslint-plugin-import@^2.25.3: + version "2.25.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz#322f3f916a4e9e991ac7af32032c25ce313209f1" + integrity sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA== + dependencies: + array-includes "^3.1.4" + array.prototype.flat "^1.2.5" + debug "^2.6.9" + doctrine "^2.1.0" + eslint-import-resolver-node "^0.3.6" + eslint-module-utils "^2.7.2" + has "^1.0.3" + is-core-module "^2.8.0" + is-glob "^4.0.3" + minimatch "^3.0.4" + object.values "^1.1.5" + resolve "^1.20.0" + tsconfig-paths "^3.12.0" + +eslint-plugin-jest@^25.3.0: + version "25.7.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz#ff4ac97520b53a96187bad9c9814e7d00de09a6a" + integrity sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ== + dependencies: + "@typescript-eslint/experimental-utils" "^5.0.0" + +eslint-plugin-jsx-a11y@^6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.5.1.tgz#cdbf2df901040ca140b6ec14715c988889c2a6d8" + integrity sha512-sVCFKX9fllURnXT2JwLN5Qgo24Ug5NF6dxhkmxsMEUZhXRcGg+X3e1JbJ84YePQKBl5E0ZjAH5Q4rkdcGY99+g== + dependencies: + "@babel/runtime" "^7.16.3" + aria-query "^4.2.2" + array-includes "^3.1.4" + ast-types-flow "^0.0.7" + axe-core "^4.3.5" + axobject-query "^2.2.0" + damerau-levenshtein "^1.0.7" + emoji-regex "^9.2.2" + has "^1.0.3" + jsx-ast-utils "^3.2.1" + language-tags "^1.0.5" + minimatch "^3.0.4" + +eslint-plugin-react-hooks@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz#318dbf312e06fab1c835a4abef00121751ac1172" + integrity sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA== + +eslint-plugin-react@^7.27.1: + version "7.29.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.29.4.tgz#4717de5227f55f3801a5fd51a16a4fa22b5914d2" + integrity sha512-CVCXajliVh509PcZYRFyu/BoUEz452+jtQJq2b3Bae4v3xBUWPLCmtmBM+ZinG4MzwmxJgJ2M5rMqhqLVn7MtQ== + dependencies: + array-includes "^3.1.4" + array.prototype.flatmap "^1.2.5" + doctrine "^2.1.0" + estraverse "^5.3.0" + jsx-ast-utils "^2.4.1 || ^3.0.0" + minimatch "^3.1.2" + object.entries "^1.1.5" + object.fromentries "^2.0.5" + object.hasown "^1.1.0" + object.values "^1.1.5" + prop-types "^15.8.1" + resolve "^2.0.0-next.3" + semver "^6.3.0" + string.prototype.matchall "^4.0.6" + +eslint-plugin-testing-library@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.1.0.tgz#6ad539a53d4e897d3045902f8e534e07cebd4e8b" + integrity sha512-YSNzasJUbyhOTe14ZPygeOBvcPvcaNkwHwrj4vdf+uirr2D32JTDaKi6CP5Os2aWtOcvt4uBSPXp9h5xGoqvWQ== + dependencies: + "@typescript-eslint/utils" "^5.13.0" + +eslint-scope@5.1.1, eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-scope@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642" + integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" + integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== + dependencies: + eslint-visitor-keys "^2.0.0" + +eslint-visitor-keys@^2.0.0, eslint-visitor-keys@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" + integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== + +eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" + integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== + +eslint-webpack-plugin@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/eslint-webpack-plugin/-/eslint-webpack-plugin-3.1.1.tgz#83dad2395e5f572d6f4d919eedaa9cf902890fcb" + integrity sha512-xSucskTN9tOkfW7so4EaiFIkulWLXwCB/15H917lR6pTv0Zot6/fetFucmENRb7J5whVSFKIvwnrnsa78SG2yg== + dependencies: + "@types/eslint" "^7.28.2" + jest-worker "^27.3.1" + micromatch "^4.0.4" + normalize-path "^3.0.0" + schema-utils "^3.1.1" + +eslint@^8.3.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.11.0.tgz#88b91cfba1356fc10bb9eb592958457dfe09fb37" + integrity sha512-/KRpd9mIRg2raGxHRGwW9ZywYNAClZrHjdueHcrVDuO3a6bj83eoTirCCk0M0yPwOjWYKHwRVRid+xK4F/GHgA== + dependencies: + "@eslint/eslintrc" "^1.2.1" + "@humanwhocodes/config-array" "^0.9.2" + ajv "^6.10.0" + 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-utils "^3.0.0" + eslint-visitor-keys "^3.3.0" + espree "^9.3.1" + esquery "^1.4.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^6.0.1" + globals "^13.6.0" + ignore "^5.2.0" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.0.4" + natural-compare "^1.4.0" + optionator "^0.9.1" + regexpp "^3.2.0" + strip-ansi "^6.0.1" + strip-json-comments "^3.1.0" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +espree@^9.3.1: + version "9.3.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.1.tgz#8793b4bc27ea4c778c19908e0719e7b8f4115bcd" + integrity sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ== + dependencies: + acorn "^8.7.0" + acorn-jsx "^5.3.1" + eslint-visitor-keys "^3.3.0" + +esprima@^4.0.0, esprima@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esquery@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" + integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +estree-walker@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" + integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + 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" + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= + +expect@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/expect/-/expect-27.5.1.tgz#83ce59f1e5bdf5f9d2b94b61d2050db48f3fef74" + 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" + +express@^4.17.1: + version "4.17.3" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.3.tgz#f6c7302194a4fb54271b73a1fe7a06478c8f85a1" + integrity sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.19.2" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.4.2" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.1.2" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.9.7" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.17.2" + serve-static "1.14.2" + setprototypeof "1.2.0" + statuses "~1.5.0" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.2.11, fast-glob@^3.2.9: + version "3.2.11" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" + integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +fastq@^1.6.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" + integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== + dependencies: + reusify "^1.0.4" + +faye-websocket@^0.11.3: + version "0.11.4" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + +fb-watchman@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85" + integrity sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg== + dependencies: + bser "2.1.1" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +file-loader@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d" + integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + +filelist@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.2.tgz#80202f21462d4d1c2e214119b1807c1bc0380e5b" + integrity sha512-z7O0IS8Plc39rTCq6i6iHxk43duYOn8uFJiWSewIq0Bww1RNybVHSCjahmcC87ZqAm4OTvFzlzeGu3XAzG1ctQ== + dependencies: + minimatch "^3.0.4" + +filesize@^8.0.6: + version "8.0.7" + resolved "https://registry.yarnpkg.com/filesize/-/filesize-8.0.7.tgz#695e70d80f4e47012c132d57a059e80c6b580bd8" + integrity sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ== + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +find-cache-dir@^3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" + integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== + dependencies: + commondir "^1.0.1" + make-dir "^3.0.2" + pkg-dir "^4.1.0" + +find-root@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" + integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== + +find-up@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= + dependencies: + locate-path "^2.0.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +find-up@^4.0.0, find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + +flatted@^3.1.0: + version "3.2.5" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3" + integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== + +follow-redirects@^1.0.0: + version "1.14.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" + integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== + +fork-ts-checker-webpack-plugin@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.0.tgz#0282b335fa495a97e167f69018f566ea7d2a2b5e" + integrity sha512-cS178Y+xxtIjEUorcHddKS7yCMlrDPV31mt47blKKRfMd70Kxu5xruAFE2o9sDY6wVC5deuob/u/alD04YYHnw== + 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" + +form-data@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" + integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fraction.js@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950" + integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + +fs-extra@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.1.tgz#27de43b4320e833f6867cc044bfce29fdf0ef3b8" + integrity sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-extra@^9.0.0, fs-extra@^9.0.1: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + 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" + +fs-monkey@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.3.tgz#ae3ac92d53bb328efe0e9a1d9541f6ad8d48e2d3" + integrity sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fsevents@^2.3.2, fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" + integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + +get-own-enumerable-property-symbols@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" + integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== + +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.1, glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + 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" + +global-modules@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" + integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== + dependencies: + global-prefix "^3.0.0" + +global-prefix@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" + integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== + dependencies: + ini "^1.3.5" + kind-of "^6.0.2" + which "^1.3.1" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globals@^13.6.0, globals@^13.9.0: + version "13.13.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.13.0.tgz#ac32261060d8070e2719dd6998406e27d2b5727b" + integrity sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A== + dependencies: + type-fest "^0.20.2" + +globby@^11.0.1, globby@^11.0.4: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + 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" + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: + version "4.2.9" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" + integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== + +gzip-size@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462" + integrity sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q== + dependencies: + duplexer "^0.1.2" + +handle-thing@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" + integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== + +harmony-reflect@^1.4.6: + version "1.6.2" + resolved "https://registry.yarnpkg.com/harmony-reflect/-/harmony-reflect-1.6.2.tgz#31ecbd32e648a34d030d86adb67d4d47547fe710" + integrity sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g== + +has-bigints@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" + integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +he@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +history@^4.9.0: + version "4.10.1" + resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" + integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== + dependencies: + "@babel/runtime" "^7.1.2" + loose-envify "^1.2.0" + resolve-pathname "^3.0.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + value-equal "^1.0.1" + +hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + +hoopy@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/hoopy/-/hoopy-0.1.4.tgz#609207d661100033a9a9402ad3dea677381c1b1d" + integrity sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ== + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + integrity sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI= + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +html-encoding-sniffer@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" + integrity sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ== + dependencies: + whatwg-encoding "^1.0.5" + +html-entities@^2.1.0, html-entities@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.2.tgz#760b404685cb1d794e4f4b744332e3b00dcfe488" + integrity sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ== + +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + +html-minifier-terser@^6.0.2: + version "6.1.0" + resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#bfc818934cc07918f6b3669f5774ecdfd48f32ab" + 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" + +html-webpack-plugin@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz#c3911936f57681c1f9f4d8b68c158cd9dfe52f50" + 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" + +htmlparser2@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" + integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.0.0" + domutils "^2.5.2" + entities "^2.0.0" + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc= + +http-errors@1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c" + integrity sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.1" + +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-parser-js@>=0.5.1: + version "0.5.6" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.6.tgz#2e02406ab2df8af8a7abfba62e0da01c62b95afd" + integrity sha512-vDlkRPDJn93swjcjqMSaGSPABbIarsr1TLAui/gLDXzV5VsJNdXNzMYDyNBLQkjWQCJ1uizu8T2oDMhmGt0PRA== + +http-proxy-agent@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" + integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== + dependencies: + "@tootallnate/once" "1" + agent-base "6" + debug "4" + +http-proxy-middleware@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz#03af0f4676d172ae775cb5c33f592f40e1a4e07a" + integrity sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg== + 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" + +http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +https-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" + integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== + dependencies: + agent-base "6" + debug "4" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +icss-utils@^5.0.0, icss-utils@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" + integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== + +idb@^6.1.4: + version "6.1.5" + resolved "https://registry.yarnpkg.com/idb/-/idb-6.1.5.tgz#dbc53e7adf1ac7c59f9b2bf56e00b4ea4fce8c7b" + integrity sha512-IJtugpKkiVXQn5Y+LteyBCNk1N8xpGV3wWZk9EVtZWH8DYkjBn0bX1XnGP9RkyZF0sAcywa6unHqSWKe7q4LGw== + +identity-obj-proxy@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz#94d2bda96084453ef36fbc5aaec37e0f79f1fc14" + integrity sha1-lNK9qWCERT7zb7xarsN+D3nx/BQ= + dependencies: + harmony-reflect "^1.4.6" + +ignore@^5.1.8, ignore@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" + integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== + +immer@^9.0.7: + version "9.0.12" + resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.12.tgz#2d33ddf3ee1d247deab9d707ca472c8c942a0f20" + integrity sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA== + +immutable@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23" + integrity sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw== + +import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +import-local@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +ini@^1.3.5: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +internal-slot@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" + integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== + dependencies: + get-intrinsic "^1.1.0" + has "^1.0.3" + side-channel "^1.0.4" + +ip@^1.1.0: + version "1.1.5" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" + integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +ipaddr.js@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0" + integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== + +is-arguments@^1.0.4: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-callable@^1.1.4, is-callable@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" + integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== + +is-core-module@^2.2.0, is-core-module@^2.8.0, is-core-module@^2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" + integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== + dependencies: + has "^1.0.3" + +is-date-object@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" + integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= + +is-negative-zero@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" + integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== + +is-number-object@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0" + integrity sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g== + dependencies: + has-tostringtag "^1.0.0" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= + +is-path-cwd@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" + integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== + +is-path-inside@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-plain-obj@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" + integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== + +is-potential-custom-element-name@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" + integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== + +is-regex@^1.0.4, is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" + integrity sha1-/S2INUXEa6xaYz57mgnof6LLUGk= + +is-root@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c" + integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg== + +is-shared-array-buffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6" + integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA== + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-typedarray@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +is-weakref@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" + integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== + +istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.1.0.tgz#7b49198b657b27a730b8e9cb601f1e1bff24c59a" + integrity sha512-czwUz525rkOFDJxfKK6mYfIs9zBKILyrZQxjz3ABhjQXhbhFsSbo1HW/BFcsDnfJYJWA6thRR5/TUY2qs5W99Q== + 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" + +istanbul-lib-report@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" + integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^3.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" + integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + source-map "^0.6.1" + +istanbul-reports@^3.1.3: + version "3.1.4" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.4.tgz#1b6f068ecbc6c331040aab5741991273e609e40c" + integrity sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + +jake@^10.6.1: + version "10.8.4" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.4.tgz#f6a8b7bf90c6306f768aa82bb7b98bf4ca15e84a" + integrity sha512-MtWeTkl1qGsWUtbl/Jsca/8xSoK3x0UmS82sNbjqxxG/de/M/3b1DntdjHgPMC50enlTNwXOCRqPXLLt5cCfZA== + dependencies: + async "0.9.x" + chalk "^4.0.2" + filelist "^1.0.1" + minimatch "^3.0.4" + +jest-changed-files@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.5.1.tgz#a348aed00ec9bf671cc58a66fcbe7c3dfd6a68f5" + integrity sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw== + dependencies: + "@jest/types" "^27.5.1" + execa "^5.0.0" + throat "^6.0.1" + +jest-circus@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-27.5.1.tgz#37a5a4459b7bf4406e53d637b49d22c65d125ecc" + 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" + +jest-cli@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-27.5.1.tgz#278794a6e6458ea8029547e6c6cbf673bd30b145" + 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" + +jest-config@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-27.5.1.tgz#5c387de33dca3f99ad6357ddeccd91bf3a0e4a41" + 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" + +jest-diff@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def" + 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" + +jest-docblock@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-27.5.1.tgz#14092f364a42c6108d42c33c8cf30e058e25f6c0" + integrity sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ== + dependencies: + detect-newline "^3.0.0" + +jest-each@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-27.5.1.tgz#5bc87016f45ed9507fed6e4702a5b468a5b2c44e" + 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" + +jest-environment-jsdom@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz#ea9ccd1fc610209655a77898f86b2b559516a546" + 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" + +jest-environment-node@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-27.5.1.tgz#dedc2cfe52fab6b8f5714b4808aefa85357a365e" + 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" + +jest-get-type@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1" + integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw== + +jest-haste-map@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.5.1.tgz#9fd8bd7e7b4fa502d9c6164c5640512b4e811e7f" + 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" + optionalDependencies: + fsevents "^2.3.2" + +jest-jasmine2@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz#a037b0034ef49a9f3d71c4375a796f3b230d1ac4" + 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" + +jest-leak-detector@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz#6ec9d54c3579dd6e3e66d70e3498adf80fde3fb8" + integrity sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ== + dependencies: + jest-get-type "^27.5.1" + pretty-format "^27.5.1" + +jest-matcher-utils@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab" + 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" + +jest-message-util@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.5.1.tgz#bdda72806da10d9ed6425e12afff38cd1458b6cf" + 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" + +jest-mock@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.5.1.tgz#19948336d49ef4d9c52021d34ac7b5f36ff967d6" + integrity sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og== + dependencies: + "@jest/types" "^27.5.1" + "@types/node" "*" + +jest-pnp-resolver@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" + integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== + +jest-regex-util@^27.0.0, jest-regex-util@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.5.1.tgz#4da143f7e9fd1e542d4aa69617b38e4a78365b95" + integrity sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg== + +jest-resolve-dependencies@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz#d811ecc8305e731cc86dd79741ee98fed06f1da8" + integrity sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg== + dependencies: + "@jest/types" "^27.5.1" + jest-regex-util "^27.5.1" + jest-snapshot "^27.5.1" + +jest-resolve@^27.4.2, jest-resolve@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-27.5.1.tgz#a2f1c5a0796ec18fe9eb1536ac3814c23617b384" + 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" + +jest-runner@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-27.5.1.tgz#071b27c1fa30d90540805c5645a0ec167c7b62e5" + 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" + +jest-runtime@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-27.5.1.tgz#4896003d7a334f7e8e4a53ba93fb9bcd3db0a1af" + 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" + +jest-serializer@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.5.1.tgz#81438410a30ea66fd57ff730835123dea1fb1f64" + integrity sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w== + dependencies: + "@types/node" "*" + graceful-fs "^4.2.9" + +jest-snapshot@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-27.5.1.tgz#b668d50d23d38054a51b42c4039cab59ae6eb6a1" + 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" + +jest-util@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.5.1.tgz#3ba9771e8e31a0b85da48fe0b0891fb86c01c2f9" + 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" + +jest-validate@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-27.5.1.tgz#9197d54dc0bdb52260b8db40b46ae668e04df067" + 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" + +jest-watch-typeahead@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/jest-watch-typeahead/-/jest-watch-typeahead-1.0.0.tgz#4de2ca1eb596acb1889752afbab84b74fcd99173" + integrity sha512-jxoszalAb394WElmiJTFBMzie/RDCF+W7Q29n5LzOPtcoQoHWfdUtHFkbhgf5NwWe8uMOxvKb/g7ea7CshfkTw== + dependencies: + ansi-escapes "^4.3.1" + chalk "^4.0.0" + jest-regex-util "^27.0.0" + jest-watcher "^27.0.0" + slash "^4.0.0" + string-length "^5.0.1" + strip-ansi "^7.0.1" + +jest-watcher@^27.0.0, jest-watcher@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-27.5.1.tgz#71bd85fb9bde3a2c2ec4dc353437971c43c642a2" + 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" + +jest-worker@^26.2.1: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" + integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^7.0.0" + +jest-worker@^27.0.2, jest-worker@^27.3.1, jest-worker@^27.4.5, jest-worker@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest@^27.4.3: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest/-/jest-27.5.1.tgz#dadf33ba70a779be7a6fc33015843b51494f63fc" + integrity sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ== + dependencies: + "@jest/core" "^27.5.1" + import-local "^3.0.2" + jest-cli "^27.5.1" + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jsdom@^16.6.0: + version "16.7.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710" + 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" + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= + +json-parse-better-errors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json-schema@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + +json5@^2.1.2, json5@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" + integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonpointer@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.0.tgz#f802669a524ec4805fa7389eadbc9921d5dc8072" + integrity sha512-PNYZIdMjVIvVgDSYKTT63Y+KZ6IZvGRNNWcxwD+GNnUz1MKPfv30J8ueCjdwcN0nDx2SlshgyB7Oy0epAzVRRg== + +"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz#720b97bfe7d901b927d87c3773637ae8ea48781b" + integrity sha512-uP5vu8xfy2F9A6LGC22KO7e2/vGTS1MhP+18f++ZNlf0Ohaxbc9nIEwHAsejlJKyzfZzU5UIhe5ItYkitcZnZA== + dependencies: + array-includes "^3.1.3" + object.assign "^4.1.2" + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +klona@^2.0.4, klona@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.5.tgz#d166574d90076395d9963aa7a928fabb8d76afbc" + integrity sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ== + +language-subtag-registry@~0.3.2: + version "0.3.21" + resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz#04ac218bea46f04cb039084602c6da9e788dd45a" + integrity sha512-L0IqwlIXjilBVVYKFT37X9Ih11Um5NEl9cbJIuU/SwP/zEEAbBPOnEeeuxVMf45ydWQRDQN3Nqc96OgbH1K+Pg== + +language-tags@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.5.tgz#d321dbc4da30ba8bf3024e040fa5c14661f9193a" + integrity sha1-0yHbxNowuovzAk4ED6XBRmH5GTo= + dependencies: + language-subtag-registry "~0.3.2" + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +lilconfig@^2.0.3, lilconfig@^2.0.4: + version "2.0.5" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.5.tgz#19e57fd06ccc3848fd1891655b5a447092225b25" + integrity sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg== + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +loader-runner@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384" + integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw== + +loader-utils@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129" + integrity sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + +loader-utils@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.2.0.tgz#bcecc51a7898bee7473d4bc6b845b23af8304d4f" + integrity sha512-HVl9ZqccQihZ7JM85dco1MvO9G+ONvxoGa9rkhzFsneGLKSUg1gJf9bWzhRhcvm2qChhWpebQhP44qxjKIUCaQ== + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.curry@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.curry/-/lodash.curry-4.1.1.tgz#248e36072ede906501d75966200a86dab8b23170" + integrity sha1-JI42By7ekGUB11lmIAqG2riyMXA= + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= + +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= + +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= + +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= + +lodash@^4.17.14, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lower-case@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" + integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== + dependencies: + tslib "^2.0.3" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +magic-string@^0.25.0, magic-string@^0.25.7: + version "0.25.9" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" + integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ== + dependencies: + sourcemap-codec "^1.4.8" + +make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +makeerror@1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" + integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== + dependencies: + tmpl "1.0.5" + +mdn-data@2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" + integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== + +mdn-data@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" + integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +memfs@^3.1.2, memfs@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.4.1.tgz#b78092f466a0dce054d63d39275b24c71d3f1305" + integrity sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw== + dependencies: + fs-monkey "1.0.3" + +memoize-one@^5.0.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + +micromatch@^4.0.2, micromatch@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +mini-create-react-context@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz#072171561bfdc922da08a60c2197a497cc2d1d5e" + integrity sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ== + dependencies: + "@babel/runtime" "^7.12.1" + tiny-warning "^1.0.3" + +mini-css-extract-plugin@^2.4.5: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.6.0.tgz#578aebc7fc14d32c0ad304c2c34f08af44673f5e" + integrity sha512-ndG8nxCEnAemsg4FSgS+yNyHKgkTB4nPKqCOgh65j3/30qqC5RaSQQXMm++Y6sb6E1zRSxPkztj9fqxhS1Eo6w== + dependencies: + schema-utils "^4.0.0" + +minimalistic-assert@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimatch@3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^3.0.4, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== + +mkdirp@^0.5.5, mkdirp@~0.5.1: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3, ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multicast-dns-service-types@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" + integrity sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE= + +multicast-dns@^6.0.1: + version "6.2.3" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229" + integrity sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g== + dependencies: + dns-packet "^1.3.1" + thunky "^1.0.2" + +nanoid@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" + integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +no-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" + integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== + dependencies: + lower-case "^2.0.2" + tslib "^2.0.3" + +node-forge@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.0.tgz#37a874ea723855f37db091e6c186e5b67a01d4b2" + integrity sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA== + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= + +node-releases@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.2.tgz#7139fe71e2f4f11b47d4d2986aaf8c48699e0c01" + integrity sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= + +normalize-url@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" + integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +nth-check@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" + integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== + dependencies: + boolbase "~1.0.0" + +nth-check@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2" + integrity sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w== + dependencies: + boolbase "^1.0.0" + +nwsapi@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" + integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== + +object-assign@^4.1.0, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +object-hash@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" + integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== + +object-inspect@^1.11.0, object-inspect@^1.9.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" + integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== + +object-is@^1.0.1: + version "1.1.5" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" + integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + +object-keys@^1.0.12, object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.0, object.assign@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" + integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + has-symbols "^1.0.1" + object-keys "^1.1.1" + +object.entries@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.5.tgz#e1acdd17c4de2cd96d5a08487cfb9db84d881861" + integrity sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.1" + +object.fromentries@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.5.tgz#7b37b205109c21e741e605727fe8b0ad5fa08251" + integrity sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.1" + +object.getownpropertydescriptors@^2.1.0: + version "2.1.3" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.3.tgz#b223cf38e17fefb97a63c10c91df72ccb386df9e" + integrity sha512-VdDoCwvJI4QdC6ndjpqFmoL3/+HxffFBbcJzKi5hwLLqqx3mdbedRpfZDdK0SrOSauj8X4GzBvnDZl4vTN7dOw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.1" + +object.hasown@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.0.tgz#7232ed266f34d197d15cac5880232f7a4790afe5" + integrity sha512-MhjYRfj3GBlhSkDHo6QmvgjRLXQ2zndabdf3nX0yTyZK9rPfxb6uRpAac8HXNLy1GpqWtZ81Qh4v3uOls2sRAg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.19.1" + +object.values@^1.1.0, object.values@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac" + integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.1" + +obuf@^1.0.0, obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +open@^8.0.9, open@^8.4.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8" + integrity sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + +optionator@^0.8.1: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + 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" + +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + 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" + +p-limit@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" + integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== + dependencies: + p-try "^1.0.0" + +p-limit@^2.0.0, p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= + dependencies: + p-limit "^1.1.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + +p-retry@^4.5.0: + version "4.6.1" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.1.tgz#8fcddd5cdf7a67a0911a9cf2ef0e5df7f602316c" + integrity sha512-e2xXGNhZOZ0lfgR9kL34iGlU8N/KO0xZnQxVEwdeOvpqNDQfdnxIYizvWtK8RglUa3bGqI8g0R/BdfzLMxRkiA== + dependencies: + "@types/retry" "^0.12.0" + retry "^0.13.1" + +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +param-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" + integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^5.0.0, parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +parse5@6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" + integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== + +parseurl@~1.3.2, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +pascal-case@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" + integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.6, path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + +path-to-regexp@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" + integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + dependencies: + isarray "0.0.1" + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +picocolors@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" + integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pirates@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" + integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== + +pkg-dir@^4.1.0, pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +pkg-up@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" + integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA== + dependencies: + find-up "^3.0.0" + +portfinder@^1.0.28: + version "1.0.28" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778" + integrity sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA== + dependencies: + async "^2.6.2" + debug "^3.1.1" + mkdirp "^0.5.5" + +postcss-attribute-case-insensitive@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.0.tgz#39cbf6babf3ded1e4abf37d09d6eda21c644105c" + integrity sha512-b4g9eagFGq9T5SWX4+USfVyjIb3liPnjhHHRMP7FMB2kFVpYyfEscV0wP3eaXhKlcHKUut8lt5BGoeylWA/dBQ== + dependencies: + postcss-selector-parser "^6.0.2" + +postcss-browser-comments@^4: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-browser-comments/-/postcss-browser-comments-4.0.0.tgz#bcfc86134df5807f5d3c0eefa191d42136b5e72a" + integrity sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg== + +postcss-calc@^8.2.3: + version "8.2.4" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.2.4.tgz#77b9c29bfcbe8a07ff6693dc87050828889739a5" + integrity sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q== + dependencies: + postcss-selector-parser "^6.0.9" + postcss-value-parser "^4.2.0" + +postcss-clamp@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-clamp/-/postcss-clamp-4.1.0.tgz#7263e95abadd8c2ba1bd911b0b5a5c9c93e02363" + integrity sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-color-functional-notation@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.2.tgz#f59ccaeb4ee78f1b32987d43df146109cc743073" + integrity sha512-DXVtwUhIk4f49KK5EGuEdgx4Gnyj6+t2jBSEmxvpIK9QI40tWrpS2Pua8Q7iIZWBrki2QOaeUdEaLPPa91K0RQ== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-color-hex-alpha@^8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.3.tgz#61a0fd151d28b128aa6a8a21a2dad24eebb34d52" + integrity sha512-fESawWJCrBV035DcbKRPAVmy21LpoyiXdPTuHUfWJ14ZRjY7Y7PA6P4g8z6LQGYhU1WAxkTxjIjurXzoe68Glw== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-color-rebeccapurple@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.0.2.tgz#5d397039424a58a9ca628762eb0b88a61a66e079" + integrity sha512-SFc3MaocHaQ6k3oZaFwH8io6MdypkUtEy/eXzXEB1vEQlO3S3oDc/FSZA8AsS04Z25RirQhlDlHLh3dn7XewWw== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-colormin@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.3.0.tgz#3cee9e5ca62b2c27e84fce63affc0cfb5901956a" + integrity sha512-WdDO4gOFG2Z8n4P8TWBpshnL3JpmNmJwdnfP2gbk2qBA8PWwOYcmjmI/t3CmMeL72a7Hkd+x/Mg9O2/0rD54Pg== + dependencies: + browserslist "^4.16.6" + caniuse-api "^3.0.0" + colord "^2.9.1" + postcss-value-parser "^4.2.0" + +postcss-convert-values@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-5.1.0.tgz#f8d3abe40b4ce4b1470702a0706343eac17e7c10" + integrity sha512-GkyPbZEYJiWtQB0KZ0X6qusqFHUepguBCNFi9t5JJc7I2OTXG7C0twbTLvCfaKOLl3rSXmpAwV7W5txd91V84g== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-custom-media@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-8.0.0.tgz#1be6aff8be7dc9bf1fe014bde3b71b92bb4552f1" + integrity sha512-FvO2GzMUaTN0t1fBULDeIvxr5IvbDXcIatt6pnJghc736nqNgsGao5NT+5+WVLAQiTt6Cb3YUms0jiPaXhL//g== + +postcss-custom-properties@^12.1.5: + version "12.1.5" + resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-12.1.5.tgz#e669cfff89b0ea6fc85c45864a32b450cb6b196f" + integrity sha512-FHbbB/hRo/7cxLGkc2NS7cDRIDN1oFqQnUKBiyh4b/gwk8DD8udvmRDpUhEK836kB8ggUCieHVOvZDnF9XhI3g== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-custom-selectors@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-6.0.0.tgz#022839e41fbf71c47ae6e316cb0e6213012df5ef" + integrity sha512-/1iyBhz/W8jUepjGyu7V1OPcGbc636snN1yXEQCinb6Bwt7KxsiU7/bLQlp8GwAXzCh7cobBU5odNn/2zQWR8Q== + dependencies: + postcss-selector-parser "^6.0.4" + +postcss-dir-pseudo-class@^6.0.4: + version "6.0.4" + resolved "https://registry.yarnpkg.com/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.4.tgz#9afe49ea631f0cb36fa0076e7c2feb4e7e3f049c" + integrity sha512-I8epwGy5ftdzNWEYok9VjW9whC4xnelAtbajGv4adql4FIF09rnrxnA9Y8xSHN47y7gqFIv10C5+ImsLeJpKBw== + dependencies: + postcss-selector-parser "^6.0.9" + +postcss-discard-comments@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-5.1.1.tgz#e90019e1a0e5b99de05f63516ce640bd0df3d369" + integrity sha512-5JscyFmvkUxz/5/+TB3QTTT9Gi9jHkcn8dcmmuN68JQcv3aQg4y88yEHHhwFB52l/NkaJ43O0dbksGMAo49nfQ== + +postcss-discard-duplicates@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz#9eb4fe8456706a4eebd6d3b7b777d07bad03e848" + integrity sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw== + +postcss-discard-empty@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz#e57762343ff7f503fe53fca553d18d7f0c369c6c" + integrity sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A== + +postcss-discard-overridden@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz#7e8c5b53325747e9d90131bb88635282fb4a276e" + integrity sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw== + +postcss-double-position-gradients@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.1.tgz#a12cfdb7d11fa1a99ccecc747f0c19718fb37152" + integrity sha512-jM+CGkTs4FcG53sMPjrrGE0rIvLDdCrqMzgDC5fLI7JHDO7o6QG8C5TQBtExb13hdBdoH9C2QVbG4jo2y9lErQ== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^1.1.0" + postcss-value-parser "^4.2.0" + +postcss-env-function@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/postcss-env-function/-/postcss-env-function-4.0.6.tgz#7b2d24c812f540ed6eda4c81f6090416722a8e7a" + integrity sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-flexbugs-fixes@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz#2028e145313074fc9abe276cb7ca14e5401eb49d" + integrity sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ== + +postcss-focus-visible@^6.0.4: + version "6.0.4" + resolved "https://registry.yarnpkg.com/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz#50c9ea9afa0ee657fb75635fabad25e18d76bf9e" + integrity sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw== + dependencies: + postcss-selector-parser "^6.0.9" + +postcss-focus-within@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz#5b1d2ec603195f3344b716c0b75f61e44e8d2e20" + integrity sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ== + dependencies: + postcss-selector-parser "^6.0.9" + +postcss-font-variant@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz#efd59b4b7ea8bb06127f2d031bfbb7f24d32fa66" + integrity sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA== + +postcss-gap-properties@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/postcss-gap-properties/-/postcss-gap-properties-3.0.3.tgz#6401bb2f67d9cf255d677042928a70a915e6ba60" + integrity sha512-rPPZRLPmEKgLk/KlXMqRaNkYTUpE7YC+bOIQFN5xcu1Vp11Y4faIXv6/Jpft6FMnl6YRxZqDZG0qQOW80stzxQ== + +postcss-image-set-function@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/postcss-image-set-function/-/postcss-image-set-function-4.0.6.tgz#bcff2794efae778c09441498f40e0c77374870a9" + integrity sha512-KfdC6vg53GC+vPd2+HYzsZ6obmPqOk6HY09kttU19+Gj1nC3S3XBVEXDHxkhxTohgZqzbUb94bKXvKDnYWBm/A== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-initial@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-4.0.1.tgz#529f735f72c5724a0fb30527df6fb7ac54d7de42" + integrity sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ== + +postcss-js@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.0.tgz#31db79889531b80dc7bc9b0ad283e418dce0ac00" + integrity sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ== + dependencies: + camelcase-css "^2.0.1" + +postcss-lab-function@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-4.1.2.tgz#b75afe43ba9c1f16bfe9bb12c8109cabd55b5fc2" + integrity sha512-isudf5ldhg4fk16M8viAwAbg6Gv14lVO35N3Z/49NhbwPQ2xbiEoHgrRgpgQojosF4vF7jY653ktB6dDrUOR8Q== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^1.1.0" + postcss-value-parser "^4.2.0" + +postcss-load-config@^3.1.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.3.tgz#21935b2c43b9a86e6581a576ca7ee1bde2bd1d23" + integrity sha512-5EYgaM9auHGtO//ljHH+v/aC/TQ5LHXtL7bQajNAUBKUVKiYE8rYpFms7+V26D9FncaGe2zwCoPQsFKb5zF/Hw== + dependencies: + lilconfig "^2.0.4" + yaml "^1.10.2" + +postcss-loader@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-6.2.1.tgz#0895f7346b1702103d30fdc66e4d494a93c008ef" + integrity sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q== + dependencies: + cosmiconfig "^7.0.0" + klona "^2.0.5" + semver "^7.3.5" + +postcss-logical@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/postcss-logical/-/postcss-logical-5.0.4.tgz#ec75b1ee54421acc04d5921576b7d8db6b0e6f73" + integrity sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g== + +postcss-media-minmax@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz#7140bddec173e2d6d657edbd8554a55794e2a5b5" + integrity sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ== + +postcss-merge-longhand@^5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.1.3.tgz#a49e2be6237316e3b55e329e0a8da15d1f9f47ab" + integrity sha512-lX8GPGvZ0iGP/IboM7HXH5JwkXvXod1Rr8H8ixwiA372hArk0zP4ZcCy4z4Prg/bfNlbbTf0KCOjCF9kKnpP/w== + dependencies: + postcss-value-parser "^4.2.0" + stylehacks "^5.1.0" + +postcss-merge-rules@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.1.1.tgz#d327b221cd07540bcc8d9ff84446d8b404d00162" + integrity sha512-8wv8q2cXjEuCcgpIB1Xx1pIy8/rhMPIQqYKNzEdyx37m6gpq83mQQdCxgIkFgliyEnKvdwJf/C61vN4tQDq4Ww== + dependencies: + browserslist "^4.16.6" + caniuse-api "^3.0.0" + cssnano-utils "^3.1.0" + postcss-selector-parser "^6.0.5" + +postcss-minify-font-values@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz#f1df0014a726083d260d3bd85d7385fb89d1f01b" + integrity sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-minify-gradients@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz#f1fe1b4f498134a5068240c2f25d46fcd236ba2c" + integrity sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw== + dependencies: + colord "^2.9.1" + cssnano-utils "^3.1.0" + postcss-value-parser "^4.2.0" + +postcss-minify-params@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.1.2.tgz#77e250780c64198289c954884ebe3ee4481c3b1c" + integrity sha512-aEP+p71S/urY48HWaRHasyx4WHQJyOYaKpQ6eXl8k0kxg66Wt/30VR6/woh8THgcpRbonJD5IeD+CzNhPi1L8g== + dependencies: + browserslist "^4.16.6" + cssnano-utils "^3.1.0" + postcss-value-parser "^4.2.0" + +postcss-minify-selectors@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-5.2.0.tgz#17c2be233e12b28ffa8a421a02fc8b839825536c" + integrity sha512-vYxvHkW+iULstA+ctVNx0VoRAR4THQQRkG77o0oa4/mBS0OzGvvzLIvHDv/nNEM0crzN2WIyFU5X7wZhaUK3RA== + dependencies: + postcss-selector-parser "^6.0.5" + +postcss-modules-extract-imports@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" + integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== + +postcss-modules-local-by-default@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c" + integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ== + dependencies: + icss-utils "^5.0.0" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.1.0" + +postcss-modules-scope@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06" + integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg== + dependencies: + postcss-selector-parser "^6.0.4" + +postcss-modules-values@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" + integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== + dependencies: + icss-utils "^5.0.0" + +postcss-nested@5.0.6: + version "5.0.6" + resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-5.0.6.tgz#466343f7fc8d3d46af3e7dba3fcd47d052a945bc" + integrity sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA== + dependencies: + postcss-selector-parser "^6.0.6" + +postcss-nesting@^10.1.3: + version "10.1.3" + resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-10.1.3.tgz#f0b1cd7ae675c697ab6a5a5ca1feea4784a2ef77" + integrity sha512-wUC+/YCik4wH3StsbC5fBG1s2Z3ZV74vjGqBFYtmYKlVxoio5TYGM06AiaKkQPPlkXWn72HKfS7Cw5PYxnoXSw== + dependencies: + postcss-selector-parser "^6.0.9" + +postcss-normalize-charset@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz#9302de0b29094b52c259e9b2cf8dc0879879f0ed" + integrity sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg== + +postcss-normalize-display-values@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz#72abbae58081960e9edd7200fcf21ab8325c3da8" + integrity sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-positions@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-5.1.0.tgz#902a7cb97cf0b9e8b1b654d4a43d451e48966458" + integrity sha512-8gmItgA4H5xiUxgN/3TVvXRoJxkAWLW6f/KKhdsH03atg0cB8ilXnrB5PpSshwVu/dD2ZsRFQcR1OEmSBDAgcQ== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-repeat-style@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.0.tgz#f6d6fd5a54f51a741cc84a37f7459e60ef7a6398" + integrity sha512-IR3uBjc+7mcWGL6CtniKNQ4Rr5fTxwkaDHwMBDGGs1x9IVRkYIT/M4NelZWkAOBdV6v3Z9S46zqaKGlyzHSchw== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-string@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz#411961169e07308c82c1f8c55f3e8a337757e228" + integrity sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-timing-functions@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz#d5614410f8f0b2388e9f240aa6011ba6f52dafbb" + integrity sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-unicode@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.0.tgz#3d23aede35e160089a285e27bf715de11dc9db75" + integrity sha512-J6M3MizAAZ2dOdSjy2caayJLQT8E8K9XjLce8AUQMwOrCvjCHv24aLC/Lps1R1ylOfol5VIDMaM/Lo9NGlk1SQ== + dependencies: + browserslist "^4.16.6" + postcss-value-parser "^4.2.0" + +postcss-normalize-url@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz#ed9d88ca82e21abef99f743457d3729a042adcdc" + integrity sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew== + dependencies: + normalize-url "^6.0.1" + postcss-value-parser "^4.2.0" + +postcss-normalize-whitespace@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz#08a1a0d1ffa17a7cc6efe1e6c9da969cc4493cfa" + integrity sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize/-/postcss-normalize-10.0.1.tgz#464692676b52792a06b06880a176279216540dd7" + integrity sha512-+5w18/rDev5mqERcG3W5GZNMJa1eoYYNGo8gB7tEwaos0ajk3ZXAI4mHGcNT47NE+ZnZD1pEpUOFLvltIwmeJA== + dependencies: + "@csstools/normalize.css" "*" + postcss-browser-comments "^4" + sanitize.css "*" + +postcss-opacity-percentage@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.2.tgz#bd698bb3670a0a27f6d657cc16744b3ebf3b1145" + integrity sha512-lyUfF7miG+yewZ8EAk9XUBIlrHyUE6fijnesuz+Mj5zrIHIEw6KcIZSOk/elVMqzLvREmXB83Zi/5QpNRYd47w== + +postcss-ordered-values@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.1.1.tgz#0b41b610ba02906a3341e92cab01ff8ebc598adb" + integrity sha512-7lxgXF0NaoMIgyihL/2boNAEZKiW0+HkMhdKMTD93CjW8TdCy2hSdj8lsAo+uwm7EDG16Da2Jdmtqpedl0cMfw== + dependencies: + cssnano-utils "^3.1.0" + postcss-value-parser "^4.2.0" + +postcss-overflow-shorthand@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.3.tgz#ebcfc0483a15bbf1b27fdd9b3c10125372f4cbc2" + integrity sha512-CxZwoWup9KXzQeeIxtgOciQ00tDtnylYIlJBBODqkgS/PU2jISuWOL/mYLHmZb9ZhZiCaNKsCRiLp22dZUtNsg== + +postcss-page-break@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/postcss-page-break/-/postcss-page-break-3.0.4.tgz#7fbf741c233621622b68d435babfb70dd8c1ee5f" + integrity sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ== + +postcss-place@^7.0.4: + version "7.0.4" + resolved "https://registry.yarnpkg.com/postcss-place/-/postcss-place-7.0.4.tgz#eb026650b7f769ae57ca4f938c1addd6be2f62c9" + integrity sha512-MrgKeiiu5OC/TETQO45kV3npRjOFxEHthsqGtkh3I1rPbZSbXGD/lZVi9j13cYh+NA8PIAPyk6sGjT9QbRyvSg== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-preset-env@^7.0.1: + version "7.4.3" + resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-7.4.3.tgz#fb1c8b4cb405da042da0ddb8c5eda7842c08a449" + integrity sha512-dlPA65g9KuGv7YsmGyCKtFkZKCPLkoVMUE3omOl6yM+qrynVHxFvf0tMuippIrXB/sB/MyhL1FgTIbrO+qMERg== + dependencies: + "@csstools/postcss-color-function" "^1.0.3" + "@csstools/postcss-font-format-keywords" "^1.0.0" + "@csstools/postcss-hwb-function" "^1.0.0" + "@csstools/postcss-ic-unit" "^1.0.0" + "@csstools/postcss-is-pseudo-class" "^2.0.1" + "@csstools/postcss-normalize-display-values" "^1.0.0" + "@csstools/postcss-oklab-function" "^1.0.2" + "@csstools/postcss-progressive-custom-properties" "^1.3.0" + autoprefixer "^10.4.4" + browserslist "^4.20.2" + css-blank-pseudo "^3.0.3" + css-has-pseudo "^3.0.4" + css-prefers-color-scheme "^6.0.3" + cssdb "^6.5.0" + postcss-attribute-case-insensitive "^5.0.0" + postcss-clamp "^4.1.0" + postcss-color-functional-notation "^4.2.2" + postcss-color-hex-alpha "^8.0.3" + postcss-color-rebeccapurple "^7.0.2" + postcss-custom-media "^8.0.0" + postcss-custom-properties "^12.1.5" + postcss-custom-selectors "^6.0.0" + postcss-dir-pseudo-class "^6.0.4" + postcss-double-position-gradients "^3.1.1" + 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.3" + postcss-image-set-function "^4.0.6" + postcss-initial "^4.0.1" + postcss-lab-function "^4.1.2" + postcss-logical "^5.0.4" + postcss-media-minmax "^5.0.0" + postcss-nesting "^10.1.3" + postcss-opacity-percentage "^1.1.2" + postcss-overflow-shorthand "^3.0.3" + postcss-page-break "^3.0.4" + postcss-place "^7.0.4" + postcss-pseudo-class-any-link "^7.1.1" + postcss-replace-overflow-wrap "^4.0.0" + postcss-selector-not "^5.0.0" + postcss-value-parser "^4.2.0" + +postcss-pseudo-class-any-link@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.1.tgz#534eb1dadd9945eb07830dbcc06fb4d5d865b8e0" + integrity sha512-JRoLFvPEX/1YTPxRxp1JO4WxBVXJYrSY7NHeak5LImwJ+VobFMwYDQHvfTXEpcn+7fYIeGkC29zYFhFWIZD8fg== + dependencies: + postcss-selector-parser "^6.0.9" + +postcss-reduce-initial@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.1.0.tgz#fc31659ea6e85c492fb2a7b545370c215822c5d6" + integrity sha512-5OgTUviz0aeH6MtBjHfbr57tml13PuedK/Ecg8szzd4XRMbYxH4572JFG067z+FqBIf6Zp/d+0581glkvvWMFw== + dependencies: + browserslist "^4.16.6" + caniuse-api "^3.0.0" + +postcss-reduce-transforms@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz#333b70e7758b802f3dd0ddfe98bb1ccfef96b6e9" + integrity sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-replace-overflow-wrap@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz#d2df6bed10b477bf9c52fab28c568b4b29ca4319" + integrity sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw== + +postcss-selector-not@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-5.0.0.tgz#ac5fc506f7565dd872f82f5314c0f81a05630dc7" + integrity sha512-/2K3A4TCP9orP4TNS7u3tGdRFVKqz/E6pX3aGnriPG0jU78of8wsUcqE4QAhWEU0d+WnMSF93Ah3F//vUtK+iQ== + dependencies: + balanced-match "^1.0.0" + +postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.6, postcss-selector-parser@^6.0.9: + version "6.0.9" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.9.tgz#ee71c3b9ff63d9cd130838876c13a2ec1a992b2f" + integrity sha512-UO3SgnZOVTwu4kyLR22UQ1xZh086RyNZppb7lLAKBFK8a32ttG5i87Y/P3+2bRSjZNyJ1B7hfFNo273tKe9YxQ== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-svgo@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.1.0.tgz#0a317400ced789f233a28826e77523f15857d80d" + integrity sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA== + dependencies: + postcss-value-parser "^4.2.0" + svgo "^2.7.0" + +postcss-unique-selectors@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz#a9f273d1eacd09e9aa6088f4b0507b18b1b541b6" + integrity sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA== + dependencies: + postcss-selector-parser "^6.0.5" + +postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +postcss@^7.0.35: + version "7.0.39" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.39.tgz#9624375d965630e2e1f2c02a935c82a59cb48309" + integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA== + dependencies: + picocolors "^0.2.1" + source-map "^0.6.1" + +postcss@^8.3.5, postcss@^8.4.4, postcss@^8.4.6, postcss@^8.4.7: + version "8.4.12" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.12.tgz#1e7de78733b28970fa4743f7da6f3763648b1905" + integrity sha512-lg6eITwYe9v6Hr5CncVbK70SoioNQIq81nsaG86ev5hAidQvmOeETBqs7jm43K2F5/Ley3ytDtriImV6TpNiSg== + dependencies: + nanoid "^3.3.1" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= + +pretty-bytes@^5.3.0, pretty-bytes@^5.4.1: + version "5.6.0" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" + integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== + +pretty-error@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-4.0.0.tgz#90a703f46dd7234adb46d0f84823e9d1cb8f10d6" + integrity sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw== + dependencies: + lodash "^4.17.20" + renderkid "^3.0.0" + +pretty-format@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" + integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== + dependencies: + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^17.0.1" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +promise@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/promise/-/promise-8.1.0.tgz#697c25c3dfe7435dd79fcd58c38a135888eaf05e" + integrity sha512-W04AqnILOL/sPRXziNicCjSNRruLAuIHEOVBazepu0545DDNGYHz7ar9ZgZ1fMU8/MA4mVxp5rkBWRi6OXIy3Q== + dependencies: + asap "~2.0.6" + +prompts@^2.0.1, prompts@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + +prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +psl@^1.1.33: + version "1.8.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" + integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== + +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +q@^1.1.2: + version "1.5.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= + +qs@6.9.7: + version "6.9.7" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.7.tgz#4610846871485e1e048f44ae3b94033f0e675afe" + integrity sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +quick-lru@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" + integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== + +raf@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" + integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== + dependencies: + performance-now "^2.1.0" + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@^1.2.1, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.3.tgz#8f80305d11c2a0a545c2d9d89d7a0286fcead43c" + integrity sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g== + dependencies: + bytes "3.1.2" + http-errors "1.8.1" + iconv-lite "0.4.24" + unpipe "1.0.0" + +react-ace@^9.4.1: + version "9.5.0" + resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-9.5.0.tgz#b6c32b70d404dd821a7e01accc2d76da667ff1f7" + integrity sha512-4l5FgwGh6K7A0yWVMQlPIXDItM4Q9zzXRqOae8KkCl6MkOob7sC1CzHxZdOGvV+QioKWbX2p5HcdOVUv6cAdSg== + dependencies: + ace-builds "^1.4.13" + diff-match-patch "^1.0.5" + lodash.get "^4.4.2" + lodash.isequal "^4.5.0" + prop-types "^15.7.2" + +react-app-polyfill@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/react-app-polyfill/-/react-app-polyfill-3.0.0.tgz#95221e0a9bd259e5ca6b177c7bb1cb6768f68fd7" + 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" + +react-base16-styling@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/react-base16-styling/-/react-base16-styling-0.9.1.tgz#4906b4c0a51636f2dca2cea8b682175aa8bd0c92" + integrity sha512-1s0CY1zRBOQ5M3T61wetEpvQmsYSNtWEcdYzyZNxKa8t7oDvaOn9d21xrGezGAHFWLM7SHcktPuPTrvoqxSfKw== + dependencies: + "@babel/runtime" "^7.16.7" + "@types/base16" "^1.0.2" + "@types/lodash" "^4.14.178" + base16 "^1.0.0" + color "^3.2.1" + csstype "^3.0.10" + lodash.curry "^4.1.1" + +react-contextmenu@^2.14.0: + version "2.14.0" + resolved "https://registry.yarnpkg.com/react-contextmenu/-/react-contextmenu-2.14.0.tgz#d8966f30614b9b780b928be4c8d92bd740d55cdd" + integrity sha512-ktqMOuad6sCFNJs/ltEwppN8F0YeXmqoZfwycgtZR/MxOXMYx1xgYC44SzWH259HdGyshk1/7sXGuIRwj9hzbw== + dependencies: + classnames "^2.2.5" + object-assign "^4.1.0" + +react-dev-utils@^12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.0.tgz#4eab12cdb95692a077616770b5988f0adf806526" + integrity sha512-xBQkitdxozPxt1YZ9O1097EJiVpwHr9FoAuEVURCKV0Av8NBERovJauzP7bo1ThvuhZ4shsQ1AJiu4vQpoT1AQ== + 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.10" + recursive-readdir "^2.2.2" + shell-quote "^1.7.3" + strip-ansi "^6.0.1" + text-table "^0.2.0" + +react-dom@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" + integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + scheduler "^0.20.2" + +react-error-overlay@^6.0.10: + version "6.0.10" + resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.10.tgz#0fe26db4fa85d9dbb8624729580e90e7159a59a6" + integrity sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA== + +react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-is@^17.0.1: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + +react-json-tree@^0.16.1: + version "0.16.1" + resolved "https://registry.yarnpkg.com/react-json-tree/-/react-json-tree-0.16.1.tgz#dd40a4f759c173873d28e51316c29150e2473de5" + integrity sha512-VYvU/tgekJUrPhEVR6ZC789b3x9QPO7LmRX1YMZfWpqbC8pvXb7D74S0+CE6RVk55bXOS2VH1+8ZWA2PioEyqg== + dependencies: + "@babel/runtime" "^7.16.7" + "@types/prop-types" "^15.7.4" + prop-types "^15.8.1" + react-base16-styling "^0.9.1" + +react-refresh@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046" + integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A== + +react-router-dom@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.0.tgz#da1bfb535a0e89a712a93b97dd76f47ad1f32363" + integrity sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ== + dependencies: + "@babel/runtime" "^7.12.13" + history "^4.9.0" + loose-envify "^1.3.1" + prop-types "^15.6.2" + react-router "5.2.1" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + +react-router@5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.1.tgz#4d2e4e9d5ae9425091845b8dbc6d9d276239774d" + integrity sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ== + dependencies: + "@babel/runtime" "^7.12.13" + history "^4.9.0" + hoist-non-react-statics "^3.1.0" + loose-envify "^1.3.1" + mini-create-react-context "^0.4.0" + path-to-regexp "^1.7.0" + prop-types "^15.6.2" + react-is "^16.6.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + +react-scripts@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-5.0.0.tgz#6547a6d7f8b64364ef95273767466cc577cb4b60" + integrity sha512-3i0L2CyIlROz7mxETEdfif6Sfhh9Lfpzi10CtcGs1emDQStmZfWjJbAIMtRD0opVUjQuFWqHZyRZ9PPzKCFxWg== + 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.0" + 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.0" + 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" + optionalDependencies: + fsevents "^2.3.2" + +react-select@^5.2.1: + version "5.2.2" + resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.2.2.tgz#3d5edf0a60f1276fd5f29f9f90a305f0a25a5189" + integrity sha512-miGS2rT1XbFNjduMZT+V73xbJEeMzVkJOz727F6MeAr2hKE0uUSA8Ff7vD44H32x2PD3SRB6OXTY/L+fTV3z9w== + dependencies: + "@babel/runtime" "^7.12.0" + "@emotion/cache" "^11.4.0" + "@emotion/react" "^11.1.1" + "@types/react-transition-group" "^4.4.0" + memoize-one "^5.0.0" + prop-types "^15.6.0" + react-transition-group "^4.3.0" + +react-transition-group@^4.3.0: + version "4.4.2" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470" + integrity sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + +react@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" + integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + +readable-stream@^2.0.1: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + 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" + +readable-stream@^3.0.6: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +recursive-readdir@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f" + integrity sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg== + dependencies: + minimatch "3.0.4" + +regenerate-unicode-properties@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz#7f442732aa7934a3740c779bb9b3340dccc1fb56" + integrity sha512-vn5DU6yg6h8hP/2OkQo3K7uVILvY4iu0oI4t3HFa81UPkhGJwkRwM10JEc3upjdhHjs/k8GJY1sRBhk5sr69Bw== + dependencies: + regenerate "^1.4.2" + +regenerate@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== + +regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.9: + version "0.13.9" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" + integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== + +regenerator-transform@^0.14.2: + version "0.14.5" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4" + integrity sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw== + dependencies: + "@babel/runtime" "^7.8.4" + +regex-parser@^2.2.11: + version "2.2.11" + resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.2.11.tgz#3b37ec9049e19479806e878cabe7c1ca83ccfe58" + integrity sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q== + +regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.1.tgz#b3f4c0059af9e47eca9f3f660e51d81307e72307" + integrity sha512-pMR7hBVUUGI7PMA37m2ofIdQCsomVnas+Jn5UPGAHQ+/LlwKm/aTLJHdasmHRzlfeZwHiAOaRSo2rbBDm3nNUQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + +regexpp@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" + integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== + +regexpu-core@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.0.1.tgz#c531122a7840de743dcf9c83e923b5560323ced3" + integrity sha512-CriEZlrKK9VJw/xQGJpQM5rY88BtuL8DM+AEwvcThHilbxiTAy8vq4iJnd2tqq8wLmjbGZzP7ZcKFjbGkmEFrw== + dependencies: + regenerate "^1.4.2" + regenerate-unicode-properties "^10.0.1" + regjsgen "^0.6.0" + regjsparser "^0.8.2" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.0.0" + +regjsgen@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.6.0.tgz#83414c5354afd7d6627b16af5f10f41c4e71808d" + integrity sha512-ozE883Uigtqj3bx7OhL1KNbCzGyW2NQZPl6Hs09WTvCuZD5sTI4JY58bkbQWa/Y9hxIsvJ3M8Nbf7j54IqeZbA== + +regjsparser@^0.8.2: + version "0.8.4" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.8.4.tgz#8a14285ffcc5de78c5b95d62bbf413b6bc132d5f" + integrity sha512-J3LABycON/VNEu3abOviqGHuB/LOtOQj8SKmfP9anY5GfAVw/SPjwzSjxGjbZXIxbGfqTHtJw58C2Li/WkStmA== + dependencies: + jsesc "~0.5.0" + +relateurl@^0.2.7: + version "0.2.7" + resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" + integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= + +renderkid@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-3.0.0.tgz#5fd823e4d6951d37358ecc9a58b1f06836b6268a" + integrity sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg== + dependencies: + css-select "^4.1.3" + dom-converter "^0.2.0" + htmlparser2 "^6.1.0" + lodash "^4.17.21" + strip-ansi "^6.0.1" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve-pathname@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" + integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== + +resolve-url-loader@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz#d50d4ddc746bb10468443167acf800dcd6c3ad57" + 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" + +resolve.exports@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9" + integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== + +resolve@^1.12.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.0: + version "1.22.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" + integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== + dependencies: + is-core-module "^2.8.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +resolve@^2.0.0-next.3: + version "2.0.0-next.3" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.3.tgz#d41016293d4a8586a39ca5d9b5f15cbea1f55e46" + integrity sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q== + dependencies: + is-core-module "^2.2.0" + path-parse "^1.0.6" + +retry@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^3.0.0, rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +rollup-plugin-terser@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d" + integrity sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ== + dependencies: + "@babel/code-frame" "^7.10.4" + jest-worker "^26.2.1" + serialize-javascript "^4.0.0" + terser "^5.0.0" + +rollup@^2.43.1: + version "2.70.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.70.1.tgz#824b1f1f879ea396db30b0fc3ae8d2fead93523e" + integrity sha512-CRYsI5EuzLbXdxC6RnYhOuRdtz4bhejPMSWjsFLfVM/7w/85n2szZv6yExqUXsBdz5KT8eoubeyDUDjhLHEslA== + optionalDependencies: + fsevents "~2.3.2" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sanitize.css@*: + version "13.0.0" + resolved "https://registry.yarnpkg.com/sanitize.css/-/sanitize.css-13.0.0.tgz#2675553974b27964c75562ade3bd85d79879f173" + integrity sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA== + +sass-loader@^12.3.0: + version "12.6.0" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-12.6.0.tgz#5148362c8e2cdd4b950f3c63ac5d16dbfed37bcb" + integrity sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA== + dependencies: + klona "^2.0.4" + neo-async "^2.6.2" + +sass@^1.34.1: + version "1.49.9" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.49.9.tgz#b15a189ecb0ca9e24634bae5d1ebc191809712f9" + integrity sha512-YlYWkkHP9fbwaFRZQRXgDi3mXZShslVmmo+FVK3kHLUELHHEYrCmL1x6IUjC7wLS6VuJSAFXRQS/DxdsC4xL1A== + dependencies: + chokidar ">=3.0.0 <4.0.0" + immutable "^4.0.0" + source-map-js ">=0.6.2 <2.0.0" + +sax@~1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + +saxes@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d" + integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw== + dependencies: + xmlchars "^2.2.0" + +scheduler@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" + integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + +schema-utils@2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" + integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== + dependencies: + "@types/json-schema" "^7.0.4" + ajv "^6.12.2" + ajv-keywords "^3.4.1" + +schema-utils@^2.6.5: + version "2.7.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" + integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== + dependencies: + "@types/json-schema" "^7.0.5" + ajv "^6.12.4" + ajv-keywords "^3.5.2" + +schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" + integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +schema-utils@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.0.0.tgz#60331e9e3ae78ec5d16353c467c34b3a0a1d3df7" + 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" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= + +selfsigned@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.0.0.tgz#e927cd5377cbb0a1075302cff8df1042cc2bce5b" + integrity sha512-cUdFiCbKoa1mZ6osuJs2uDHrs0k0oprsKveFiiaBKCNq3SYyb5gs2HxhQyDNLCmL51ZZThqi4YNDpCK6GOP1iQ== + dependencies: + node-forge "^1.2.0" + +semver@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" + integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== + +semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +semver@^7.3.2, semver@^7.3.5: + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + dependencies: + lru-cache "^6.0.0" + +send@0.17.2: + version "0.17.2" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.2.tgz#926622f76601c41808012c8bf1688fe3906f7820" + integrity sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "1.8.1" + mime "1.6.0" + ms "2.1.3" + on-finished "~2.3.0" + range-parser "~1.2.1" + statuses "~1.5.0" + +serialize-javascript@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" + integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== + dependencies: + randombytes "^2.1.0" + +serialize-javascript@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" + integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== + dependencies: + randombytes "^2.1.0" + +serve-index@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + integrity sha1-03aNabHn2C5c4FD/9bRTvqEqkjk= + 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" + +serve-static@1.14.2: + version "1.14.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.2.tgz#722d6294b1d62626d41b43a013ece4598d292bfa" + integrity sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.17.2" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shell-quote@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123" + integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw== + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +signal-exit@^3.0.2, signal-exit@^3.0.3: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= + dependencies: + is-arrayish "^0.3.1" + +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +slash@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" + integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== + +sockjs@^0.3.21: + version "0.3.24" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" + integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== + dependencies: + faye-websocket "^0.11.3" + uuid "^8.3.2" + websocket-driver "^0.7.4" + +source-list-map@^2.0.0, source-list-map@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" + integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== + +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +source-map-loader@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-3.0.1.tgz#9ae5edc7c2d42570934be4c95d1ccc6352eba52d" + integrity sha512-Vp1UsfyPvgujKQzi4pyDiTOnE3E4H+yHvkVRN3c/9PJmQS4CQJExvcDvaX/D+RV+xQben9HJ56jMJS3CgUeWyA== + dependencies: + abab "^2.0.5" + iconv-lite "^0.6.3" + source-map-js "^1.0.1" + +source-map-support@^0.5.6, source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@^0.5.0, source-map@^0.5.7: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + +source-map@^0.7.3, source-map@~0.7.2: + version "0.7.3" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" + integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== + +source-map@^0.8.0-beta.0: + version "0.8.0-beta.0" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.8.0-beta.0.tgz#d4c1bb42c3f7ee925f005927ba10709e0d1d1f11" + integrity sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA== + dependencies: + whatwg-url "^7.0.0" + +sourcemap-codec@^1.4.8: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + 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" + +spdy@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" + 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" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +stable@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" + integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== + +stack-utils@^2.0.3: + version "2.0.5" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5" + integrity sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA== + dependencies: + escape-string-regexp "^2.0.0" + +stackframe@^1.1.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.2.1.tgz#1033a3473ee67f08e2f2fc8eba6aef4f845124e1" + integrity sha512-h88QkzREN/hy8eRdyNhhsO7RSJ5oyTqxxmmn0dzBIMUclZsjpfmrsg81vp8mjjAs2vAZ72nyWxRUwSwmh0e4xg== + +"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +string-length@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" + integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== + dependencies: + char-regex "^1.0.2" + strip-ansi "^6.0.0" + +string-length@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-5.0.1.tgz#3d647f497b6e8e8d41e422f7e0b23bc536c8381e" + integrity sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow== + dependencies: + char-regex "^2.0.0" + strip-ansi "^7.0.1" + +string-natural-compare@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" + integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + 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" + +string.prototype.matchall@^4.0.6: + version "4.0.7" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz#8e6ecb0d8a1fb1fda470d81acecb2dba057a481d" + integrity sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.1" + get-intrinsic "^1.1.1" + has-symbols "^1.0.3" + internal-slot "^1.0.3" + regexp.prototype.flags "^1.4.1" + side-channel "^1.0.4" + +string.prototype.trimend@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" + integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + +string.prototype.trimstart@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" + integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +stringify-object@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" + integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== + dependencies: + get-own-enumerable-property-symbols "^3.0.0" + is-obj "^1.0.1" + is-regexp "^1.0.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.0, strip-ansi@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" + integrity sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw== + dependencies: + ansi-regex "^6.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + +strip-bom@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== + +strip-comments@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-comments/-/strip-comments-2.0.1.tgz#4ad11c3fbcac177a67a40ac224ca339ca1c1ba9b" + integrity sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +style-loader@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575" + integrity sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ== + +stylehacks@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.1.0.tgz#a40066490ca0caca04e96c6b02153ddc39913520" + integrity sha512-SzLmvHQTrIWfSgljkQCw2++C9+Ne91d/6Sp92I8c5uHTcy/PgeHamwITIbBW9wnFTY/3ZfSXR9HIL6Ikqmcu6Q== + dependencies: + browserslist "^4.16.6" + postcss-selector-parser "^6.0.4" + +stylis@4.0.13: + version "4.0.13" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.13.tgz#f5db332e376d13cc84ecfe5dace9a2a51d954c91" + integrity sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag== + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.0.0, supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-hyperlinks@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz#4f77b42488765891774b70c79babd87f9bd594bb" + integrity sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ== + dependencies: + has-flag "^4.0.0" + supports-color "^7.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +svg-parser@^2.0.2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" + integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== + +svgo@^1.2.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167" + integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw== + 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" + +svgo@^2.7.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24" + 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" + +symbol-tree@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" + integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== + +tailwindcss@^3.0.2: + version "3.0.23" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.0.23.tgz#c620521d53a289650872a66adfcb4129d2200d10" + integrity sha512-+OZOV9ubyQ6oI2BXEhzw4HrqvgcARY38xv3zKcjnWtMIZstEsXdI9xftd1iB7+RbOnj2HOEzkA0OyB5BaSxPQA== + dependencies: + arg "^5.0.1" + chalk "^4.1.2" + chokidar "^3.5.3" + color-name "^1.1.4" + cosmiconfig "^7.0.1" + detective "^5.2.0" + didyoumean "^1.2.2" + dlv "^1.1.3" + fast-glob "^3.2.11" + glob-parent "^6.0.2" + is-glob "^4.0.3" + normalize-path "^3.0.0" + object-hash "^2.2.0" + postcss "^8.4.6" + postcss-js "^4.0.0" + postcss-load-config "^3.1.0" + postcss-nested "5.0.6" + postcss-selector-parser "^6.0.9" + postcss-value-parser "^4.2.0" + quick-lru "^5.1.1" + resolve "^1.22.0" + +tapable@^1.0.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" + integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== + +tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +temp-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-2.0.0.tgz#bde92b05bdfeb1516e804c9c00ad45177f31321e" + integrity sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg== + +tempy@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tempy/-/tempy-0.6.0.tgz#65e2c35abc06f1124a97f387b08303442bde59f3" + integrity sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw== + dependencies: + is-stream "^2.0.0" + temp-dir "^2.0.0" + type-fest "^0.16.0" + unique-string "^2.0.0" + +terminal-link@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" + integrity sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ== + dependencies: + ansi-escapes "^4.2.1" + supports-hyperlinks "^2.0.0" + +terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.2.5: + version "5.3.1" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1.tgz#0320dcc270ad5372c1e8993fabbd927929773e54" + integrity sha512-GvlZdT6wPQKbDNW/GDQzZFg/j4vKU96yl2q6mcUkzKOgW4gwf1Z8cZToUCrz31XHlPWH8MVb1r2tFtdDtTGJ7g== + dependencies: + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.0" + source-map "^0.6.1" + terser "^5.7.2" + +terser@^5.0.0, terser@^5.10.0, terser@^5.7.2: + version "5.12.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.12.1.tgz#4cf2ebed1f5bceef5c83b9f60104ac4a78b49e9c" + integrity sha512-NXbs+7nisos5E+yXwAD+y7zrcTkMqb0dEJxIGtSKPdCBzopf7ni4odPul2aechpV7EXNvOudYOX2bb5tln1jbQ== + dependencies: + acorn "^8.5.0" + commander "^2.20.0" + source-map "~0.7.2" + source-map-support "~0.5.20" + +test-exclude@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" + integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^7.1.4" + minimatch "^3.0.4" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + +throat@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375" + integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w== + +thunky@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" + integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== + +timsort@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" + integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= + +tiny-invariant@^1.0.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9" + integrity sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg== + +tiny-warning@^1.0.0, tiny-warning@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + +tmpl@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" + integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +tough-cookie@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4" + integrity sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg== + dependencies: + psl "^1.1.33" + punycode "^2.1.1" + universalify "^0.1.2" + +tr46@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" + integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk= + dependencies: + punycode "^2.1.0" + +tr46@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240" + integrity sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw== + dependencies: + punycode "^2.1.1" + +tryer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" + integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA== + +tsconfig-paths@^3.12.0: + version "3.14.1" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" + integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tslib@^1.8.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tslib@^2.0.3: + version "2.3.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" + integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== + +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= + dependencies: + prelude-ls "~1.1.2" + +type-detect@4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +type-fest@^0.16.0: + version "0.16.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.16.0.tgz#3240b891a78b0deae910dbeb86553e552a148860" + integrity sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg== + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typedarray-to-buffer@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" + +unbox-primitive@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" + integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw== + dependencies: + function-bind "^1.1.1" + has-bigints "^1.0.1" + has-symbols "^1.0.2" + which-boxed-primitive "^1.0.2" + +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" + integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== + +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== + dependencies: + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" + +unicode-match-property-value-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz#1a01aa57247c14c568b89775a54938788189a714" + integrity sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw== + +unicode-property-aliases-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz#0a36cb9a585c4f6abd51ad1deddb285c165297c8" + integrity sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ== + +unique-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" + integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== + dependencies: + crypto-random-string "^2.0.0" + +universalify@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + +unquote@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" + integrity sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ= + +upath@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" + integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +util.promisify@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee" + 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" + +utila@~0.4: + version "0.4.0" + resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" + integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw= + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +v8-compile-cache@^2.0.3: + version "2.3.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" + integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== + +v8-to-istanbul@^8.1.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz#77b752fd3975e31bbcef938f85e9bd1c7a8d60ed" + integrity sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^1.6.0" + source-map "^0.7.3" + +value-equal@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" + integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + +w3c-hr-time@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" + integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ== + dependencies: + browser-process-hrtime "^1.0.0" + +w3c-xmlserializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz#3e7104a05b75146cc60f564380b7f683acf1020a" + integrity sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA== + dependencies: + xml-name-validator "^3.0.0" + +walker@^1.0.7: + version "1.0.8" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" + integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== + dependencies: + makeerror "1.0.12" + +watchpack@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.3.1.tgz#4200d9447b401156eeca7767ee610f8809bc9d25" + integrity sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +wbuf@^1.1.0, wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== + dependencies: + minimalistic-assert "^1.0.0" + +webidl-conversions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" + integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== + +webidl-conversions@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" + integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA== + +webidl-conversions@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" + integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== + +webpack-dev-middleware@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.1.tgz#aa079a8dedd7e58bfeab358a9af7dab304cee57f" + integrity sha512-81EujCKkyles2wphtdrnPg/QqegC/AtqNH//mQkBYSMqwFVCQrxM6ktB2O/SPlZy7LqeEfTbV3cZARGQz6umhg== + dependencies: + colorette "^2.0.10" + memfs "^3.4.1" + mime-types "^2.1.31" + range-parser "^1.2.1" + schema-utils "^4.0.0" + +webpack-dev-server@^4.6.0: + version "4.7.4" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.7.4.tgz#d0ef7da78224578384e795ac228d8efb63d5f945" + integrity sha512-nfdsb02Zi2qzkNmgtZjkrMOcXnYZ6FLKcQwpxT7MvmHKc+oTtDsBju8j+NMyAygZ9GW1jMEUpy3itHtqgEhe1A== + 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/sockjs" "^0.3.33" + "@types/ws" "^8.2.2" + ansi-html-community "^0.0.8" + bonjour "^3.5.0" + chokidar "^3.5.3" + colorette "^2.0.10" + compression "^1.7.4" + connect-history-api-fallback "^1.6.0" + default-gateway "^6.0.3" + del "^6.0.0" + express "^4.17.1" + graceful-fs "^4.2.6" + html-entities "^2.3.2" + http-proxy-middleware "^2.0.0" + ipaddr.js "^2.0.1" + open "^8.0.9" + p-retry "^4.5.0" + portfinder "^1.0.28" + schema-utils "^4.0.0" + selfsigned "^2.0.0" + serve-index "^1.9.1" + sockjs "^0.3.21" + spdy "^4.0.2" + strip-ansi "^7.0.0" + webpack-dev-middleware "^5.3.1" + ws "^8.4.2" + +webpack-manifest-plugin@^4.0.2: + version "4.1.1" + resolved "https://registry.yarnpkg.com/webpack-manifest-plugin/-/webpack-manifest-plugin-4.1.1.tgz#10f8dbf4714ff93a215d5a45bcc416d80506f94f" + integrity sha512-YXUAwxtfKIJIKkhg03MKuiFAD72PlrqCiwdwO4VEXdRO5V0ORCNwaOwAZawPZalCbmH9kBDmXnNeQOw+BIEiow== + dependencies: + tapable "^2.0.0" + webpack-sources "^2.2.0" + +webpack-sources@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" + integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== + dependencies: + source-list-map "^2.0.0" + source-map "~0.6.1" + +webpack-sources@^2.2.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-2.3.1.tgz#570de0af163949fe272233c2cefe1b56f74511fd" + integrity sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA== + dependencies: + source-list-map "^2.0.1" + source-map "^0.6.1" + +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@^5.64.4: + version "5.70.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.70.0.tgz#3461e6287a72b5e6e2f4872700bc8de0d7500e6d" + integrity sha512-ZMWWy8CeuTTjCxbeaQI21xSswseF2oNOwc70QSKNePvmxE7XW36i7vpBMYZFAUHPwQiEbNGCEYIOOlyRbdGmxw== + 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.4.1" + acorn-import-assertions "^1.7.6" + browserslist "^4.14.5" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.9.2" + 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-better-errors "^1.0.2" + 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.3.1" + webpack-sources "^3.2.3" + +websocket-driver@>=0.5.1, websocket-driver@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + +whatwg-encoding@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" + integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== + dependencies: + iconv-lite "0.4.24" + +whatwg-fetch@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c" + integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA== + +whatwg-mimetype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" + integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== + +whatwg-url@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" + integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + +whatwg-url@^8.0.0, whatwg-url@^8.5.0: + version "8.7.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77" + integrity sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg== + dependencies: + lodash "^4.7.0" + tr46 "^2.1.0" + webidl-conversions "^6.1.0" + +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.3, word-wrap@~1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +workbox-background-sync@6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-6.5.2.tgz#28be9bf89b8e4e0379d45903280c7c12f4df836f" + integrity sha512-EjG37LSMDJ1TFlFg56wx6YXbH4/NkG09B9OHvyxx+cGl2gP5OuOzsCY3rOPJSpbcz6jpuA40VIC3HzSD4OvE1g== + dependencies: + idb "^6.1.4" + workbox-core "6.5.2" + +workbox-broadcast-update@6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/workbox-broadcast-update/-/workbox-broadcast-update-6.5.2.tgz#b1f32bb40a9dcb5b05ca27e09fb7c01a0a126182" + integrity sha512-DjJYraYnprTZE/AQNoeogaxI1dPuYmbw+ZJeeP8uXBSbg9SNv5wLYofQgywXeRepv4yr/vglMo9yaHUmBMc+4Q== + dependencies: + workbox-core "6.5.2" + +workbox-build@6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/workbox-build/-/workbox-build-6.5.2.tgz#774faafd84b1dc94b74739ceb5d8ff367748523b" + integrity sha512-TVi4Otf6fgwikBeMpXF9n0awHfZTMNu/nwlMIT9W+c13yvxkmDFMPb7vHYK6RUmbcxwPnz4I/R+uL76+JxG4JQ== + dependencies: + "@apideck/better-ajv-errors" "^0.3.1" + "@babel/core" "^7.11.1" + "@babel/preset-env" "^7.11.0" + "@babel/runtime" "^7.11.2" + "@rollup/plugin-babel" "^5.2.0" + "@rollup/plugin-node-resolve" "^11.2.1" + "@rollup/plugin-replace" "^2.4.1" + "@surma/rollup-plugin-off-main-thread" "^2.2.3" + ajv "^8.6.0" + common-tags "^1.8.0" + fast-json-stable-stringify "^2.1.0" + fs-extra "^9.0.1" + glob "^7.1.6" + lodash "^4.17.20" + pretty-bytes "^5.3.0" + rollup "^2.43.1" + rollup-plugin-terser "^7.0.0" + source-map "^0.8.0-beta.0" + stringify-object "^3.3.0" + strip-comments "^2.0.1" + tempy "^0.6.0" + upath "^1.2.0" + workbox-background-sync "6.5.2" + workbox-broadcast-update "6.5.2" + workbox-cacheable-response "6.5.2" + workbox-core "6.5.2" + workbox-expiration "6.5.2" + workbox-google-analytics "6.5.2" + workbox-navigation-preload "6.5.2" + workbox-precaching "6.5.2" + workbox-range-requests "6.5.2" + workbox-recipes "6.5.2" + workbox-routing "6.5.2" + workbox-strategies "6.5.2" + workbox-streams "6.5.2" + workbox-sw "6.5.2" + workbox-window "6.5.2" + +workbox-cacheable-response@6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-6.5.2.tgz#d9252eb99f0d0fceb70f63866172f4eaac56a3e8" + integrity sha512-UnHGih6xqloV808T7ve1iNKZMbpML0jGLqkkmyXkJbZc5j16+HRSV61Qrh+tiq3E3yLvFMGJ3AUBODOPNLWpTg== + dependencies: + workbox-core "6.5.2" + +workbox-core@6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-6.5.2.tgz#f5e06a22c6cb4651d3e13107443d972fdbd47364" + integrity sha512-IlxLGQf+wJHCR+NM0UWqDh4xe/Gu6sg2i4tfZk6WIij34IVk9BdOQgi6WvqSHd879jbQIUgL2fBdJUJyAP5ypQ== + +workbox-expiration@6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-6.5.2.tgz#ee6ed755a220a0b375d67831f9237e4dcbccb59c" + integrity sha512-5Hfp0uxTZJrgTiy9W7AjIIec+9uTOtnxY/tRBm4DbqcWKaWbVTa+izrKzzOT4MXRJJIJUmvRhWw4oo8tpmMouw== + dependencies: + idb "^6.1.4" + workbox-core "6.5.2" + +workbox-google-analytics@6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-6.5.2.tgz#a79fa7a40824873baaa333dcd72d1fdf1c53adf5" + integrity sha512-8SMar+N0xIreP5/2we3dwtN1FUmTMScoopL86aKdXBpio8vXc8Oqb5fCJG32ialjN8BAOzDqx/FnGeCtkIlyvw== + dependencies: + workbox-background-sync "6.5.2" + workbox-core "6.5.2" + workbox-routing "6.5.2" + workbox-strategies "6.5.2" + +workbox-navigation-preload@6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-6.5.2.tgz#ffb3d9d5cdb881a3824851707da221dbb0bb3f23" + integrity sha512-iqDNWWMswjCsZuvGFDpcX1Z8InBVAlVBELJ28xShsWWntALzbtr0PXMnm2WHkXCc56JimmGldZi1N5yDPiTPOg== + dependencies: + workbox-core "6.5.2" + +workbox-precaching@6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-6.5.2.tgz#a3117b4d3eb61ce8d01b9dfc063c48155bd7f9d3" + integrity sha512-OZAlQ8AAT20KugGKKuJMHdQ8X1IyNQaLv+mPTHj+8Dmv8peBq5uWNzs4g/1OSFmXsbXZ6a1CBC6YtQWVPhJQ9w== + dependencies: + workbox-core "6.5.2" + workbox-routing "6.5.2" + workbox-strategies "6.5.2" + +workbox-range-requests@6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-6.5.2.tgz#b8b7e5b5830fecc22f0a1d8815457921df2e5bf9" + integrity sha512-zi5VqF1mWqfCyJLTMXn1EuH/E6nisqWDK1VmOJ+TnjxGttaQrseOhMn+BMvULFHeF8AvrQ0ogfQ6bSv0rcfAlg== + dependencies: + workbox-core "6.5.2" + +workbox-recipes@6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/workbox-recipes/-/workbox-recipes-6.5.2.tgz#19f47ec25a8788c65d0cc8d217cbebc0bbbb5c63" + integrity sha512-2lcUKMYDiJKvuvRotOxLjH2z9K7jhj8GNUaHxHNkJYbTCUN3LsX1cWrsgeJFDZ/LgI565t3fntpbG9J415ZBXA== + dependencies: + workbox-cacheable-response "6.5.2" + workbox-core "6.5.2" + workbox-expiration "6.5.2" + workbox-precaching "6.5.2" + workbox-routing "6.5.2" + workbox-strategies "6.5.2" + +workbox-routing@6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-6.5.2.tgz#e0ad46246ba51224fd57eff0dd46891b3220cb9a" + integrity sha512-nR1w5PjF6IVwo0SX3oE88LhmGFmTnqqU7zpGJQQPZiKJfEKgDENQIM9mh3L1ksdFd9Y3CZVkusopHfxQvit/BA== + dependencies: + workbox-core "6.5.2" + +workbox-strategies@6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-6.5.2.tgz#56b02e6959c6391351011fc2e5b0829aff1ed859" + integrity sha512-fgbwaUMxbG39BHjJIs2y2X21C0bmf1Oq3vMQxJ1hr6y5JMJIm8rvKCcf1EIdAr+PjKdSk4ddmgyBQ4oO8be4Uw== + dependencies: + workbox-core "6.5.2" + +workbox-streams@6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-6.5.2.tgz#2fb6ba307f7d2cbda63f64522a197be868b4ea25" + integrity sha512-ovD0P4UrgPtZ2Lfc/8E8teb1RqNOSZr+1ZPqLR6sGRZnKZviqKbQC3zVvvkhmOIwhWbpL7bQlWveLVONHjxd5w== + dependencies: + workbox-core "6.5.2" + workbox-routing "6.5.2" + +workbox-sw@6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/workbox-sw/-/workbox-sw-6.5.2.tgz#2f5dca0e96c61a450fccf0405095ddf1b6f43bc7" + integrity sha512-2KhlYqtkoqlnPdllj2ujXUKRuEFsRDIp6rdE4l1PsxiFHRAFaRTisRQpGvRem5yxgXEr+fcEKiuZUW2r70KZaw== + +workbox-webpack-plugin@^6.4.1: + version "6.5.2" + resolved "https://registry.yarnpkg.com/workbox-webpack-plugin/-/workbox-webpack-plugin-6.5.2.tgz#0cf6e1d23d5107a88fd8502fd4f534215e1dd298" + integrity sha512-StrJ7wKp5tZuGVcoKLVjFWlhDy+KT7ZWsKnNcD6F08wA9Cpt6JN+PLIrplcsTHbQpoAV8+xg6RvcG0oc9z+RpQ== + 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.2" + +workbox-window@6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/workbox-window/-/workbox-window-6.5.2.tgz#46d6412cd57039bdf3d5dd914ad21fb3f98fe980" + integrity sha512-2kZH37r9Wx8swjEOL4B8uGM53lakMxsKkQ7mOKzGA/QAn/DQTEZGrdHWtypk2tbhKY5S0jvPS+sYDnb2Z3378A== + dependencies: + "@types/trusted-types" "^2.0.2" + workbox-core "6.5.2" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +write-file-atomic@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" + 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" + +ws@^7.4.6: + version "7.5.7" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.7.tgz#9e0ac77ee50af70d58326ecff7e85eb3fa375e67" + integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A== + +ws@^8.4.2: + version "8.5.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" + integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg== + +xml-name-validator@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" + integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== + +xmlchars@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" + integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== + +xtend@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + 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" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/maubot-src/maubot/matrix.py b/maubot-src/maubot/matrix.py new file mode 100644 index 0000000..8a811af --- /dev/null +++ b/maubot-src/maubot/matrix.py @@ -0,0 +1,314 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +from typing import Any, Awaitable +from html import escape +import asyncio + +import attr + +from mautrix.client import Client as MatrixClient, SyncStream +from mautrix.errors import DecryptionError +from mautrix.types import ( + BaseMessageEventContentFuncs, + EncryptedEvent, + Event, + EventID, + EventType, + Format, + MessageEvent, + MessageEventContent, + MessageType, + RelatesTo, + RoomID, + TextMessageEventContent, +) +from mautrix.util import markdown +from mautrix.util.formatter import EntityType, MarkdownString, MatrixParser + + +class HumanReadableString(MarkdownString): + def format(self, entity_type: EntityType, **kwargs) -> MarkdownString: + if entity_type == EntityType.URL and kwargs["url"] != self.text: + self.text = f"{self.text} ({kwargs['url']})" + return self + return super(HumanReadableString, self).format(entity_type, **kwargs) + + +class MaubotHTMLParser(MatrixParser[HumanReadableString]): + fs = HumanReadableString + + +async def parse_formatted( + message: str, allow_html: bool = False, render_markdown: bool = True +) -> tuple[str, str]: + if render_markdown: + html = markdown.render(message, allow_html=allow_html) + elif allow_html: + html = message + else: + return message, escape(message) + text = (await MaubotHTMLParser().parse(html)).text + if len(text) > 100 and len(text) + len(html) > 40000: + text = text[:100] + "[long message cut off]" + return text, html + + +class MaubotMessageEvent(MessageEvent): + client: MaubotMatrixClient + disable_reply: bool + + def __init__(self, base: MessageEvent, client: MaubotMatrixClient): + super().__init__( + **{a.name.lstrip("_"): getattr(base, a.name) for a in attr.fields(MessageEvent)} + ) + self.client = client + self.disable_reply = client.disable_replies + + async def respond( + self, + content: str | MessageEventContent, + event_type: EventType = EventType.ROOM_MESSAGE, + markdown: bool = True, + allow_html: bool = False, + reply: bool | str = False, + in_thread: bool | None = None, + edits: EventID | MessageEvent | None = None, + extra_content: dict[str, Any] | None = None, + ) -> EventID: + """ + Respond to the message. + + Args: + content: The content to respond with. If this is a string, it will be passed to + :func:`parse_formatted` with the markdown and allow_html flags. + Otherwise, the content is used as-is + event_type: The type of event to send. + markdown: When content is a string, should it be parsed as markdown? + allow_html: When content is a string, should it allow raw HTML? + reply: Should the response be sent as a reply to this event? + in_thread: Should the response be sent in a thread with this event? + By default (``None``), the response will be in a thread if this event is in a + thread. If set to ``False``, the response will never be in a thread. If set to + ``True``, the response will always be in a thread, creating one with this event as + the root if necessary. + edits: An event ID or MessageEvent to edit. If set, the reply and in_thread parameters + are ignored, as edits can't change the reply or thread status. + extra_content: Extra content to add to the event. + + Returns: + The ID of the response event. + """ + if isinstance(content, str): + content = TextMessageEventContent(msgtype=MessageType.NOTICE, body=content) + if allow_html or markdown: + content.format = Format.HTML + content.body, content.formatted_body = await parse_formatted( + content.body, render_markdown=markdown, allow_html=allow_html + ) + if edits: + content.set_edit(edits) + if ( + not edits + and in_thread is not False + and ( + in_thread + or ( + isinstance(self.content, BaseMessageEventContentFuncs) + and self.content.get_thread_parent() + ) + ) + ): + content.set_thread_parent(self) + if reply and not edits: + if reply != "force" and self.disable_reply: + content.body = f"{self.sender}: {content.body}" + fmt_body = content.formatted_body or escape(content.body).replace("\n", "
") + content.formatted_body = ( + f'' + f"{self.sender}" + f": {fmt_body}" + ) + else: + content.set_reply(self) + if extra_content: + for k, v in extra_content.items(): + content[k] = v + return await self.client.send_message_event(self.room_id, event_type, content) + + def reply( + self, + content: str | MessageEventContent, + event_type: EventType = EventType.ROOM_MESSAGE, + markdown: bool = True, + allow_html: bool = False, + in_thread: bool | None = None, + extra_content: dict[str, Any] | None = None, + ) -> Awaitable[EventID]: + """ + Reply to the message. The parameters are the same as :meth:`respond`, + but ``reply`` is always ``True`` and ``edits`` is not supported. + + Args: + content: The content to respond with. If this is a string, it will be passed to + :func:`parse_formatted` with the markdown and allow_html flags. + Otherwise, the content is used as-is + event_type: The type of event to send. + markdown: When content is a string, should it be parsed as markdown? + allow_html: When content is a string, should it allow raw HTML? + in_thread: Should the response be sent in a thread with this event? + By default (``None``), the response will be in a thread if this event is in a + thread. If set to ``False``, the response will never be in a thread. If set to + ``True``, the response will always be in a thread, creating one with this event as + the root if necessary. + extra_content: Extra content to add to the event. + + Returns: + The ID of the response event. + """ + return self.respond( + content, + event_type, + markdown=markdown, + reply=True, + in_thread=in_thread, + allow_html=allow_html, + extra_content=extra_content, + ) + + def mark_read(self) -> Awaitable[None]: + """ + Mark this event as read. + """ + return self.client.send_receipt(self.room_id, self.event_id, "m.read") + + def react(self, key: str) -> Awaitable[EventID]: + """ + React to this event with the given key. + + Args: + key: The key to react with. Often an unicode emoji. + + Returns: + The ID of the reaction event. + + Examples: + >>> evt: MaubotMessageEvent + >>> evt.react("🐈️") + """ + return self.client.react(self.room_id, self.event_id, key) + + def redact(self, reason: str | None = None) -> Awaitable[EventID]: + """ + Redact this event. + + Args: + reason: Optionally, the reason for redacting the event. + + Returns: + The ID of the redaction event. + """ + return self.client.redact(self.room_id, self.event_id, reason=reason) + + def edit( + self, + content: str | MessageEventContent, + event_type: EventType = EventType.ROOM_MESSAGE, + markdown: bool = True, + allow_html: bool = False, + ) -> Awaitable[EventID]: + """ + Edit this event. Note that other clients will only render the edit if it was sent by the + same user who's doing the editing. + + Args: + content: The new content for the event. If this is a string, it will be passed to + :func:`parse_formatted` with the markdown and allow_html flags. + Otherwise, the content is used as-is. + event_type: The type of event to edit into. + markdown: When content is a string, should it be parsed as markdown? + allow_html: When content is a string, should it allow raw HTML? + + Returns: + The ID of the edit event. + """ + return self.respond( + content, event_type, markdown=markdown, edits=self, allow_html=allow_html + ) + + +class MaubotMatrixClient(MatrixClient): + disable_replies: bool + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.disable_replies = False + + async def send_markdown( + self, + room_id: RoomID, + markdown: str, + *, + allow_html: bool = False, + render_markdown: bool = True, + msgtype: MessageType = MessageType.TEXT, + edits: EventID | MessageEvent | None = None, + relates_to: RelatesTo | None = None, + extra_content: dict[str, Any] = None, + **kwargs, + ) -> EventID: + content = TextMessageEventContent(msgtype=msgtype, format=Format.HTML) + content.body, content.formatted_body = await parse_formatted( + markdown, + allow_html=allow_html, + render_markdown=render_markdown, + ) + if relates_to: + if edits: + raise ValueError("Can't use edits and relates_to at the same time.") + content.relates_to = relates_to + elif edits: + content.set_edit(edits) + if extra_content: + for k, v in extra_content.items(): + content[k] = v + return await self.send_message(room_id, content, **kwargs) + + def dispatch_event(self, event: Event, source: SyncStream) -> list[asyncio.Task]: + if isinstance(event, MessageEvent) and not isinstance(event, MaubotMessageEvent): + event = MaubotMessageEvent(event, self) + elif source != SyncStream.INTERNAL: + event.client = self + return super().dispatch_event(event, source) + + async def get_event(self, room_id: RoomID, event_id: EventID) -> Event: + evt = await super().get_event(room_id, event_id) + if isinstance(evt, EncryptedEvent) and self.crypto: + try: + self.crypto_log.trace(f"get_event: Decrypting {evt.event_id} in {evt.room_id}...") + decrypted = await self.crypto.decrypt_megolm_event(evt) + except DecryptionError as e: + self.crypto_log.warning(f"get_event: Failed to decrypt {evt.event_id}: {e}") + return + self.crypto_log.trace(f"get_event: Decrypted {evt.event_id}: {decrypted}") + evt = decrypted + if isinstance(evt, MessageEvent): + evt.content.trim_reply_fallback() + return MaubotMessageEvent(evt, self) + else: + evt.client = self + return evt diff --git a/maubot-src/maubot/plugin_base.py b/maubot-src/maubot/plugin_base.py new file mode 100644 index 0000000..1be15e0 --- /dev/null +++ b/maubot-src/maubot/plugin_base.py @@ -0,0 +1,142 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +from typing import TYPE_CHECKING, Awaitable +from abc import ABC +from asyncio import AbstractEventLoop + +from aiohttp import ClientSession +from yarl import URL + +from mautrix.util.async_db import Database, UpgradeTable +from mautrix.util.config import BaseProxyConfig +from mautrix.util.logging import TraceLogger + +from .scheduler import BasicScheduler + +if TYPE_CHECKING: + from sqlalchemy.engine.base import Engine + + from .client import MaubotMatrixClient + from .loader import BasePluginLoader + from .plugin_server import PluginWebApp + + +class Plugin(ABC): + client: MaubotMatrixClient + http: ClientSession + id: str + log: TraceLogger + loop: AbstractEventLoop + loader: BasePluginLoader + sched: BasicScheduler + config: BaseProxyConfig | None + database: Engine | Database | None + webapp: PluginWebApp | None + webapp_url: URL | None + + def __init__( + self, + client: MaubotMatrixClient, + loop: AbstractEventLoop, + http: ClientSession, + instance_id: str, + log: TraceLogger, + config: BaseProxyConfig | None, + database: Engine | Database | None, + webapp: PluginWebApp | None, + webapp_url: str | None, + loader: BasePluginLoader, + ) -> None: + self.sched = BasicScheduler(log=log.getChild("scheduler")) + self.client = client + self.loop = loop + self.http = http + self.id = instance_id + self.log = log + self.config = config + self.database = database + self.webapp = webapp + self.webapp_url = URL(webapp_url) if webapp_url else None + self.loader = loader + self._handlers_at_startup = [] + + def register_handler_class(self, obj) -> None: + warned_webapp = False + for key in dir(obj): + val = getattr(obj, key) + try: + if val.__mb_event_handler__: + for event_type in val.__mb_event_types__: + self._handlers_at_startup.append((val, event_type)) + self.client.add_event_handler(event_type, val) + except AttributeError: + pass + try: + web_handlers = val.__mb_web_handler__ + except AttributeError: + pass + else: + if len(web_handlers) > 0 and self.webapp is None: + if not warned_webapp: + self.log.warning( + f"{type(obj).__name__} has web handlers, but the webapp" + " feature isn't enabled in the plugin's maubot.yaml" + ) + warned_webapp = True + continue + for method, path, kwargs in web_handlers: + self.webapp.add_route(method=method, path=path, handler=val, **kwargs) + + async def pre_start(self) -> None: + pass + + async def internal_start(self) -> None: + await self.pre_start() + self.register_handler_class(self) + await self.start() + + async def start(self) -> None: + pass + + async def pre_stop(self) -> None: + pass + + async def internal_stop(self) -> None: + await self.pre_stop() + for func, event_type in self._handlers_at_startup: + self.client.remove_event_handler(event_type, func) + if self.webapp is not None: + self.webapp.clear() + self.sched.stop() + await self.stop() + + async def stop(self) -> None: + pass + + @classmethod + def get_config_class(cls) -> type[BaseProxyConfig] | None: + return None + + @classmethod + def get_db_upgrade_table(cls) -> UpgradeTable | None: + return None + + def on_external_config_update(self) -> Awaitable[None] | None: + if self.config: + self.config.load_and_update() + return None diff --git a/maubot-src/maubot/plugin_server.py b/maubot-src/maubot/plugin_server.py new file mode 100644 index 0000000..e5c246c --- /dev/null +++ b/maubot-src/maubot/plugin_server.py @@ -0,0 +1,90 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +from typing import Awaitable, Callable +from functools import partial + +from aiohttp import hdrs, web +from yarl import URL + +Handler = Callable[[web.Request], Awaitable[web.Response]] +Middleware = Callable[[web.Request, Handler], Awaitable[web.Response]] + + +class PluginWebApp(web.UrlDispatcher): + def __init__(self): + super().__init__() + self._middleware: list[Middleware] = [] + + def add_middleware(self, middleware: Middleware) -> None: + self._middleware.append(middleware) + + def remove_middleware(self, middleware: Middleware) -> None: + self._middleware.remove(middleware) + + def clear(self) -> None: + self._resources = [] + self._named_resources = {} + self._middleware = [] + self._resource_index = {} + self._matched_sub_app_resources = [] + + async def handle(self, request: web.Request) -> web.StreamResponse: + match_info = await self.resolve(request) + match_info.freeze() + resp = None + request._match_info = match_info + expect = request.headers.get(hdrs.EXPECT) + if expect: + resp = await match_info.expect_handler(request) + await request.writer.drain() + if resp is None: + handler = match_info.handler + for middleware in self._middleware: + handler = partial(middleware, handler=handler) + resp = await handler(request) + return resp + + +class PrefixResource(web.Resource): + def __init__(self, prefix, *, name=None): + assert not prefix or prefix.startswith("/"), prefix + assert prefix in ("", "/") or not prefix.endswith("/"), prefix + super().__init__(name=name) + self._prefix = URL.build(path=prefix).raw_path + + @property + def canonical(self): + return self._prefix + + def get_info(self): + return {"path": self._prefix} + + def url_for(self): + return URL.build(path=self._prefix, encoded=True) + + def add_prefix(self, prefix): + assert prefix.startswith("/") + assert not prefix.endswith("/") + assert len(prefix) > 1 + self._prefix = prefix + self._prefix + + def _match(self, path: str) -> dict: + return {} if self.raw_match(path) else None + + def raw_match(self, path: str) -> bool: + return path and path.startswith(self._prefix) diff --git a/maubot-src/maubot/py.typed b/maubot-src/maubot/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/maubot-src/maubot/scheduler.py b/maubot-src/maubot/scheduler.py new file mode 100644 index 0000000..0cb39ed --- /dev/null +++ b/maubot-src/maubot/scheduler.py @@ -0,0 +1,159 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2024 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +from typing import Awaitable, Callable +import asyncio +import logging + + +class BasicScheduler: + background_loop: asyncio.Task | None + tasks: set[asyncio.Task] + log: logging.Logger + + def __init__(self, log: logging.Logger) -> None: + self.log = log + self.tasks = set() + + def _find_caller(self) -> str: + try: + file_name, line_number, function_name, _ = self.log.findCaller() + return f"{function_name} at {file_name}:{line_number}" + except ValueError: + return "unknown function" + + def run_periodically( + self, + period: float | int, + func: Callable[[], Awaitable], + run_task_in_background: bool = False, + catch_errors: bool = True, + ) -> asyncio.Task: + """ + Run a function periodically in the background. + + Args: + period: The period in seconds between each call to the function. + func: The function to run. No parameters will be provided, + use :meth:`functools.partial` if you need to pass parameters. + run_task_in_background: If ``True``, the function will be run in a background task. + If ``False`` (the default), the loop will wait for the task to return before + sleeping for the next period. + catch_errors: Whether the scheduler should catch and log any errors. + If ``False``, errors will be raised, and the caller must await the returned task + to find errors. This parameter has no effect if ``run_task_in_background`` + is ``True``. + + Returns: + The asyncio task object representing the background loop. + """ + task = asyncio.create_task( + self._call_periodically( + period, + func, + caller=self._find_caller(), + catch_errors=catch_errors, + run_task_in_background=run_task_in_background, + ) + ) + self._register_task(task) + return task + + def run_later( + self, delay: float | int, coro: Awaitable, catch_errors: bool = True + ) -> asyncio.Task: + """ + Run a coroutine after a delay. + + Examples: + >>> self.sched.run_later(5, self.async_task(meow=True)) + + Args: + delay: The delay in seconds to await the coroutine after. + coro: The coroutine to await. + catch_errors: Whether the scheduler should catch and log any errors. + If ``False``, errors will be raised, and the caller must await the returned task + to find errors. + + Returns: + The asyncio task object representing the scheduled task. + """ + task = asyncio.create_task( + self._call_with_delay( + delay, coro, caller=self._find_caller(), catch_errors=catch_errors + ) + ) + self._register_task(task) + return task + + def _register_task(self, task: asyncio.Task) -> None: + self.tasks.add(task) + task.add_done_callback(self.tasks.discard) + + async def _call_periodically( + self, + period: float | int, + func: Callable[[], Awaitable], + caller: str, + catch_errors: bool, + run_task_in_background: bool, + ) -> None: + while True: + try: + await asyncio.sleep(period) + if run_task_in_background: + self._register_task( + asyncio.create_task(self._call_periodically_background(func(), caller)) + ) + else: + await func() + except asyncio.CancelledError: + raise + except Exception: + if catch_errors: + self.log.exception(f"Uncaught error in background loop (created in {caller})") + else: + raise + + async def _call_periodically_background(self, coro: Awaitable, caller: str) -> None: + try: + await coro + except asyncio.CancelledError: + raise + except Exception: + self.log.exception(f"Uncaught error in background loop subtask (created in {caller})") + + async def _call_with_delay( + self, delay: float | int, coro: Awaitable, caller: str, catch_errors: bool + ) -> None: + try: + await asyncio.sleep(delay) + await coro + except asyncio.CancelledError: + raise + except Exception: + if catch_errors: + self.log.exception(f"Uncaught error in scheduled task (created in {caller})") + else: + raise + + def stop(self) -> None: + """ + Stop all scheduled tasks and background loops. + """ + for task in self.tasks: + task.cancel(msg="Scheduler stopped") diff --git a/maubot-src/maubot/server.py b/maubot-src/maubot/server.py new file mode 100644 index 0000000..dd1101e --- /dev/null +++ b/maubot-src/maubot/server.py @@ -0,0 +1,182 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +from typing import Callable +import asyncio +import json +import logging + +from aiohttp import hdrs, web +from aiohttp.abc import AbstractAccessLogger +from yarl import URL +import pkg_resources + +from mautrix.api import Method, PathBuilder + +from .__meta__ import __version__ +from .config import Config +from .plugin_server import PluginWebApp, PrefixResource + + +class AccessLogger(AbstractAccessLogger): + def log(self, request: web.Request, response: web.Response, time: int): + self.logger.info( + f'{request.remote} "{request.method} {request.path} ' + f"{response.status} {response.body_length} " + f'in {round(time, 4)}s"' + ) + + +class MaubotServer: + log: logging.Logger = logging.getLogger("maubot.server") + plugin_routes: dict[str, PluginWebApp] + + def __init__( + self, management_api: web.Application, config: Config, loop: asyncio.AbstractEventLoop + ) -> None: + self.loop = loop or asyncio.get_event_loop() + self.app = web.Application(loop=self.loop, client_max_size=100 * 1024 * 1024) + self.config = config + + self.setup_appservice() + self.app.add_subapp("/_matrix/maubot/v1", management_api) + self.setup_instance_subapps() + self.setup_management_ui() + + self.runner = web.AppRunner(self.app, access_log_class=AccessLogger) + + async def handle_plugin_path(self, request: web.Request) -> web.StreamResponse: + for path, app in self.plugin_routes.items(): + if request.path.startswith(path): + request = request.clone( + rel_url=request.rel_url.with_path( + request.rel_url.path[len(path) - 1 :] + ).with_query(request.query_string) + ) + return await app.handle(request) + return web.Response(status=404) + + def get_instance_subapp(self, instance_id: str) -> tuple[PluginWebApp, str]: + subpath = self.config["server.plugin_base_path"] + instance_id + "/" + url = self.config["server.public_url"] + subpath + try: + return self.plugin_routes[subpath], url + except KeyError: + app = PluginWebApp() + self.plugin_routes[subpath] = app + return app, url + + def remove_instance_webapp(self, instance_id: str) -> None: + try: + subpath = self.config["server.plugin_base_path"] + instance_id + "/" + self.plugin_routes.pop(subpath).clear() + except KeyError: + return + + def setup_instance_subapps(self) -> None: + self.plugin_routes = {} + resource = PrefixResource(self.config["server.plugin_base_path"].rstrip("/")) + resource.add_route(hdrs.METH_ANY, self.handle_plugin_path) + self.app.router.register_resource(resource) + + def setup_appservice(self) -> None: + as_path = PathBuilder("/_matrix/appservice/v1") + self.add_route(Method.PUT, as_path.transactions, self.handle_transaction) + + def setup_management_ui(self) -> None: + ui_base = self.config["server.ui_base_path"] + if ui_base == "/": + ui_base = "" + directory = self.config[ + "server.override_resource_path" + ] or pkg_resources.resource_filename("maubot", "management/frontend/build") + self.app.router.add_static(f"{ui_base}/static", f"{directory}/static") + self.setup_static_root_files(directory, ui_base) + + with open(f"{directory}/index.html", "r") as file: + index_html = file.read() + + @web.middleware + async def frontend_404_middleware(request: web.Request, handler) -> web.Response: + if hasattr(handler, "__self__") and isinstance(handler.__self__, web.StaticResource): + try: + return await handler(request) + except web.HTTPNotFound: + return web.Response(body=index_html, content_type="text/html") + return await handler(request) + + async def ui_base_redirect(_: web.Request) -> web.Response: + raise web.HTTPFound(f"{ui_base}/") + + self.app.middlewares.append(frontend_404_middleware) + self.app.router.add_get( + f"{ui_base}/", lambda _: web.Response(body=index_html, content_type="text/html") + ) + self.app.router.add_get(ui_base, ui_base_redirect) + + @staticmethod + def _static_data(data: bytes, mime: str) -> Callable[[web.Request], web.Response]: + def fn(_: web.Request) -> web.Response: + return web.Response(body=data, content_type=mime) + + return fn + + def setup_static_root_files(self, directory: str, ui_base: str) -> None: + files = { + "asset-manifest.json": "application/json", + "manifest.json": "application/json", + "favicon.png": "image/png", + } + for file, mime in files.items(): + with open(f"{directory}/{file}", "rb") as stream: + data = stream.read() + self.app.router.add_get(f"{ui_base}/{file}", self._static_data(data, mime)) + + public_url = self.config["server.public_url"] + public_url_path = "" + if public_url: + public_url_path = URL(public_url).path.rstrip("/") + + api_path = f"{public_url_path}/_matrix/maubot/v1" + + path_prefix_response_body = json.dumps({"api_path": api_path.rstrip("/")}) + self.app.router.add_get( + f"{ui_base}/paths.json", + lambda _: web.Response( + body=path_prefix_response_body, content_type="application/json" + ), + ) + + def add_route(self, method: Method, path: PathBuilder, handler) -> None: + self.app.router.add_route(method.value, str(path), handler) + + async def start(self) -> None: + await self.runner.setup() + site = web.TCPSite(self.runner, self.config["server.hostname"], self.config["server.port"]) + await site.start() + self.log.info(f"Listening on {site.name}") + + async def stop(self) -> None: + await self.runner.shutdown() + await self.runner.cleanup() + + @staticmethod + async def version(_: web.Request) -> web.Response: + return web.json_response({"version": __version__}) + + async def handle_transaction(self, request: web.Request) -> web.Response: + return web.Response(status=501) diff --git a/maubot-src/maubot/standalone/Dockerfile b/maubot-src/maubot/standalone/Dockerfile new file mode 100644 index 0000000..54623f2 --- /dev/null +++ b/maubot-src/maubot/standalone/Dockerfile @@ -0,0 +1,32 @@ +FROM docker.io/alpine:3.21 + +RUN apk add --no-cache \ + python3 py3-pip py3-setuptools py3-wheel \ + py3-aiohttp \ + py3-attrs \ + py3-bcrypt \ + py3-cffi \ + ca-certificates \ + su-exec \ + py3-psycopg2 \ + py3-ruamel.yaml \ + py3-jinja2 \ + py3-packaging \ + py3-markdown \ + py3-cffi \ + py3-olm \ + py3-pycryptodome \ + py3-unpaddedbase64 + +COPY requirements.txt /opt/maubot/requirements.txt +COPY optional-requirements.txt /opt/maubot/optional-requirements.txt +RUN cd /opt/maubot \ + && apk add --no-cache --virtual .build-deps \ + python3-dev \ + libffi-dev \ + build-base \ + && pip3 install --break-system-packages -r requirements.txt -r optional-requirements.txt \ + && apk del .build-deps + +COPY . /opt/maubot +RUN cd /opt/maubot && pip3 install --break-system-packages . diff --git a/maubot-src/maubot/standalone/__init__.py b/maubot-src/maubot/standalone/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/maubot-src/maubot/standalone/__main__.py b/maubot-src/maubot/standalone/__main__.py new file mode 100644 index 0000000..c320af4 --- /dev/null +++ b/maubot-src/maubot/standalone/__main__.py @@ -0,0 +1,424 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +from typing import cast +import argparse +import asyncio +import copy +import importlib +import logging.config +import os.path +import signal +import sys + +from aiohttp import ClientSession, hdrs, web +from ruamel.yaml import YAML +from ruamel.yaml.comments import CommentedMap +from yarl import URL + +from mautrix.appservice import AppServiceServerMixin +from mautrix.client import SyncStream +from mautrix.types import ( + BaseMessageEventContentFuncs, + Event, + EventType, + Filter, + Membership, + RoomEventFilter, + RoomFilter, + StrippedStateEvent, +) +from mautrix.util.async_db import Database, Scheme +from mautrix.util.config import BaseMissingError, RecursiveDict +from mautrix.util.logging import TraceLogger + +from ..__meta__ import __version__ +from ..loader import DatabaseType, PluginMeta +from ..matrix import MaubotMatrixClient +from ..plugin_base import Plugin +from ..plugin_server import PluginWebApp, PrefixResource +from ..server import AccessLogger +from .config import Config +from .database import NextBatch, upgrade_table +from .loader import FileSystemLoader + +crypto_import_error = None + +try: + from mautrix.crypto import OlmMachine, PgCryptoStateStore, PgCryptoStore +except ImportError as err: + crypto_import_error = err + OlmMachine = PgCryptoStateStore = PgCryptoStore = None + +parser = argparse.ArgumentParser( + description="A plugin-based Matrix bot system -- standalone mode.", + prog="python -m maubot.standalone", +) +parser.add_argument( + "-c", + "--config", + type=str, + default="config.yaml", + metavar="", + help="the path to your config file", +) +parser.add_argument( + "-b", + "--base-config", + type=str, + default="pkg://maubot.standalone/example-config.yaml", + metavar="", + help="the path to the example config (for automatic config updates)", +) +parser.add_argument( + "-m", + "--meta", + type=str, + default="maubot.yaml", + metavar="", + help="the path to your plugin metadata file", +) +args = parser.parse_args() + +config = Config(args.config, args.base_config) +config.load() +try: + config.update() +except BaseMissingError: + print("No example config found, not updating config") +except Exception as e: + print("Failed to update config:", e) + +logging.config.dictConfig(copy.deepcopy(config["logging"])) + +log = logging.getLogger("maubot.init") + +log.debug(f"Loading plugin metadata from {args.meta}") +yaml = YAML() +with open(args.meta, "r") as meta_file: + meta: PluginMeta = PluginMeta.deserialize(yaml.load(meta_file.read())) + +if "/" in meta.main_class: + module, main_class = meta.main_class.split("/", 1) +else: + module = meta.modules[-1] + main_class = meta.main_class + +if args.meta != "maubot.yaml" and os.path.dirname(args.meta) != "": + sys.path.append(os.path.dirname(args.meta)) +bot_module = importlib.import_module(module) +plugin: type[Plugin] = getattr(bot_module, main_class) +loader = FileSystemLoader(os.path.dirname(args.meta), meta) + +log.info(f"Initializing standalone {meta.id} v{meta.version} on maubot {__version__}") + +db = Database.create( + config["database"], + db_args=config.get("database_opts", {}), + ignore_foreign_tables=True, + log=logging.getLogger("maubot.db.standalone.upgrade"), + upgrade_table=upgrade_table, +) + +user_id = config["user.credentials.id"] +device_id = config["user.credentials.device_id"] +homeserver = config["user.credentials.homeserver"] +access_token = config["user.credentials.access_token"] +appservice_listener = config["user.appservice"] + +crypto_store = state_store = None +if device_id and not OlmMachine: + log.warning( + "device_id set in config, but encryption dependencies not installed", + exc_info=crypto_import_error, + ) +elif device_id: + crypto_store = PgCryptoStore(account_id=user_id, pickle_key="mau.crypto", db=db) + state_store = PgCryptoStateStore(db) + +bot_config = None +if not meta.config and "base-config.yaml" in meta.extra_files: + log.warning( + "base-config.yaml in extra files, but config is not set to true. " + "Assuming legacy plugin and loading config." + ) + meta.config = True +if meta.config: + log.debug("Loading config") + config_class = plugin.get_config_class() + + def load() -> CommentedMap: + return config["plugin_config"] + + def load_base() -> RecursiveDict[CommentedMap]: + return RecursiveDict(config.load_base()["plugin_config"], CommentedMap) + + def save(data: RecursiveDict[CommentedMap]) -> None: + config["plugin_config"] = data + config.save() + + try: + bot_config = config_class(load=load, load_base=load_base, save=save) + bot_config.load_and_update() + except Exception: + log.fatal("Failed to load plugin config", exc_info=True) + sys.exit(1) + +if meta.webapp: + web_app = web.Application() + web_runner = web.AppRunner(web_app, access_log_class=AccessLogger) + web_base_path = config["server.base_path"].rstrip("/") + public_url = str(URL(config["server.public_url"]) / web_base_path.lstrip("/")).rstrip("/") + plugin_webapp = PluginWebApp() + + async def _handle_plugin_request(req: web.Request) -> web.StreamResponse: + if req.path.startswith(web_base_path): + path_override = req.rel_url.path[len(web_base_path) :] + url_override = req.rel_url.with_path(path_override).with_query(req.query_string) + req = req.clone(rel_url=url_override) + return await plugin_webapp.handle(req) + return web.Response(status=404) + + resource = PrefixResource(web_base_path) + resource.add_route(hdrs.METH_ANY, _handle_plugin_request) + web_app.router.register_resource(resource) +elif appservice_listener: + web_app = web.Application() + web_runner = web.AppRunner(web_app, access_log_class=AccessLogger) + public_url = plugin_webapp = None +else: + web_app = web_runner = public_url = plugin_webapp = None + +loop = asyncio.get_event_loop() + +client: MaubotMatrixClient | None = None +bot: Plugin | None = None +appservice: AppServiceServerMixin | None = None + + +if appservice_listener: + assert web_app is not None, "web_app is always set when appservice_listener is set" + appservice = AppServiceServerMixin( + ephemeral_events=True, + encryption_events=True, + log=logging.getLogger("maubot.appservice"), + hs_token=config["user.hs_token"], + ) + appservice.register_routes(web_app) + + @appservice.matrix_event_handler + async def handle_appservice_event(evt: Event) -> None: + if isinstance(evt.content, BaseMessageEventContentFuncs): + evt.content.trim_reply_fallback() + fake_sync_stream = SyncStream.JOINED_ROOM + if evt.type.is_ephemeral: + fake_sync_stream |= SyncStream.EPHEMERAL + else: + fake_sync_stream |= SyncStream.TIMELINE + setattr(evt, "source", fake_sync_stream) + tasks = client.dispatch_manual_event(evt.type, evt, include_global_handlers=True) + await asyncio.gather(*tasks) + + +async def main(): + http_client = ClientSession(loop=loop) + + global client, bot + + await db.start() + nb = await NextBatch(db, user_id).load() + + client_log = logging.getLogger("maubot.client").getChild(user_id) + client = MaubotMatrixClient( + mxid=user_id, + base_url=homeserver, + token=access_token, + client_session=http_client, + loop=loop, + log=client_log, + sync_store=nb, + state_store=state_store, + device_id=device_id, + ) + if appservice: + client.api.as_user_id = user_id + client.ignore_first_sync = config["user.ignore_first_sync"] + client.ignore_initial_sync = config["user.ignore_initial_sync"] + if crypto_store: + await crypto_store.upgrade_table.upgrade(db) + await state_store.upgrade_table.upgrade(db) + await crypto_store.open() + + client.crypto = OlmMachine(client, crypto_store, state_store) + if appservice: + appservice.otk_handler = client.crypto.handle_as_otk_counts + appservice.device_list_handler = client.crypto.handle_as_device_lists + appservice.to_device_handler = client.crypto.handle_as_to_device_event + client.api.as_device_id = device_id + crypto_device_id = await crypto_store.get_device_id() + if crypto_device_id and crypto_device_id != device_id: + log.fatal( + "Mismatching device ID in crypto store and config " + f"(store: {crypto_device_id}, config: {device_id})" + ) + sys.exit(10) + await client.crypto.load() + if not crypto_device_id: + await crypto_store.put_device_id(device_id) + log.debug("Enabled encryption support") + + if web_runner: + await web_runner.setup() + site = web.TCPSite(web_runner, config["server.hostname"], config["server.port"]) + await site.start() + log.info(f"Web server listening on {site.name}") + + while True: + try: + whoami = await client.whoami() + except Exception as e: + log.error( + f"Failed to connect to homeserver: {type(e).__name__}: {e}" + " - retrying in 10 seconds..." + ) + await asyncio.sleep(10) + continue + if whoami.user_id != user_id: + log.fatal(f"User ID mismatch: configured {user_id}, but server said {whoami.user_id}") + sys.exit(11) + elif whoami.device_id and device_id and whoami.device_id != device_id: + log.fatal( + f"Device ID mismatch: configured {device_id}, " + f"but server said {whoami.device_id}" + ) + sys.exit(12) + log.debug(f"Confirmed connection as {whoami.user_id} / {whoami.device_id}") + break + + if config["user.sync"]: + if not nb.filter_id: + filter_id = await client.create_filter( + Filter(room=RoomFilter(timeline=RoomEventFilter(limit=50))) + ) + await nb.put_filter_id(filter_id) + _ = client.start(nb.filter_id) + elif appservice_listener and crypto_store and not client.crypto.account.shared: + await client.crypto.share_keys() + + if config["user.autojoin"]: + log.debug("Autojoin is enabled") + + @client.on(EventType.ROOM_MEMBER) + async def _handle_invite(evt: StrippedStateEvent) -> None: + if evt.state_key == client.mxid and evt.content.membership == Membership.INVITE: + await client.join_room(evt.room_id) + + displayname, avatar_url = config["user.displayname"], config["user.avatar_url"] + if avatar_url != "disable": + await client.set_avatar_url(avatar_url) + if displayname != "disable": + await client.set_displayname(displayname) + + plugin_log = cast(TraceLogger, logging.getLogger("maubot.instance.__main__")) + if meta.database: + if meta.database_type == DatabaseType.SQLALCHEMY: + import sqlalchemy as sql + + plugin_db = sql.create_engine(config["database"]) + if db.scheme == Scheme.SQLITE: + log.warning( + "Using SQLite with legacy plugins in standalone mode can cause database " + "locking issues. Switching to Postgres or updating the plugin to use the " + "new asyncpg/aiosqlite database interface is recommended." + ) + elif meta.database_type == DatabaseType.ASYNCPG: + plugin_db = db + upgrade_table = plugin.get_db_upgrade_table() + if upgrade_table: + await upgrade_table.upgrade(plugin_db) + else: + log.fatal(f"Unsupported database type {meta.database_type}") + sys.exit(13) + else: + plugin_db = None + bot = plugin( + client=client, + loop=loop, + http=http_client, + instance_id="__main__", + log=plugin_log, + config=bot_config, + database=plugin_db, + webapp=plugin_webapp, + webapp_url=public_url, + loader=loader, + ) + + await bot.internal_start() + + +async def stop(suppress_stop_error: bool = False) -> None: + if client: + client.stop() + if bot: + try: + await bot.internal_stop() + except Exception: + if not suppress_stop_error: + log.exception("Error stopping bot") + if web_runner and web_runner.server: + try: + await web_runner.shutdown() + await web_runner.cleanup() + except RuntimeError: + if not suppress_stop_error: + await db.stop() + raise + await db.stop() + + +signal.signal(signal.SIGINT, signal.default_int_handler) +signal.signal(signal.SIGTERM, signal.default_int_handler) + + +try: + log.info("Starting plugin") + loop.run_until_complete(main()) +except SystemExit: + loop.run_until_complete(stop(suppress_stop_error=True)) + loop.close() + raise +except (Exception, KeyboardInterrupt) as e: + if isinstance(e, KeyboardInterrupt): + log.info("Startup interrupted, stopping") + else: + log.fatal("Failed to start plugin", exc_info=True) + loop.run_until_complete(stop(suppress_stop_error=True)) + loop.close() + sys.exit(1) + +try: + log.info("Startup completed, running forever") + loop.run_forever() +except KeyboardInterrupt: + log.info("Interrupt received, stopping") + loop.run_until_complete(stop()) + loop.close() + sys.exit(0) +except Exception: + log.fatal("Fatal error in bot", exc_info=True) + sys.exit(1) diff --git a/maubot-src/maubot/standalone/config.py b/maubot-src/maubot/standalone/config.py new file mode 100644 index 0000000..f2c90a0 --- /dev/null +++ b/maubot-src/maubot/standalone/config.py @@ -0,0 +1,52 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from typing import Any +import os + +from mautrix.util.config import BaseFileConfig, ConfigUpdateHelper + + +class Config(BaseFileConfig): + def __getitem__(self, key: str) -> Any: + try: + return os.environ[f"MAUBOT_{key.replace('.', '_').upper()}"] + except KeyError: + return super().__getitem__(key) + + def do_update(self, helper: ConfigUpdateHelper) -> None: + copy, _, base = helper + copy("user.credentials.id") + copy("user.credentials.homeserver") + copy("user.credentials.access_token") + copy("user.credentials.device_id") + copy("user.sync") + copy("user.appservice") + copy("user.hs_token") + copy("user.autojoin") + copy("user.displayname") + copy("user.avatar_url") + copy("user.ignore_initial_sync") + copy("user.ignore_first_sync") + if "server" in base: + copy("server.hostname") + copy("server.port") + copy("server.base_path") + copy("server.public_url") + copy("database") + copy("database_opts") + if "plugin_config" in base: + copy("plugin_config") + copy("logging") diff --git a/maubot-src/maubot/standalone/database.py b/maubot-src/maubot/standalone/database.py new file mode 100644 index 0000000..f09906d --- /dev/null +++ b/maubot-src/maubot/standalone/database.py @@ -0,0 +1,73 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +import logging + +from attr import dataclass + +from mautrix.client import SyncStore +from mautrix.types import FilterID, SyncToken, UserID +from mautrix.util.async_db import Connection, Database, UpgradeTable + +upgrade_table = UpgradeTable( + version_table_name="standalone_version", log=logging.getLogger("maubot.db") +) + + +@upgrade_table.register(description="Initial revision") +async def upgrade_v1(conn: Connection) -> None: + await conn.execute( + """CREATE TABLE IF NOT EXISTS standalone_next_batch ( + user_id TEXT PRIMARY KEY, + next_batch TEXT, + filter_id TEXT + )""" + ) + + +find_q = "SELECT next_batch, filter_id FROM standalone_next_batch WHERE user_id=$1" +insert_q = "INSERT INTO standalone_next_batch (user_id, next_batch, filter_id) VALUES ($1, $2, $3)" +update_nb_q = "UPDATE standalone_next_batch SET next_batch=$1 WHERE user_id=$2" +update_filter_q = "UPDATE standalone_next_batch SET filter_id=$1 WHERE user_id=$2" + + +@dataclass +class NextBatch(SyncStore): + db: Database + user_id: UserID + next_batch: SyncToken = "" + filter_id: FilterID = "" + + async def load(self) -> NextBatch: + row = await self.db.fetchrow(find_q, self.user_id) + if row is not None: + self.next_batch = row["next_batch"] + self.filter_id = row["filter_id"] + else: + await self.db.execute(insert_q, self.user_id, self.next_batch, self.filter_id) + return self + + async def put_filter_id(self, filter_id: FilterID) -> None: + self.filter_id = filter_id + await self.db.execute(update_filter_q, self.filter_id, self.user_id) + + async def put_next_batch(self, next_batch: SyncToken) -> None: + self.next_batch = next_batch + await self.db.execute(update_nb_q, self.next_batch, self.user_id) + + async def get_next_batch(self) -> SyncToken: + return self.next_batch diff --git a/maubot-src/maubot/standalone/example-config.yaml b/maubot-src/maubot/standalone/example-config.yaml new file mode 100644 index 0000000..8ee4e43 --- /dev/null +++ b/maubot-src/maubot/standalone/example-config.yaml @@ -0,0 +1,78 @@ +# Bot account details +user: + credentials: + id: "@bot:example.com" + homeserver: https://example.com + access_token: foo + # If you want to enable encryption, set the device ID corresponding to the access token here. + # When using an appservice, you should use appservice login manually to generate a device ID and access token. + device_id: null + # Enable /sync? This is not needed for purely unencrypted webhook-based bots, but is necessary in most other cases. + sync: true + # Receive appservice transactions? This will add a /_matrix/app/v1/transactions endpoint on + # the HTTP server configured below. The base_path will not be applied for the /transactions path. + appservice: false + # When appservice mode is enabled, the hs_token for the appservice. + hs_token: null + # Automatically accept invites? + autojoin: false + # The displayname and avatar URL to set for the bot on startup. + # Set to "disable" to not change the the current displayname/avatar. + displayname: Standalone Bot + avatar_url: mxc://maunium.net/AKwRzQkTbggfVZGEqexbYLIO + + # Should events from the initial sync be ignored? This should usually always be true. + ignore_initial_sync: true + # Should events from the first sync after starting be ignored? This can be set to false + # if you want the bot to handle messages that were sent while the bot was down. + ignore_first_sync: true + +# Web server settings. These will only take effect if the plugin requests it using `webapp: true` in the meta file, +# or if user -> appservice is set to true. +server: + # The IP and port to listen to. + hostname: 0.0.0.0 + port: 8080 + # The base path where the plugin's web resources will be served. Unlike the normal mode, + # the webserver is dedicated for a single bot in standalone mode, so the default path + # is just /. If you want to emulate normal mode, set this to /_matrix/maubot/plugin/something + base_path: / + # The public URL where the resources are available. The base path is automatically appended to this. + public_url: https://example.com + +# The database for the plugin. Used for plugin data, the sync token and e2ee data (if enabled). +# SQLite and Postgres are supported. +database: sqlite:bot.db + +# Additional arguments for asyncpg.create_pool() or sqlite3.connect() +# https://magicstack.github.io/asyncpg/current/api/index.html#asyncpg.pool.create_pool +# https://docs.python.org/3/library/sqlite3.html#sqlite3.connect +# For sqlite, min_size is used as the connection thread pool size and max_size is ignored. +database_opts: + min_size: 1 + max_size: 10 + +# Config for the plugin. Refer to the plugin's base-config.yaml to find what (if anything) to put here. +plugin_config: {} + +# Standard Python logging configuration +logging: + version: 1 + formatters: + colored: + (): maubot.lib.color_log.ColorFormatter + format: "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s" + handlers: + console: + class: logging.StreamHandler + formatter: colored + loggers: + maubot: + level: DEBUG + mau: + level: DEBUG + aiohttp: + level: INFO + root: + level: DEBUG + handlers: [console] diff --git a/maubot-src/maubot/standalone/loader.py b/maubot-src/maubot/standalone/loader.py new file mode 100644 index 0000000..3d5a907 --- /dev/null +++ b/maubot-src/maubot/standalone/loader.py @@ -0,0 +1,44 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2022 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + +import os +import os.path + +from ..loader import BasePluginLoader, PluginMeta + + +class FileSystemLoader(BasePluginLoader): + def __init__(self, path: str, meta: PluginMeta) -> None: + self.path = path + self.meta = meta + + @property + def source(self) -> str: + return self.path + + def sync_read_file(self, path: str) -> bytes: + with open(os.path.join(self.path, path), "rb") as file: + return file.read() + + async def read_file(self, path: str) -> bytes: + return self.sync_read_file(path) + + def sync_list_files(self, directory: str) -> list[str]: + return os.listdir(os.path.join(self.path, directory)) + + async def list_files(self, directory: str) -> list[str]: + return self.sync_list_files(directory) diff --git a/maubot-src/maubot/testing/__init__.py b/maubot-src/maubot/testing/__init__.py new file mode 100644 index 0000000..1fcdfc0 --- /dev/null +++ b/maubot-src/maubot/testing/__init__.py @@ -0,0 +1,17 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2023 Aurélien Bompard +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from .bot import TestBot, make_message # noqa: F401 +from .fixtures import * # noqa: F401,F403 diff --git a/maubot-src/maubot/testing/bot.py b/maubot-src/maubot/testing/bot.py new file mode 100644 index 0000000..0519016 --- /dev/null +++ b/maubot-src/maubot/testing/bot.py @@ -0,0 +1,100 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2023 Aurélien Bompard +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +import asyncio +import time + +from attr import dataclass + +from maubot.matrix import MaubotMatrixClient, MaubotMessageEvent +from mautrix.api import HTTPAPI +from mautrix.types import ( + EventContent, + EventType, + MessageEvent, + MessageType, + RoomID, + TextMessageEventContent, +) + + +@dataclass +class MatrixEvent: + room_id: RoomID + event_type: EventType + content: EventContent + kwargs: dict + + +class TestBot: + """A mocked bot used for testing purposes. + + Send messages to the mock Matrix server with the ``send()`` method. + Look into the ``responded`` list to get what server has replied. + """ + + def __init__(self, mxid="@botname:example.com", mxurl="http://matrix.example.com"): + api = HTTPAPI(base_url=mxurl) + self.client = MaubotMatrixClient(api=api) + self.responded = [] + self.client.mxid = mxid + self.client.send_message_event = self._mock_send_message_event + + async def _mock_send_message_event(self, room_id, event_type, content, txn_id=None, **kwargs): + self.responded.append( + MatrixEvent(room_id=room_id, event_type=event_type, content=content, kwargs=kwargs) + ) + + async def dispatch(self, event_type: EventType, event): + tasks = self.client.dispatch_manual_event(event_type, event, force_synchronous=True) + return await asyncio.gather(*tasks) + + async def send( + self, + content, + html=None, + room_id="testroom", + msg_type=MessageType.TEXT, + sender="@dummy:example.com", + timestamp=None, + ): + event = make_message( + content, + html=html, + room_id=room_id, + msg_type=msg_type, + sender=sender, + timestamp=timestamp, + ) + await self.dispatch(EventType.ROOM_MESSAGE, MaubotMessageEvent(event, self.client)) + + +def make_message( + content, + html=None, + room_id="testroom", + msg_type=MessageType.TEXT, + sender="@dummy:example.com", + timestamp=None, +): + """Make a Matrix message event.""" + return MessageEvent( + type=EventType.ROOM_MESSAGE, + room_id=room_id, + event_id="test", + sender=sender, + timestamp=timestamp or int(time.time() * 1000), + content=TextMessageEventContent(msgtype=msg_type, body=content, formatted_body=html), + ) diff --git a/maubot-src/maubot/testing/fixtures.py b/maubot-src/maubot/testing/fixtures.py new file mode 100644 index 0000000..e975782 --- /dev/null +++ b/maubot-src/maubot/testing/fixtures.py @@ -0,0 +1,135 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2023 Aurélien Bompard +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from pathlib import Path +import asyncio +import logging + +from ruamel.yaml import YAML +import aiohttp +import pytest +import pytest_asyncio + +from maubot import Plugin +from maubot.loader import PluginMeta +from maubot.standalone.loader import FileSystemLoader +from mautrix.util.async_db import Database +from mautrix.util.config import BaseProxyConfig, RecursiveDict +from mautrix.util.logging import TraceLogger + +from .bot import TestBot + + +@pytest_asyncio.fixture +async def maubot_test_bot(): + return TestBot() + + +@pytest.fixture +def maubot_upgrade_table(): + return None + + +@pytest.fixture +def maubot_plugin_path(): + return Path(".") + + +@pytest.fixture +def maubot_plugin_meta(maubot_plugin_path): + yaml = YAML() + with open(maubot_plugin_path.joinpath("maubot.yaml")) as fh: + plugin_meta = PluginMeta.deserialize(yaml.load(fh.read())) + return plugin_meta + + +@pytest_asyncio.fixture +async def maubot_plugin_db(tmp_path, maubot_plugin_meta, maubot_upgrade_table): + if not maubot_plugin_meta.get("database", False): + return + db_path = tmp_path.joinpath("maubot-tests.db").as_posix() + db = Database.create( + f"sqlite:{db_path}", + upgrade_table=maubot_upgrade_table, + log=logging.getLogger("db"), + ) + await db.start() + yield db + await db.stop() + + +@pytest.fixture +def maubot_plugin_class(): + return Plugin + + +@pytest.fixture +def maubot_plugin_config_class(): + return BaseProxyConfig + + +@pytest.fixture +def maubot_plugin_config_dict(): + return {} + + +@pytest.fixture +def maubot_plugin_config_overrides(): + return {} + + +@pytest.fixture +def maubot_plugin_config( + maubot_plugin_path, + maubot_plugin_config_class, + maubot_plugin_config_dict, + maubot_plugin_config_overrides, +): + yaml = YAML() + with open(maubot_plugin_path.joinpath("base-config.yaml")) as fh: + base_config = RecursiveDict(yaml.load(fh)) + maubot_plugin_config_dict.update(maubot_plugin_config_overrides) + return maubot_plugin_config_class( + load=lambda: maubot_plugin_config_dict, + load_base=lambda: base_config, + save=lambda c: None, + ) + + +@pytest_asyncio.fixture +async def maubot_plugin( + maubot_test_bot, + maubot_plugin_db, + maubot_plugin_class, + maubot_plugin_path, + maubot_plugin_config, + maubot_plugin_meta, +): + loader = FileSystemLoader(maubot_plugin_path, maubot_plugin_meta) + async with aiohttp.ClientSession() as http: + instance = maubot_plugin_class( + client=maubot_test_bot.client, + loop=asyncio.get_running_loop(), + http=http, + instance_id="tests", + log=TraceLogger("test"), + config=maubot_plugin_config, + database=maubot_plugin_db, + webapp=None, + webapp_url=None, + loader=loader, + ) + await instance.internal_start() + yield instance diff --git a/maubot-src/nginx.conf b/maubot-src/nginx.conf new file mode 100644 index 0000000..7765d7a --- /dev/null +++ b/maubot-src/nginx.conf @@ -0,0 +1,58 @@ +worker_processes auto; +pid /run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$request_time"'; + + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + access_log /dev/stdout main; + error_log /dev/stderr info; + + upstream maubot_upstream { + server 127.0.0.1:3001; + } + + server { + listen 3000 default_server; + listen [::]:3000 default_server; + + client_max_body_size 64m; + + location = /healthz { + add_header Content-Type text/plain; + return 200 "OK"; + } + + location / { + proxy_pass http://maubot_upstream; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_buffering off; + } + } +} diff --git a/maubot-src/optional-requirements.txt b/maubot-src/optional-requirements.txt new file mode 100644 index 0000000..3676b0e --- /dev/null +++ b/maubot-src/optional-requirements.txt @@ -0,0 +1,15 @@ +# Format: #/name defines a new extras_require group called name +# Uncommented lines after the group definition insert things into that group. + +#/encryption +python-olm>=3,<4 +pycryptodome>=3,<4 +unpaddedbase64>=1,<3 +base58>=2,<3 + +#/testing +pytest +pytest-asyncio + +#/legacydb +SQLAlchemy>1,<1.4 diff --git a/maubot-src/pyproject.toml b/maubot-src/pyproject.toml new file mode 100644 index 0000000..6c2ca27 --- /dev/null +++ b/maubot-src/pyproject.toml @@ -0,0 +1,13 @@ +[tool.isort] +profile = "black" +force_to_top = "typing" +from_first = true +combine_as_imports = true +known_first_party = "mautrix" +line_length = 99 +skip = ["maubot/management/frontend"] + +[tool.black] +line-length = 99 +target-version = ["py310"] +force-exclude = "maubot/management/frontend" diff --git a/maubot-src/requirements.txt b/maubot-src/requirements.txt new file mode 100644 index 0000000..fd96666 --- /dev/null +++ b/maubot-src/requirements.txt @@ -0,0 +1,16 @@ +mautrix>=0.21.0b5,<0.22 +aiohttp>=3,<4 +yarl>=1,<2 +asyncpg>=0.20,<1 +aiosqlite>=0.16,<1 +commonmark>=0.9,<1 +ruamel.yaml>=0.15.35,<0.19 +attrs>=18.1.0 +bcrypt>=3,<5 +packaging>=10 + +click>=7,<9 +colorama>=0.4,<0.5 +questionary>=1,<3 +jinja2>=2,<4 +setuptools diff --git a/maubot-src/setup.py b/maubot-src/setup.py new file mode 100644 index 0000000..838196f --- /dev/null +++ b/maubot-src/setup.py @@ -0,0 +1,79 @@ +import setuptools +import os + +with open("requirements.txt") as reqs: + install_requires = reqs.read().splitlines() + +with open("optional-requirements.txt") as reqs: + extras_require = {} + current = [] + for line in reqs.read().splitlines(): + if line.startswith("#/"): + extras_require[line[2:]] = current = [] + elif not line or line.startswith("#"): + continue + else: + current.append(line) + +extras_require["all"] = list({dep for deps in extras_require.values() for dep in deps}) + +path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "maubot", "__meta__.py") +__version__ = "UNKNOWN" +with open(path) as f: + exec(f.read()) + +setuptools.setup( + name="maubot", + version=__version__, + url="https://github.com/maubot/maubot", + project_urls={ + "Changelog": "https://github.com/maubot/maubot/blob/master/CHANGELOG.md", + }, + + author="Tulir Asokan", + author_email="tulir@maunium.net", + + description="A plugin-based Matrix bot system.", + long_description=open("README.md").read(), + long_description_content_type="text/markdown", + + packages=setuptools.find_packages(), + + install_requires=install_requires, + extras_require=extras_require, + python_requires="~=3.10", + + classifiers=[ + "Development Status :: 4 - Beta", + "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", + "Topic :: Communications :: Chat", + "Framework :: AsyncIO", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + ], + entry_points=""" + [console_scripts] + mbc=maubot.cli:app + [pytest11] + maubot=maubot.testing + """, + data_files=[ + (".", ["maubot/example-config.yaml"]), + ], + package_data={ + "maubot": [ + "example-config.yaml", + "management/frontend/build/*", + "management/frontend/build/static/css/*", + "management/frontend/build/static/js/*", + "management/frontend/build/static/media/*", + "py.typed", + ], + "maubot.cli": ["res/*"], + "maubot.standalone": ["example-config.yaml"], + }, +) diff --git a/maubot-src/start.sh b/maubot-src/start.sh new file mode 100755 index 0000000..9636c57 --- /dev/null +++ b/maubot-src/start.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +set -euo pipefail + +APP_DIR="/app/code" +DATA_DIR="/app/data" +CONFIG_PATH="${DATA_DIR}/config.yaml" +DEFAULT_CONFIG="/tmp/data/config.yaml" +FRONTEND_DIR="${APP_DIR}/frontend" +ADMIN_FILE="${DATA_DIR}/.admin_password" +MAUBOT_INTERNAL_PORT="${MAUBOT_INTERNAL_PORT:-3001}" + +umask 0027 + +mkdir -p /run/nginx \ + "${DATA_DIR}" \ + "${DATA_DIR}/plugins" \ + "${DATA_DIR}/trash" \ + "${DATA_DIR}/dbs" \ + "${DATA_DIR}/logs" + +if [ ! -f "${CONFIG_PATH}" ]; then + cp "${DEFAULT_CONFIG}" "${CONFIG_PATH}" + echo "Generated initial Maubot configuration at ${CONFIG_PATH}" +fi + +if compgen -G "${DATA_DIR}/plugins/*.db" > /dev/null; then + mv "${DATA_DIR}"/plugins/*.db "${DATA_DIR}/dbs/" || true +fi + +chown -R cloudron:cloudron "${DATA_DIR}" + +export CONFIG_PATH DATA_DIR FRONTEND_DIR ADMIN_FILE +export MAUBOT_PORT="${MAUBOT_INTERNAL_PORT}" +export CLOUDRON_APP_ORIGIN="${CLOUDRON_APP_ORIGIN:-https://example.com}" +export CLOUDRON_POSTGRESQL_URL="${CLOUDRON_POSTGRESQL_URL:-}" +export MAUBOT_CRYPTO_DATABASE="${MAUBOT_CRYPTO_DATABASE:-default}" + +"${APP_DIR}/venv/bin/python3" <<'PY' +import os +import pathlib +import secrets +import string +from ruamel.yaml import YAML + +yaml = YAML() +yaml.preserve_quotes = True + +config_path = pathlib.Path(os.environ["CONFIG_PATH"]) +data_dir = pathlib.Path(os.environ["DATA_DIR"]) +frontend_dir = pathlib.Path(os.environ["FRONTEND_DIR"]) +admin_file = pathlib.Path(os.environ["ADMIN_FILE"]) +public_url = os.environ.get("CLOUDRON_APP_ORIGIN") or "https://example.com" +db_url = os.environ.get("CLOUDRON_POSTGRESQL_URL") +crypto_url = os.environ.get("MAUBOT_CRYPTO_DATABASE", "default") +maubot_port = int(os.environ["MAUBOT_PORT"]) + +with config_path.open() as handle: + config = yaml.load(handle) or {} + +config["database"] = db_url or f"sqlite:{data_dir / 'maubot.db'}" +config["crypto_database"] = "default" if crypto_url in ("", "default") else crypto_url + +plugin_dirs = config.setdefault("plugin_directories", {}) +plugin_dirs["upload"] = str(data_dir / "plugins") +plugin_dirs["load"] = [str(data_dir / "plugins")] +plugin_dirs["trash"] = str(data_dir / "trash") + +plugin_dbs = config.setdefault("plugin_databases", {}) +plugin_dbs["sqlite"] = str(data_dir / "dbs") +plugin_dbs["postgres"] = "default" if db_url else None +plugin_dbs.setdefault("postgres_max_conns_per_plugin", 3) +plugin_dbs.setdefault("postgres_opts", {}) + +server = config.setdefault("server", {}) +server["hostname"] = "127.0.0.1" +server["port"] = maubot_port +server["public_url"] = public_url.rstrip("/") +server.setdefault("ui_base_path", "/") +server.setdefault("plugin_base_path", "/_matrix/maubot/plugin/") +server["override_resource_path"] = str(frontend_dir) + +logging = config.setdefault("logging", {}) +handlers = logging.setdefault("handlers", {}) +file_handler = handlers.setdefault("file", {}) +file_handler["filename"] = str(data_dir / "logs" / "maubot.log") +file_handler.setdefault("class", "logging.handlers.RotatingFileHandler") +file_handler.setdefault("formatter", "normal") +file_handler.setdefault("maxBytes", 10485760) +file_handler.setdefault("backupCount", 10) + +admins = config.setdefault("admins", {}) +if admin_file.exists(): + admin_password = admin_file.read_text().strip() +else: + alphabet = string.ascii_letters + string.digits + admin_password = "".join(secrets.choice(alphabet) for _ in range(40)) + admin_file.write_text(admin_password) + admin_file.chmod(0o640) +admins["root"] = admin_password + +with config_path.open("w") as handle: + yaml.dump(config, handle) +PY + +chown cloudron:cloudron "${CONFIG_PATH}" "${ADMIN_FILE}" + +exec /usr/bin/supervisord -c /app/code/supervisord.conf diff --git a/maubot-src/supervisord.conf b/maubot-src/supervisord.conf new file mode 100644 index 0000000..58d7ccf --- /dev/null +++ b/maubot-src/supervisord.conf @@ -0,0 +1,22 @@ +[supervisord] +nodaemon=true +logfile=/dev/null + +[program:nginx] +command=/usr/sbin/nginx -g "daemon off;" -c /app/code/nginx.conf +autorestart=true +priority=10 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +[program:maubot] +command=/usr/sbin/gosu cloudron:cloudron /app/code/venv/bin/python3 -m maubot -c /app/data/config.yaml +directory=/app/code +autorestart=true +priority=20 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0